Compare commits

...

5 commits

Author SHA1 Message Date
d171b92989
Merge branch 'v6' into docs-beta 2025-04-03 05:39:26 +00:00
172b1ed5e6
.feed will now properly add feeds to the database 2025-04-03 17:56:08 +13:00
8f6ecff5af
Update changelog 2025-04-03 16:31:46 +13:00
94aee4ad10
added some config options .conf fish
adding 4 basic items automatically to fish.yml once user updates and has no items
2025-04-03 16:29:45 +13:00
d910683d78
added hangman category to hangman output - regardless of whether you've selected a category or got a random one. 2025-04-03 15:50:06 +13:00
12 changed files with 230 additions and 99 deletions

View file

@ -2,6 +2,21 @@
*a,c,f,r,o* *a,c,f,r,o*
## [6.1.2] - 03.04.2025
### Fixed
- Fixed `.feed` not adding new feeds to the database
## [6.1.1] - 03.04.2025
### Added
- Added some config options for .conf fish
### Fixed
- Fixed a typo in fish shop
- .fishlb will now compare unique fish caught, instead of total catches
- hangman category now appears in .hangman output
## [6.1.0] - 30.03.2025 ## [6.1.0] - 30.03.2025
### Added ### Added

View file

@ -4,7 +4,7 @@
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<ImplicitUsings>true</ImplicitUsings> <ImplicitUsings>true</ImplicitUsings>
<SatelliteResourceLanguages>en</SatelliteResourceLanguages> <SatelliteResourceLanguages>en</SatelliteResourceLanguages>
<Version>6.1.0</Version> <Version>6.1.2</Version>
<!-- Output/build --> <!-- Output/build -->
<RunWorkingDirectory>$(MSBuildProjectDirectory)</RunWorkingDirectory> <RunWorkingDirectory>$(MSBuildProjectDirectory)</RunWorkingDirectory>

View file

@ -15,17 +15,91 @@ public sealed class FishConfigService : ConfigServiceBase<FishConfig>
IPubSub pubSub) IPubSub pubSub)
: base(FILE_PATH, serializer, pubSub, _changeKey) : base(FILE_PATH, serializer, pubSub, _changeKey)
{ {
AddParsedProp("captcha",
static (conf) => conf.RequireCaptcha,
bool.TryParse,
ConfigPrinters.ToString);
AddParsedProp("chance.nothing",
static (conf) => conf.Chance.Nothing,
int.TryParse,
ConfigPrinters.ToString);
AddParsedProp("chance.fish",
static (conf) => conf.Chance.Fish,
int.TryParse,
ConfigPrinters.ToString);
AddParsedProp("chance.trash",
static (conf) => conf.Chance.Trash,
int.TryParse,
ConfigPrinters.ToString);
Migrate(); Migrate();
} }
private void Migrate() private void Migrate()
{ {
if (data.Version < 2) if (data.Version < 11)
{ {
ModifyConfig(c => ModifyConfig(c =>
{ {
c.Version = 2; c.Version = 11;
c.RequireCaptcha = true; if (c.Items is { Count: > 0 })
return;
c.Items =
[
new FishItem
{
Id = 1,
ItemType = FishItemType.Pole,
Name = "Wooden Rod",
Description = "Better than catching it with bare hands.",
Price = 1000,
FishMultiplier = 1.2
},
new FishItem
{
Id = 11,
ItemType = FishItemType.Pole,
Name = "Magnet on a Stick",
Description = "Attracts all trash, not just metal.",
Price = 3000,
FishMultiplier = 0.9,
TrashMultiplier = 2
},
new FishItem
{
Id = 21,
ItemType = FishItemType.Bait,
Name = "Corn",
Description = "Just some cooked corn.",
Price = 100,
Uses = 100,
RareMultiplier = 1.1
},
new FishItem
{
Id = 31,
ItemType = FishItemType.Potion,
Name = "A Cup of Tea",
Description = "Helps you focus.",
Price = 12000,
DurationMinutes = 30,
MaxStarMultiplier = 1.1,
FishingSpeedMultiplier = 1.01
},
new FishItem
{
Id = 41,
ItemType = FishItemType.Boat,
Name = "Canoe",
Description = "Lets you fish a little faster.",
Price = 3000,
FishingSpeedMultiplier = 1.201,
MaxStarMultiplier = 1.1
}
];
}); });
} }
} }

View file

@ -44,7 +44,7 @@ public sealed class DefaultHangmanSource : IHangmanSource
public IReadOnlyCollection<string> GetCategories() public IReadOnlyCollection<string> GetCategories()
=> termsDict.Keys.ToList(); => termsDict.Keys.ToList();
public bool GetTerm(string? category, [NotNullWhen(true)] out HangmanTerm? term) public bool GetTerm(string? category, [NotNullWhen(true)] out (HangmanTerm Term, string Category)? term)
{ {
if (category is null) if (category is null)
{ {
@ -54,7 +54,7 @@ public sealed class DefaultHangmanSource : IHangmanSource
if (termsDict.TryGetValue(category, out var terms)) if (termsDict.TryGetValue(category, out var terms))
{ {
term = terms[_rng.Next(0, terms.Length)]; term = (terms[_rng.Next(0, terms.Length)], category);
return true; return true;
} }

View file

@ -10,7 +10,8 @@ public partial class Games
[Cmd] [Cmd]
[RequireContext(ContextType.Guild)] [RequireContext(ContextType.Guild)]
public async Task Hangmanlist() public async Task Hangmanlist()
=> await Response().Confirm(GetText(strs.hangman_types(prefix)), _service.GetHangmanTypes().Join('\n')) => await Response()
.Confirm(GetText(strs.hangman_types(prefix)), _service.GetHangmanTypes().Join('\n'))
.SendAsync(); .SendAsync();
private static string Draw(HangmanGame.State state) private static string Draw(HangmanGame.State state)
@ -35,29 +36,25 @@ public partial class Games
public static EmbedBuilder GetEmbed(IMessageSenderService sender, HangmanGame.State state) public static EmbedBuilder GetEmbed(IMessageSenderService sender, HangmanGame.State state)
{ {
var eb = sender.CreateEmbed()
.WithOkColor()
.AddField("Hangman", Draw(state))
.AddField("Guess", Format.Code(state.Word));
if (state.Phase == HangmanGame.Phase.Running) if (state.Phase == HangmanGame.Phase.Running)
{ {
return sender.CreateEmbed() return eb
.WithOkColor() .WithFooter(state.MissedLetters.Join(' '))
.AddField("Hangman", Draw(state)) .WithAuthor(state.Category);
.AddField("Guess", Format.Code(state.Word))
.WithFooter(state.MissedLetters.Join(' '));
} }
if (state.Phase == HangmanGame.Phase.Ended && state.Failed) if (state.Phase == HangmanGame.Phase.Ended && state.Failed)
{ {
return sender.CreateEmbed() return eb
.WithErrorColor()
.AddField("Hangman", Draw(state))
.AddField("Guess", Format.Code(state.Word))
.WithFooter(state.MissedLetters.Join(' ')); .WithFooter(state.MissedLetters.Join(' '));
} }
return sender.CreateEmbed() return eb.WithFooter(state.MissedLetters.Join(' '));
.WithOkColor()
.AddField("Hangman", Draw(state))
.AddField("Guess", Format.Code(state.Word))
.WithFooter(state.MissedLetters.Join(' '));
} }
[Cmd] [Cmd]

View file

@ -4,9 +4,20 @@ namespace EllieBot.Modules.Games.Hangman;
public sealed class HangmanGame public sealed class HangmanGame
{ {
public enum GuessResult { NoAction, AlreadyTried, Incorrect, Guess, Win } public enum GuessResult
{
NoAction,
AlreadyTried,
Incorrect,
Guess,
Win
}
public enum Phase { Running, Ended } public enum Phase
{
Running,
Ended
}
private Phase CurrentPhase { get; set; } private Phase CurrentPhase { get; set; }
@ -17,16 +28,21 @@ public sealed class HangmanGame
private readonly string _word; private readonly string _word;
private readonly string _imageUrl; private readonly string _imageUrl;
public HangmanGame(HangmanTerm term) public string Category { get; }
public HangmanGame(HangmanTerm term, string cat)
{ {
_word = term.Word; _word = term.Word;
_imageUrl = term.ImageUrl; _imageUrl = term.ImageUrl;
Category = cat;
_remaining = _word.ToLowerInvariant().Where(x => char.IsLetter(x)).Select(char.ToLowerInvariant).ToHashSet(); _remaining = _word.ToLowerInvariant().Where(x => char.IsLetter(x)).Select(char.ToLowerInvariant).ToHashSet();
} }
public State GetState(GuessResult guessResult = GuessResult.NoAction) public State GetState(GuessResult guessResult = GuessResult.NoAction)
=> new(_incorrect.Count, => new(
Category,
_incorrect.Count,
CurrentPhase, CurrentPhase,
CurrentPhase == Phase.Ended ? _word : GetScrambledWord(), CurrentPhase == Phase.Ended ? _word : GetScrambledWord(),
guessResult, guessResult,
@ -99,6 +115,7 @@ public sealed class HangmanGame
} }
public record State( public record State(
string Category,
int Errors, int Errors,
Phase Phase, Phase Phase,
string Word, string Word,

View file

@ -36,10 +36,10 @@ public sealed class HangmanService : IHangmanService, IExecNoCommand
public bool StartHangman(ulong channelId, string? category, [NotNullWhen(true)] out HangmanGame.State? state) public bool StartHangman(ulong channelId, string? category, [NotNullWhen(true)] out HangmanGame.State? state)
{ {
state = null; state = null;
if (!_source.GetTerm(category, out var term)) if (!_source.GetTerm(category, out var termData))
return false; return false;
var game = new HangmanGame(term); var game = new HangmanGame(termData.Value.Term, termData.Value.Category);
lock (_locker) lock (_locker)
{ {
var hc = _hangmanGames.GetOrAdd(channelId, game); var hc = _hangmanGames.GetOrAdd(channelId, game);

View file

@ -1,4 +1,6 @@
#nullable disable #nullable disable
using YamlDotNet.Serialization;
namespace EllieBot.Modules.Games.Hangman; namespace EllieBot.Modules.Games.Hangman;
public sealed class HangmanTerm public sealed class HangmanTerm

View file

@ -6,5 +6,5 @@ public interface IHangmanSource : IEService
{ {
public IReadOnlyCollection<string> GetCategories(); public IReadOnlyCollection<string> GetCategories();
public void Reload(); public void Reload();
public bool GetTerm(string? category, [NotNullWhen(true)] out HangmanTerm? term); public bool GetTerm(string? category, [NotNullWhen(true)] out (HangmanTerm Term, string Category)? term);
} }

View file

@ -53,6 +53,8 @@ public partial class Searches
[Priority(1)] [Priority(1)]
public async Task Feed(string url, ITextChannel? channel = null, [Leftover] string? message = null) public async Task Feed(string url, ITextChannel? channel = null, [Leftover] string? message = null)
{ {
await ctx.Channel.TriggerTypingAsync();
if (!Uri.TryCreate(url, UriKind.Absolute, out var uri) if (!Uri.TryCreate(url, UriKind.Absolute, out var uri)
|| (uri.Scheme != Uri.UriSchemeHttp && uri.Scheme != Uri.UriSchemeHttps)) || (uri.Scheme != Uri.UriSchemeHttp && uri.Scheme != Uri.UriSchemeHttps))
{ {
@ -61,7 +63,7 @@ public partial class Searches
} }
channel ??= (ITextChannel)ctx.Channel; channel ??= (ITextChannel)ctx.Channel;
if (!((IGuildUser)ctx.User).GetPermissions(channel).MentionEveryone) if (!((IGuildUser)ctx.User).GetPermissions(channel).MentionEveryone)
message = message?.SanitizeAllMentions(); message = message?.SanitizeAllMentions();
@ -79,7 +81,7 @@ public partial class Searches
if (ctx.User is not IGuildUser gu || !gu.GuildPermissions.Administrator) if (ctx.User is not IGuildUser gu || !gu.GuildPermissions.Administrator)
message = message?.SanitizeMentions(true); message = message?.SanitizeMentions(true);
var result = _service.AddFeed(ctx.Guild.Id, channel.Id, url, message); var result = await _service.AddFeedAsync(ctx.Guild.Id, channel.Id, url, message);
if (result == FeedAddResult.Success) if (result == FeedAddResult.Success)
{ {
await Response().Confirm(strs.feed_added).SendAsync(); await Response().Confirm(strs.feed_added).SendAsync();
@ -117,32 +119,32 @@ public partial class Searches
{ {
if (--page < 0) if (--page < 0)
return; return;
var feeds = _service.GetFeeds(ctx.Guild.Id); var feeds = _service.GetFeeds(ctx.Guild.Id);
if (!feeds.Any()) if (!feeds.Any())
{ {
await Response() await Response()
.Embed(CreateEmbed().WithOkColor().WithDescription(GetText(strs.feed_no_feed))) .Embed(CreateEmbed().WithOkColor().WithDescription(GetText(strs.feed_no_feed)))
.SendAsync(); .SendAsync();
return; return;
} }
await Response() await Response()
.Paginated() .Paginated()
.Items(feeds) .Items(feeds)
.PageSize(10) .PageSize(10)
.CurrentPage(page) .CurrentPage(page)
.Page((items, cur) => .Page((items, cur) =>
{ {
var embed = CreateEmbed().WithOkColor(); var embed = CreateEmbed().WithOkColor();
var i = 0; var i = 0;
var fs = string.Join("\n", var fs = string.Join("\n",
items.Select(x => $"`{(cur * 10) + ++i}.` <#{x.ChannelId}> {x.Url}")); items.Select(x => $"`{(cur * 10) + ++i}.` <#{x.ChannelId}> {x.Url}"));
return embed.WithDescription(fs); return embed.WithDescription(fs);
}) })
.SendAsync(); .SendAsync();
} }
} }
} }

View file

@ -40,13 +40,13 @@ public class FeedsService : IEService, IReadyExecutor
await using (var uow = _db.GetDbContext()) await using (var uow = _db.GetDbContext())
{ {
var subs = await uow.Set<FeedSub>() var subs = await uow.Set<FeedSub>()
.AsQueryable() .AsQueryable()
.Where(x => Queries.GuildOnShard(x.GuildId, _shardData.TotalShards, _shardData.ShardId)) .Where(x => Queries.GuildOnShard(x.GuildId, _shardData.TotalShards, _shardData.ShardId))
.ToListAsyncLinqToDB(); .ToListAsyncLinqToDB();
_subs = subs _subs = subs
.GroupBy(x => x.Url.ToLower()) .GroupBy(x => x.Url.ToLower())
.ToDictionary(x => x.Key, x => x.ToList()) .ToDictionary(x => x.Key, x => x.ToList())
.ToConcurrent(); .ToConcurrent();
} }
await TrackFeeds(); await TrackFeeds();
@ -66,7 +66,7 @@ public class FeedsService : IEService, IReadyExecutor
// remove from db // remove from db
await using var ctx = _db.GetDbContext(); await using var ctx = _db.GetDbContext();
await ctx.GetTable<FeedSub>() await ctx.GetTable<FeedSub>()
.DeleteAsync(x => ids.Contains(x.Id)); .DeleteAsync(x => ids.Contains(x.Id));
// remove from the local cache // remove from the local cache
_subs.TryRemove(url, out _); _subs.TryRemove(url, out _);
@ -163,12 +163,12 @@ public class FeedsService : IEService, IReadyExecutor
if (!gotImage && feedItem.SpecificItem is AtomFeedItem afi) if (!gotImage && feedItem.SpecificItem is AtomFeedItem afi)
{ {
var previewElement = afi.Element.Elements() var previewElement = afi.Element.Elements()
.FirstOrDefault(x => x.Name.LocalName == "preview"); .FirstOrDefault(x => x.Name.LocalName == "preview");
if (previewElement is null) if (previewElement is null)
{ {
previewElement = afi.Element.Elements() previewElement = afi.Element.Elements()
.FirstOrDefault(x => x.Name.LocalName == "thumbnail"); .FirstOrDefault(x => x.Name.LocalName == "thumbnail");
} }
if (previewElement is not null) if (previewElement is not null)
@ -201,11 +201,11 @@ public class FeedsService : IEService, IReadyExecutor
continue; continue;
var sendTask = _sender.Response(ch) var sendTask = _sender.Response(ch)
.Embed(embed) .Embed(embed)
.Text(string.IsNullOrWhiteSpace(val.Message) .Text(string.IsNullOrWhiteSpace(val.Message)
? string.Empty ? string.Empty
: val.Message) : val.Message)
.SendAsync(); .SendAsync();
tasks.Add(sendTask); tasks.Add(sendTask);
} }
@ -236,12 +236,14 @@ public class FeedsService : IEService, IReadyExecutor
using var uow = _db.GetDbContext(); using var uow = _db.GetDbContext();
return uow.GetTable<FeedSub>() return uow.GetTable<FeedSub>()
.Where(x => x.GuildId == guildId) .Where(x => x.GuildId == guildId)
.OrderBy(x => x.Id) .OrderBy(x => x.Id)
.ToList(); .ToList();
} }
public FeedAddResult AddFeed( private const int MAX_FEEDS = 10;
public async Task<FeedAddResult> AddFeedAsync(
ulong guildId, ulong guildId,
ulong channelId, ulong channelId,
string rssFeed, string rssFeed,
@ -249,25 +251,24 @@ public class FeedsService : IEService, IReadyExecutor
{ {
ArgumentNullException.ThrowIfNull(rssFeed, nameof(rssFeed)); ArgumentNullException.ThrowIfNull(rssFeed, nameof(rssFeed));
var fs = new FeedSub await using var uow = _db.GetDbContext();
{ var feedUrl = rssFeed.Trim();
ChannelId = channelId, if (await uow.GetTable<FeedSub>().AnyAsyncLinqToDB(x => x.GuildId == guildId &&
Url = rssFeed.Trim(), x.Url.ToLower() == feedUrl.ToLower()))
Message = message
};
using var uow = _db.GetDbContext();
var feeds = uow.GetTable<FeedSub>()
.Where(x => x.GuildId == guildId)
.ToArray();
if (feeds.Any(x => x.Url.ToLower() == fs.Url.ToLower()))
return FeedAddResult.Duplicate; return FeedAddResult.Duplicate;
if (feeds.Length >= 10)
var count = await uow.GetTable<FeedSub>().CountAsyncLinqToDB(x => x.GuildId == guildId);
if (count >= MAX_FEEDS)
return FeedAddResult.LimitReached; return FeedAddResult.LimitReached;
uow.Add(fs); var fs = await uow.GetTable<FeedSub>()
uow.SaveChanges(); .InsertWithOutputAsync(() => new FeedSub
{
GuildId = guildId,
ChannelId = channelId,
Url = feedUrl,
Message = message,
});
_subs.AddOrUpdate(fs.Url.ToLower(), _subs.AddOrUpdate(fs.Url.ToLower(),
[fs], [fs],
@ -293,10 +294,7 @@ public class FeedsService : IEService, IReadyExecutor
var toRemove = items[index]; var toRemove = items[index];
_subs.AddOrUpdate(toRemove.Url.ToLower(), _subs.AddOrUpdate(toRemove.Url.ToLower(),
[], [],
(_, old) => (_, old) => { return old.Where(x => x.Id != toRemove.Id).ToList(); });
{
return old.Where(x => x.Id != toRemove.Id).ToList();
});
uow.Remove(toRemove); uow.Remove(toRemove);
uow.SaveChanges(); uow.SaveChanges();

View file

@ -1,5 +1,5 @@
# DO NOT CHANGE # DO NOT CHANGE
version: 2 version: 11
weatherSeed: w%29';^eGE)9oWHM(aI9I;%1[.r^z2ZS7ShV,l')o(e%#"hVzb>oxQq^`.&/7srh weatherSeed: w%29';^eGE)9oWHM(aI9I;%1[.r^z2ZS7ShV,l')o(e%#"hVzb>oxQq^`.&/7srh
requireCaptcha: true requireCaptcha: true
starEmojis: starEmojis:
@ -45,40 +45,66 @@ trash:
items: items:
- id: 1 - id: 1
itemType: Pole itemType: Pole
name: "Wooden Rod" name: Wooden Rod
description: "Better than catching it with bare hands." emoji: ''
description: Better than catching it with bare hands.
price: 1000 price: 1000
uses:
durationMinutes:
fishMultiplier: 1.2 fishMultiplier: 1.2
trashMultiplier:
maxStarMultiplier:
rareMultiplier:
fishingSpeedMultiplier:
- id: 11 - id: 11
itemType: Pole itemType: Pole
name: Magnet on a Stick name: Magnet on a Stick
description: "Attracts all trash, not just metal." emoji: ''
description: Attracts all trash, not just metal.
price: 3000 price: 3000
uses:
durationMinutes:
fishMultiplier: 0.9 fishMultiplier: 0.9
trashMultiplier: 2 trashMultiplier: 2
maxStarMultiplier:
rareMultiplier:
fishingSpeedMultiplier:
- id: 21 - id: 21
itemType: Bait itemType: Bait
name: "Corn" name: Corn
description: "Just some cooked corn." emoji: ''
description: Just some cooked corn.
price: 100 price: 100
uses: 100 uses: 100
durationMinutes:
fishMultiplier:
trashMultiplier:
maxStarMultiplier:
rareMultiplier: 1.1 rareMultiplier: 1.1
fishingSpeedMultiplier:
- id: 31 - id: 31
itemType: Potion itemType: Potion
name: "A Cup of Tea" name: A Cup of Tea
description: "Helps you focus." emoji: ''
description: Helps you focus.
price: 12000 price: 12000
uses:
durationMinutes: 30 durationMinutes: 30
fishMultiplier:
trashMultiplier:
maxStarMultiplier: 1.1 maxStarMultiplier: 1.1
rareMultiplier:
fishingSpeedMultiplier: 1.01 fishingSpeedMultiplier: 1.01
- id: 41 - id: 41
itemType: Boat itemType: Boat
name: "Canoe" name: Canoe
description: "Lets you fish a little faster." emoji: ''
description: Lets you fish a little faster.
price: 3000 price: 3000
uses:
durationMinutes:
fishMultiplier:
trashMultiplier:
maxStarMultiplier: 1.1
rareMultiplier:
fishingSpeedMultiplier: 1.201 fishingSpeedMultiplier: 1.201
maxStarMultiplier: 1.1