mirror of
https://gitlab.com/Kwoth/nadekobot.git
synced 2025-09-10 17:28:27 -04:00
Compare commits
10 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
5bee5e63d2 | ||
|
815e318610 | ||
|
634c6c99ee | ||
|
6b37b49439 | ||
|
f42afa7eae | ||
|
ccae1c59e9 | ||
|
b0d8137a7a | ||
|
e78a7d0efa | ||
|
1da19a51f6 | ||
|
91eed9dbd8 |
12
CHANGELOG.md
12
CHANGELOG.md
@@ -2,6 +2,18 @@
|
|||||||
|
|
||||||
Mostly based on [keepachangelog](https://keepachangelog.com/en/1.0.0/) except date format. a-c-f-r-o
|
Mostly based on [keepachangelog](https://keepachangelog.com/en/1.0.0/) except date format. a-c-f-r-o
|
||||||
|
|
||||||
|
## [5.3.4] - 14.01.2025
|
||||||
|
|
||||||
|
## Added
|
||||||
|
|
||||||
|
- Added `.fish` commands
|
||||||
|
- `.fish` - Attempt to catch a fish - different fish live in different places, at different times and during different times of the day
|
||||||
|
- `.fishlist` - Look at your fish catalogue - shows how many of each fish you caught and what was the highest quality - for each caught fish, it also shows its required spot, time of day and weather
|
||||||
|
- `.fishspot` - Shows information about the current fish spot, time of day and weather
|
||||||
|
|
||||||
|
## Fixed
|
||||||
|
|
||||||
|
- `.timely` fixed captcha sometimes generating only 2 characters
|
||||||
|
|
||||||
## [5.3.3] - 15.12.2024
|
## [5.3.3] - 15.12.2024
|
||||||
|
|
||||||
|
88
src/NadekoBot.Tests/FishTests.cs
Normal file
88
src/NadekoBot.Tests/FishTests.cs
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
// using System;
|
||||||
|
// using System.Collections.Generic;
|
||||||
|
// using System.Diagnostics;
|
||||||
|
// using System.IO;
|
||||||
|
// using System.Linq;
|
||||||
|
// using Nadeko.Common;
|
||||||
|
// using NadekoBot.Modules.Games;
|
||||||
|
// using NUnit.Framework;
|
||||||
|
//
|
||||||
|
// namespace NadekoBot.Tests;
|
||||||
|
//
|
||||||
|
// public class FishTests
|
||||||
|
// {
|
||||||
|
// [Test]
|
||||||
|
// public void TestWeather()
|
||||||
|
// {
|
||||||
|
// var fs = new FishService(null, null);
|
||||||
|
//
|
||||||
|
// var rng = new Random();
|
||||||
|
//
|
||||||
|
// // output = @"ro+dD:bN0uVqV3ZOAv6r""EFeA'A]u]uSyz2Qd'r#0Vf:5zOX\VgSsF8LgRCL/uOW";
|
||||||
|
// while (true)
|
||||||
|
// {
|
||||||
|
// var output = "";
|
||||||
|
// for (var i = 0; i < 64; i++)
|
||||||
|
// {
|
||||||
|
// var c = (char)rng.Next(33, 123);
|
||||||
|
// output += c;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// output = "";
|
||||||
|
// var weathers = new List<FishingWeather>();
|
||||||
|
// for (var i = 0; i < 1_000_000; i++)
|
||||||
|
// {
|
||||||
|
// var w = fs.GetWeather(DateTime.UtcNow.AddHours(6 * i), output);
|
||||||
|
// weathers.Add(w);
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// var vals = weathers.GroupBy(x => x)
|
||||||
|
// .ToDictionary(x => x.Key, x => x.Count());
|
||||||
|
//
|
||||||
|
// var str = weathers.Select(x => (int)x).Join("");
|
||||||
|
// var maxLength = MaxLength(str);
|
||||||
|
//
|
||||||
|
// if (maxLength < 12)
|
||||||
|
// {
|
||||||
|
// foreach (var v in vals)
|
||||||
|
// {
|
||||||
|
// Console.WriteLine($"{v.Key}: {v.Value}");
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// Console.WriteLine(output);
|
||||||
|
// Console.WriteLine(maxLength);
|
||||||
|
//
|
||||||
|
// File.WriteAllText("data.txt", weathers.Select(x => (int)x).Join(""));
|
||||||
|
//
|
||||||
|
// break;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // string with same characters
|
||||||
|
// static int MaxLength(String s)
|
||||||
|
// {
|
||||||
|
// int ans = 1, temp = 1;
|
||||||
|
//
|
||||||
|
// // Traverse the string
|
||||||
|
// for (int i = 1; i < s.Length; i++)
|
||||||
|
// {
|
||||||
|
// // If character is same as
|
||||||
|
// // previous increment temp value
|
||||||
|
// if (s[i] == s[i - 1])
|
||||||
|
// {
|
||||||
|
// ++temp;
|
||||||
|
// }
|
||||||
|
// else
|
||||||
|
// {
|
||||||
|
// ans = Math.Max(ans, temp);
|
||||||
|
// temp = 1;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// ans = Math.Max(ans, temp);
|
||||||
|
//
|
||||||
|
// // Return the required answer
|
||||||
|
// return ans;
|
||||||
|
// }
|
||||||
|
// }
|
@@ -1,4 +1,5 @@
|
|||||||
using Nadeko.Common;
|
using System;
|
||||||
|
using Nadeko.Common;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
|
|
||||||
namespace NadekoBot.Tests
|
namespace NadekoBot.Tests
|
||||||
@@ -120,5 +121,12 @@ namespace NadekoBot.Tests
|
|||||||
num = new kwum(int.MaxValue);
|
num = new kwum(int.MaxValue);
|
||||||
Assert.AreEqual("3zzzzzz", num.ToString());
|
Assert.AreEqual("3zzzzzz", num.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestPower()
|
||||||
|
{
|
||||||
|
var num = new kwum((int)Math.Pow(32, 2));
|
||||||
|
Assert.AreEqual("322", num.ToString());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -218,12 +218,12 @@ public sealed class Bot : IBot
|
|||||||
catch (HttpException ex)
|
catch (HttpException ex)
|
||||||
{
|
{
|
||||||
LoginErrorHandler.Handle(ex);
|
LoginErrorHandler.Handle(ex);
|
||||||
Helpers.ReadErrorAndExit(3);
|
Helpers.ReadErrorAndExit(101);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
LoginErrorHandler.Handle(ex);
|
LoginErrorHandler.Handle(ex);
|
||||||
Helpers.ReadErrorAndExit(4);
|
Helpers.ReadErrorAndExit(5);
|
||||||
}
|
}
|
||||||
|
|
||||||
await clientReady.Task.ConfigureAwait(false);
|
await clientReady.Task.ConfigureAwait(false);
|
||||||
@@ -275,7 +275,7 @@ public sealed class Bot : IBot
|
|||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Log.Error(ex, "Error adding services");
|
Log.Error(ex, "Error adding services");
|
||||||
Helpers.ReadErrorAndExit(9);
|
Helpers.ReadErrorAndExit(103);
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.Information("Shard {ShardId} connected in {Elapsed:F2}s",
|
Log.Information("Shard {ShardId} connected in {Elapsed:F2}s",
|
||||||
|
@@ -74,6 +74,9 @@ public abstract class NadekoContext : DbContext
|
|||||||
|
|
||||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
|
// load all entities from current assembly
|
||||||
|
modelBuilder.ApplyConfigurationsFromAssembly(typeof(NadekoContext).Assembly);
|
||||||
|
|
||||||
#region Notify
|
#region Notify
|
||||||
|
|
||||||
modelBuilder.Entity<Notify>(e =>
|
modelBuilder.Entity<Notify>(e =>
|
||||||
|
@@ -17,6 +17,9 @@ public static class MigrationQueries
|
|||||||
|
|
||||||
public static void MigrateSar(MigrationBuilder migrationBuilder)
|
public static void MigrateSar(MigrationBuilder migrationBuilder)
|
||||||
{
|
{
|
||||||
|
if (migrationBuilder.IsNpgsql())
|
||||||
|
return;
|
||||||
|
|
||||||
migrationBuilder.Sql("""
|
migrationBuilder.Sql("""
|
||||||
INSERT INTO GroupName (Number, GuildConfigId)
|
INSERT INTO GroupName (Number, GuildConfigId)
|
||||||
SELECT DISTINCT "Group", GC.Id
|
SELECT DISTINCT "Group", GC.Id
|
||||||
|
4151
src/NadekoBot/Migrations/PostgreSql/20250113135504_fishes.Designer.cs
generated
Normal file
4151
src/NadekoBot/Migrations/PostgreSql/20250113135504_fishes.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
39
src/NadekoBot/Migrations/PostgreSql/20250113135504_fishes.cs
Normal file
39
src/NadekoBot/Migrations/PostgreSql/20250113135504_fishes.cs
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace NadekoBot.Migrations.PostgreSql
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class fishes : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "fishcatch",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
id = table.Column<int>(type: "integer", nullable: false)
|
||||||
|
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||||
|
userid = table.Column<decimal>(type: "numeric(20,0)", nullable: false),
|
||||||
|
fishid = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
count = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
maxstars = table.Column<int>(type: "integer", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("pk_fishcatch", x => x.id);
|
||||||
|
table.UniqueConstraint("ak_fishcatch_userid_fishid", x => new { x.userid, x.fishid });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "fishcatch");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -3374,6 +3374,40 @@ namespace NadekoBot.Migrations.PostgreSql
|
|||||||
b.ToTable("xpshopowneditem", (string)null);
|
b.ToTable("xpshopowneditem", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("NadekoBot.Modules.Games.FishCatch", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<int>("Count")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("count");
|
||||||
|
|
||||||
|
b.Property<int>("FishId")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("fishid");
|
||||||
|
|
||||||
|
b.Property<int>("MaxStars")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("maxstars");
|
||||||
|
|
||||||
|
b.Property<decimal>("UserId")
|
||||||
|
.HasColumnType("numeric(20,0)")
|
||||||
|
.HasColumnName("userid");
|
||||||
|
|
||||||
|
b.HasKey("Id")
|
||||||
|
.HasName("pk_fishcatch");
|
||||||
|
|
||||||
|
b.HasAlternateKey("UserId", "FishId")
|
||||||
|
.HasName("ak_fishcatch_userid_fishid");
|
||||||
|
|
||||||
|
b.ToTable("fishcatch", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("NadekoBot.Services.GreetSettings", b =>
|
modelBuilder.Entity("NadekoBot.Services.GreetSettings", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("Id")
|
b.Property<int>("Id")
|
||||||
|
3198
src/NadekoBot/Migrations/Sqlite/20250113135453_fishes.Designer.cs
generated
Normal file
3198
src/NadekoBot/Migrations/Sqlite/20250113135453_fishes.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
38
src/NadekoBot/Migrations/Sqlite/20250113135453_fishes.cs
Normal file
38
src/NadekoBot/Migrations/Sqlite/20250113135453_fishes.cs
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace NadekoBot.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class fishes : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "FishCatch",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||||
|
.Annotation("Sqlite:Autoincrement", true),
|
||||||
|
UserId = table.Column<ulong>(type: "INTEGER", nullable: false),
|
||||||
|
FishId = table.Column<int>(type: "INTEGER", nullable: false),
|
||||||
|
Count = table.Column<int>(type: "INTEGER", nullable: false),
|
||||||
|
MaxStars = table.Column<int>(type: "INTEGER", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_FishCatch", x => x.Id);
|
||||||
|
table.UniqueConstraint("AK_FishCatch_UserId_FishId", x => new { x.UserId, x.FishId });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "FishCatch");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -2508,6 +2508,31 @@ namespace NadekoBot.Migrations
|
|||||||
b.ToTable("XpShopOwnedItem");
|
b.ToTable("XpShopOwnedItem");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("NadekoBot.Modules.Games.FishCatch", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("Count")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("FishId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("MaxStars")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<ulong>("UserId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasAlternateKey("UserId", "FishId");
|
||||||
|
|
||||||
|
b.ToTable("FishCatch");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("NadekoBot.Services.GreetSettings", b =>
|
modelBuilder.Entity("NadekoBot.Services.GreetSettings", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("Id")
|
b.Property<int>("Id")
|
||||||
|
@@ -59,10 +59,15 @@ public class SelfAssignedRolesService : INService, IReadyExecutor
|
|||||||
},
|
},
|
||||||
_ => new()
|
_ => new()
|
||||||
{
|
{
|
||||||
|
SarGroupId = ctx.GetTable<SarGroup>()
|
||||||
|
.Where(x => x.GuildId == guildId && x.GroupNumber == groupNumber)
|
||||||
|
.Select(x => x.Id)
|
||||||
|
.First()
|
||||||
},
|
},
|
||||||
() => new()
|
() => new()
|
||||||
{
|
{
|
||||||
RoleId = roleId,
|
RoleId = roleId,
|
||||||
|
GuildId = guildId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -280,8 +285,12 @@ public sealed class SarAssignerService : INService, IReadyExecutor
|
|||||||
|
|
||||||
if (item.Group.IsExclusive)
|
if (item.Group.IsExclusive)
|
||||||
{
|
{
|
||||||
var rolesToRemove = item.Group.Roles.Select(x => x.RoleId);
|
var rolesToRemove = item.Group.Roles
|
||||||
await item.User.RemoveRolesAsync(rolesToRemove);
|
.Where(x => item.User.RoleIds.Contains(x.RoleId))
|
||||||
|
.Select(x => x.RoleId)
|
||||||
|
.ToArray();
|
||||||
|
if (rolesToRemove.Length > 0)
|
||||||
|
await item.User.RemoveRolesAsync(rolesToRemove);
|
||||||
}
|
}
|
||||||
|
|
||||||
await item.User.AddRoleAsync(item.RoleId);
|
await item.User.AddRoleAsync(item.RoleId);
|
||||||
|
56
src/NadekoBot/Modules/Games/Fish/CaptchaService.cs
Normal file
56
src/NadekoBot/Modules/Games/Fish/CaptchaService.cs
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
using SixLabors.Fonts;
|
||||||
|
using SixLabors.Fonts.Unicode;
|
||||||
|
using SixLabors.ImageSharp;
|
||||||
|
using SixLabors.ImageSharp.Drawing.Processing;
|
||||||
|
using SixLabors.ImageSharp.PixelFormats;
|
||||||
|
using SixLabors.ImageSharp.Processing;
|
||||||
|
using Color = SixLabors.ImageSharp.Color;
|
||||||
|
|
||||||
|
|
||||||
|
namespace NadekoBot.Modules.Games;
|
||||||
|
|
||||||
|
public sealed class CaptchaService(FontProvider fonts) : INService
|
||||||
|
{
|
||||||
|
private readonly NadekoRandom _rng = new();
|
||||||
|
|
||||||
|
public Image<Rgba32> GetPasswordImage(string password)
|
||||||
|
{
|
||||||
|
var img = new Image<Rgba32>(50, 24);
|
||||||
|
|
||||||
|
var font = fonts.NotoSans.CreateFont(22);
|
||||||
|
var outlinePen = new SolidPen(Color.Black, 0.5f);
|
||||||
|
var strikeoutRun = new RichTextRun
|
||||||
|
{
|
||||||
|
Start = 0,
|
||||||
|
End = password.GetGraphemeCount(),
|
||||||
|
Font = font,
|
||||||
|
StrikeoutPen = new SolidPen(Color.White, 4),
|
||||||
|
TextDecorations = TextDecorations.Strikeout
|
||||||
|
};
|
||||||
|
|
||||||
|
// draw password on the image
|
||||||
|
img.Mutate(x =>
|
||||||
|
{
|
||||||
|
DrawTextExtensions.DrawText(x,
|
||||||
|
new RichTextOptions(font)
|
||||||
|
{
|
||||||
|
HorizontalAlignment = HorizontalAlignment.Center,
|
||||||
|
VerticalAlignment = VerticalAlignment.Center,
|
||||||
|
FallbackFontFamilies = fonts.FallBackFonts,
|
||||||
|
Origin = new(25, 12),
|
||||||
|
TextRuns = [strikeoutRun]
|
||||||
|
},
|
||||||
|
password,
|
||||||
|
Brushes.Solid(Color.White),
|
||||||
|
outlinePen);
|
||||||
|
});
|
||||||
|
|
||||||
|
return img;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string GeneratePassword()
|
||||||
|
{
|
||||||
|
var num = _rng.Next((int)Math.Pow(31, 2), (int)Math.Pow(32, 3));
|
||||||
|
return new kwum(num).ToString();
|
||||||
|
}
|
||||||
|
}
|
27
src/NadekoBot/Modules/Games/Fish/FishCatch.cs
Normal file
27
src/NadekoBot/Modules/Games/Fish/FishCatch.cs
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace NadekoBot.Modules.Games;
|
||||||
|
|
||||||
|
public sealed class FishCatch
|
||||||
|
{
|
||||||
|
[Key]
|
||||||
|
public int Id { get; set; }
|
||||||
|
public ulong UserId { get; set; }
|
||||||
|
public int FishId { get; set; }
|
||||||
|
public int Count { get; set; }
|
||||||
|
public int MaxStars { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class FishCatchConfiguration : IEntityTypeConfiguration<FishCatch>
|
||||||
|
{
|
||||||
|
public void Configure(EntityTypeBuilder<FishCatch> builder)
|
||||||
|
{
|
||||||
|
builder.HasAlternateKey(x => new
|
||||||
|
{
|
||||||
|
x.UserId,
|
||||||
|
x.FishId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
8
src/NadekoBot/Modules/Games/Fish/FishChance.cs
Normal file
8
src/NadekoBot/Modules/Games/Fish/FishChance.cs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
namespace NadekoBot.Modules.Games;
|
||||||
|
|
||||||
|
public sealed class FishChance
|
||||||
|
{
|
||||||
|
public int Fish { get; set; } = 75;
|
||||||
|
public int Trash { get; set; } = 20;
|
||||||
|
public int Nothing { get; set; } = 0;
|
||||||
|
}
|
292
src/NadekoBot/Modules/Games/Fish/FishCommands.cs
Normal file
292
src/NadekoBot/Modules/Games/Fish/FishCommands.cs
Normal file
@@ -0,0 +1,292 @@
|
|||||||
|
using System.Text;
|
||||||
|
using Format = Discord.Format;
|
||||||
|
|
||||||
|
namespace NadekoBot.Modules.Games;
|
||||||
|
|
||||||
|
public partial class Games
|
||||||
|
{
|
||||||
|
public class FishCommands(
|
||||||
|
FishService fs,
|
||||||
|
FishConfigService fcs,
|
||||||
|
IBotCache cache,
|
||||||
|
CaptchaService service) : NadekoModule
|
||||||
|
{
|
||||||
|
private TypedKey<bool> FishingWhitelistKey(ulong userId)
|
||||||
|
=> new($"fishingwhitelist:{userId}");
|
||||||
|
|
||||||
|
[Cmd]
|
||||||
|
public async Task Fish()
|
||||||
|
{
|
||||||
|
var cRes = await cache.GetAsync(FishingWhitelistKey(ctx.User.Id));
|
||||||
|
if (cRes.TryPickT1(out _, out _))
|
||||||
|
{
|
||||||
|
var password = await GetUserCaptcha(ctx.User.Id);
|
||||||
|
var img = service.GetPasswordImage(password);
|
||||||
|
using var stream = await img.ToStreamAsync();
|
||||||
|
var captcha = await Response()
|
||||||
|
.File(stream, "timely.png")
|
||||||
|
.SendAsync();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var userInput = await GetUserInputAsync(ctx.User.Id, ctx.Channel.Id);
|
||||||
|
if (userInput?.ToLowerInvariant() != password?.ToLowerInvariant())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// whitelist the user for 30 minutes
|
||||||
|
await cache.AddAsync(FishingWhitelistKey(ctx.User.Id), true, TimeSpan.FromMinutes(30));
|
||||||
|
// reset the password
|
||||||
|
await ClearUserCaptcha(ctx.User.Id);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_ = captcha.DeleteAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var fishResult = await fs.FishAsync(ctx.User.Id, ctx.Channel.Id);
|
||||||
|
if (fishResult.TryPickT1(out _, out var fishTask))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var currentWeather = fs.GetCurrentWeather();
|
||||||
|
var currentTod = fs.GetTime();
|
||||||
|
var spot = fs.GetSpot(ctx.Channel.Id);
|
||||||
|
|
||||||
|
var msg = await Response()
|
||||||
|
.Embed(CreateEmbed()
|
||||||
|
.WithPendingColor()
|
||||||
|
.WithAuthor(ctx.User)
|
||||||
|
.WithDescription(GetText(strs.fish_waiting))
|
||||||
|
.AddField(GetText(strs.fish_spot), GetSpotEmoji(spot) + " " + spot.ToString(), true)
|
||||||
|
.AddField(GetText(strs.fish_weather),
|
||||||
|
GetWeatherEmoji(currentWeather) + " " + currentWeather,
|
||||||
|
true)
|
||||||
|
.AddField(GetText(strs.fish_tod), GetTodEmoji(currentTod) + " " + currentTod, true))
|
||||||
|
.SendAsync();
|
||||||
|
|
||||||
|
var res = await fishTask;
|
||||||
|
if (res is null)
|
||||||
|
{
|
||||||
|
await Response().Error(strs.fish_nothing).SendAsync();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
await Response()
|
||||||
|
.Embed(CreateEmbed()
|
||||||
|
.WithOkColor()
|
||||||
|
.WithAuthor(ctx.User)
|
||||||
|
.WithDescription(GetText(strs.fish_caught(Format.Bold(res.Fish.Name))))
|
||||||
|
.AddField(GetText(strs.fish_quality), GetStarText(res.Stars, res.Fish.Stars), true)
|
||||||
|
.AddField(GetText(strs.desc), res.Fish.Fluff, true)
|
||||||
|
.WithThumbnailUrl(res.Fish.Image))
|
||||||
|
.SendAsync();
|
||||||
|
|
||||||
|
await msg.DeleteAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Cmd]
|
||||||
|
public async Task FishSpot()
|
||||||
|
{
|
||||||
|
var ws = fs.GetWeatherForPeriods(7);
|
||||||
|
var spot = fs.GetSpot(ctx.Channel.Id);
|
||||||
|
var time = fs.GetTime();
|
||||||
|
|
||||||
|
await Response()
|
||||||
|
.Embed(CreateEmbed()
|
||||||
|
.WithOkColor()
|
||||||
|
.WithDescription(GetText(strs.fish_weather_duration(fs.GetWeatherPeriodDuration())))
|
||||||
|
.AddField(GetText(strs.fish_spot), GetSpotEmoji(spot) + " " + spot, true)
|
||||||
|
.AddField(GetText(strs.fish_tod), GetTodEmoji(time) + " " + time, true)
|
||||||
|
.AddField(GetText(strs.fish_weather_forecast),
|
||||||
|
ws.Select(x => GetWeatherEmoji(x)).Join(""),
|
||||||
|
true))
|
||||||
|
.SendAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Cmd]
|
||||||
|
public async Task Fishlist(int page = 1)
|
||||||
|
{
|
||||||
|
if (--page < 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var fishes = await fs.GetAllFish();
|
||||||
|
|
||||||
|
Log.Information(fishes.Count.ToString());
|
||||||
|
var catches = await fs.GetUserCatches(ctx.User.Id);
|
||||||
|
|
||||||
|
var catchDict = catches.ToDictionary(x => x.FishId, x => x);
|
||||||
|
|
||||||
|
await Response()
|
||||||
|
.Paginated()
|
||||||
|
.Items(fishes)
|
||||||
|
.PageSize(9)
|
||||||
|
.CurrentPage(page)
|
||||||
|
.Page((fs, i) =>
|
||||||
|
{
|
||||||
|
var eb = CreateEmbed()
|
||||||
|
.WithOkColor();
|
||||||
|
|
||||||
|
foreach (var f in fs)
|
||||||
|
{
|
||||||
|
if (catchDict.TryGetValue(f.Id, out var c))
|
||||||
|
{
|
||||||
|
eb.AddField(f.Name,
|
||||||
|
GetFishEmoji(f, c.Count)
|
||||||
|
+ " "
|
||||||
|
+ GetSpotEmoji(f.Spot)
|
||||||
|
+ GetTodEmoji(f.Time)
|
||||||
|
+ GetWeatherEmoji(f.Weather)
|
||||||
|
+ "\n"
|
||||||
|
+ GetStarText(c.MaxStars, f.Stars)
|
||||||
|
+ "\n"
|
||||||
|
+ Format.Italics(f.Fluff),
|
||||||
|
true);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
eb.AddField("?", GetFishEmoji(null, 0) + "\n" + GetStarText(0, f.Stars), true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return eb;
|
||||||
|
})
|
||||||
|
.SendAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetFishEmoji(FishData? fish, int count)
|
||||||
|
{
|
||||||
|
if (fish is null)
|
||||||
|
return "";
|
||||||
|
|
||||||
|
return fish.Emoji + " x" + count;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetSpotEmoji(FishingSpot? spot)
|
||||||
|
{
|
||||||
|
if (spot is not FishingSpot fs)
|
||||||
|
return string.Empty;
|
||||||
|
|
||||||
|
var conf = fcs.Data;
|
||||||
|
|
||||||
|
return conf.SpotEmojis[(int)fs];
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetTodEmoji(FishingTime? fishTod)
|
||||||
|
{
|
||||||
|
return fishTod switch
|
||||||
|
{
|
||||||
|
FishingTime.Night => "🌑",
|
||||||
|
FishingTime.Dawn => "🌅",
|
||||||
|
FishingTime.Dusk => "🌆",
|
||||||
|
FishingTime.Day => "☀️",
|
||||||
|
_ => ""
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetWeatherEmoji(FishingWeather? w)
|
||||||
|
=> w switch
|
||||||
|
{
|
||||||
|
FishingWeather.Rain => "🌧️",
|
||||||
|
FishingWeather.Snow => "❄️",
|
||||||
|
FishingWeather.Storm => "⛈️",
|
||||||
|
FishingWeather.Clear => "☀️",
|
||||||
|
_ => ""
|
||||||
|
};
|
||||||
|
|
||||||
|
private string GetStarText(int resStars, int fishStars)
|
||||||
|
{
|
||||||
|
if (resStars == fishStars)
|
||||||
|
{
|
||||||
|
return MultiplyStars(fcs.Data.StarEmojis[^1], fishStars);
|
||||||
|
}
|
||||||
|
|
||||||
|
var c = fcs.Data;
|
||||||
|
var starsp1 = MultiplyStars(c.StarEmojis[resStars], resStars);
|
||||||
|
var starsp2 = MultiplyStars(c.StarEmojis[0], fishStars - resStars);
|
||||||
|
|
||||||
|
return starsp1 + starsp2;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string MultiplyStars(string starEmoji, int count)
|
||||||
|
{
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
|
||||||
|
for (var i = 0; i < count; i++)
|
||||||
|
{
|
||||||
|
sb.Append(starEmoji);
|
||||||
|
}
|
||||||
|
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static TypedKey<string> CaptchaPasswordKey(ulong userId)
|
||||||
|
=> new($"timely_password:{userId}");
|
||||||
|
|
||||||
|
private async Task<string> GetUserCaptcha(ulong userId)
|
||||||
|
{
|
||||||
|
var pw = await cache.GetOrAddAsync(CaptchaPasswordKey(userId),
|
||||||
|
() =>
|
||||||
|
{
|
||||||
|
var password = service.GeneratePassword();
|
||||||
|
return Task.FromResult(password)!;
|
||||||
|
});
|
||||||
|
|
||||||
|
return pw!;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ValueTask<bool> ClearUserCaptcha(ulong userId)
|
||||||
|
=> cache.RemoveAsync(CaptchaPasswordKey(userId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// public sealed class UserFishStats
|
||||||
|
// {
|
||||||
|
// [Key]
|
||||||
|
// public int Id { get; set; }
|
||||||
|
//
|
||||||
|
// public ulong UserId { get; set; }
|
||||||
|
//
|
||||||
|
// public ulong CommonCatches { get; set; }
|
||||||
|
// public ulong RareCatches { get; set; }
|
||||||
|
// public ulong VeryRareCatches { get; set; }
|
||||||
|
// public ulong EpicCatches { get; set; }
|
||||||
|
//
|
||||||
|
// public ulong CommonMaxCatches { get; set; }
|
||||||
|
// public ulong RareMaxCatches { get; set; }
|
||||||
|
// public ulong VeryRareMaxCatches { get; set; }
|
||||||
|
// public ulong EpicMaxCatches { get; set; }
|
||||||
|
//
|
||||||
|
// public int TotalStars { get; set; }
|
||||||
|
// }
|
||||||
|
|
||||||
|
public enum FishingSpot
|
||||||
|
{
|
||||||
|
Ocean,
|
||||||
|
River,
|
||||||
|
Lake,
|
||||||
|
Swamp,
|
||||||
|
Reef
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum FishingTime
|
||||||
|
{
|
||||||
|
Night,
|
||||||
|
Dawn,
|
||||||
|
Day,
|
||||||
|
Dusk
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum FishingWeather
|
||||||
|
{
|
||||||
|
Clear,
|
||||||
|
Rain,
|
||||||
|
Storm,
|
||||||
|
Snow
|
||||||
|
}
|
19
src/NadekoBot/Modules/Games/Fish/FishConfig.cs
Normal file
19
src/NadekoBot/Modules/Games/Fish/FishConfig.cs
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
using Cloneable;
|
||||||
|
using NadekoBot.Common.Yml;
|
||||||
|
|
||||||
|
namespace NadekoBot.Modules.Games;
|
||||||
|
|
||||||
|
[Cloneable]
|
||||||
|
public sealed partial class FishConfig : ICloneable<FishConfig>
|
||||||
|
{
|
||||||
|
[Comment("DO NOT CHANGE")]
|
||||||
|
public int Version { get; set; } = 1;
|
||||||
|
|
||||||
|
public string WeatherSeed { get; set; } = string.Empty;
|
||||||
|
public List<string> StarEmojis { get; set; } = new();
|
||||||
|
public List<string> SpotEmojis { get; set; } = new();
|
||||||
|
public FishChance Chance { get; set; } = new FishChance();
|
||||||
|
|
||||||
|
public List<FishData> Fish { get; set; } = new();
|
||||||
|
public List<FishData> Trash { get; set; } = new();
|
||||||
|
}
|
19
src/NadekoBot/Modules/Games/Fish/FishConfigService.cs
Normal file
19
src/NadekoBot/Modules/Games/Fish/FishConfigService.cs
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
using NadekoBot.Common.Configs;
|
||||||
|
|
||||||
|
namespace NadekoBot.Modules.Games;
|
||||||
|
|
||||||
|
public sealed class FishConfigService : ConfigServiceBase<FishConfig>
|
||||||
|
{
|
||||||
|
private static string FILE_PATH = "data/fish.yml";
|
||||||
|
private static readonly TypedKey<FishConfig> _changeKey = new("config.fish.updated");
|
||||||
|
|
||||||
|
public override string Name
|
||||||
|
=> "fishing";
|
||||||
|
|
||||||
|
public FishConfigService(
|
||||||
|
IConfigSeria serializer,
|
||||||
|
IPubSub pubSub)
|
||||||
|
: base(FILE_PATH, serializer, pubSub, _changeKey)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
16
src/NadekoBot/Modules/Games/Fish/FishData.cs
Normal file
16
src/NadekoBot/Modules/Games/Fish/FishData.cs
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
namespace NadekoBot.Modules.Games;
|
||||||
|
|
||||||
|
public class FishData
|
||||||
|
{
|
||||||
|
public required int Id { get; set; }
|
||||||
|
public required string Name { get; set; }
|
||||||
|
public FishingWeather? Weather { get; set; }
|
||||||
|
public FishingSpot? Spot { get; set; }
|
||||||
|
public FishingTime? Time { get; set; }
|
||||||
|
public required double Chance { get; set; }
|
||||||
|
public required int Stars { get; set; }
|
||||||
|
public required string Fluff { get; set; }
|
||||||
|
public List<string>? Condition { get; set; }
|
||||||
|
public string? Image { get; init; }
|
||||||
|
public string? Emoji { get; set; }
|
||||||
|
}
|
9
src/NadekoBot/Modules/Games/Fish/FishResult.cs
Normal file
9
src/NadekoBot/Modules/Games/Fish/FishResult.cs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
namespace NadekoBot.Modules.Games;
|
||||||
|
|
||||||
|
public sealed class FishResult
|
||||||
|
{
|
||||||
|
public required FishData Fish { get; init; }
|
||||||
|
public int Stars { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public readonly record struct AlreadyFishing;
|
315
src/NadekoBot/Modules/Games/Fish/FishService.cs
Normal file
315
src/NadekoBot/Modules/Games/Fish/FishService.cs
Normal file
@@ -0,0 +1,315 @@
|
|||||||
|
using LinqToDB;
|
||||||
|
using LinqToDB.EntityFrameworkCore;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
|
||||||
|
namespace NadekoBot.Modules.Games;
|
||||||
|
|
||||||
|
public sealed class FishService(FishConfigService fcs, IBotCache cache, DbService db) : INService
|
||||||
|
{
|
||||||
|
private Random _rng = new Random();
|
||||||
|
|
||||||
|
private static TypedKey<bool> FishingKey(ulong userId)
|
||||||
|
=> new($"fishing:{userId}");
|
||||||
|
|
||||||
|
public async Task<OneOf.OneOf<Task<FishResult?>, AlreadyFishing>> FishAsync(ulong userId, ulong channelId)
|
||||||
|
{
|
||||||
|
var duration = _rng.Next(5, 9);
|
||||||
|
|
||||||
|
if (!await cache.AddAsync(FishingKey(userId), true, TimeSpan.FromSeconds(duration), overwrite: false))
|
||||||
|
{
|
||||||
|
return new AlreadyFishing();
|
||||||
|
}
|
||||||
|
|
||||||
|
return TryFishAsync(userId, channelId, duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<FishResult?> TryFishAsync(ulong userId, ulong channelId, int duration)
|
||||||
|
{
|
||||||
|
var conf = fcs.Data;
|
||||||
|
await Task.Delay(TimeSpan.FromSeconds(duration));
|
||||||
|
|
||||||
|
// first roll whether it's fish, trash or nothing
|
||||||
|
var totalChance = conf.Chance.Fish + conf.Chance.Trash + conf.Chance.Nothing;
|
||||||
|
var typeRoll = _rng.NextDouble() * totalChance;
|
||||||
|
|
||||||
|
if (typeRoll < conf.Chance.Nothing)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var items = typeRoll < conf.Chance.Nothing + conf.Chance.Fish
|
||||||
|
? conf.Fish
|
||||||
|
: conf.Trash;
|
||||||
|
|
||||||
|
return await FishAsyncInternal(userId, channelId, items);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<FishResult?> FishAsyncInternal(ulong userId, ulong channelId, List<FishData> items)
|
||||||
|
{
|
||||||
|
var filteredItems = new List<FishData>();
|
||||||
|
|
||||||
|
var loc = GetSpot(channelId);
|
||||||
|
var time = GetTime();
|
||||||
|
var w = GetWeather(DateTime.UtcNow);
|
||||||
|
|
||||||
|
foreach (var item in items)
|
||||||
|
{
|
||||||
|
if (item.Condition is { Count: > 0 })
|
||||||
|
{
|
||||||
|
if (!item.Condition.Any(x => channelId.ToString().EndsWith(x)))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.Spot is not null && item.Spot != loc)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (item.Time is not null && item.Time != time)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (item.Weather is not null && item.Weather != w)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
filteredItems.Add(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
var maxSum = filteredItems.Sum(x => x.Chance * 100);
|
||||||
|
|
||||||
|
|
||||||
|
var roll = _rng.NextDouble() * maxSum;
|
||||||
|
|
||||||
|
FishResult? caught = null;
|
||||||
|
|
||||||
|
var curSum = 0d;
|
||||||
|
foreach (var i in filteredItems)
|
||||||
|
{
|
||||||
|
curSum += i.Chance * 100;
|
||||||
|
|
||||||
|
if (roll < curSum)
|
||||||
|
{
|
||||||
|
caught = new FishResult()
|
||||||
|
{
|
||||||
|
Fish = i,
|
||||||
|
Stars = GetRandomStars(i.Stars),
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (caught is not null)
|
||||||
|
{
|
||||||
|
await using var uow = db.GetDbContext();
|
||||||
|
|
||||||
|
await uow.GetTable<FishCatch>()
|
||||||
|
.InsertOrUpdateAsync(() => new FishCatch()
|
||||||
|
{
|
||||||
|
UserId = userId,
|
||||||
|
FishId = caught.Fish.Id,
|
||||||
|
MaxStars = caught.Stars,
|
||||||
|
Count = 1
|
||||||
|
},
|
||||||
|
(old) => new()
|
||||||
|
{
|
||||||
|
Count = old.Count + 1,
|
||||||
|
MaxStars = Math.Max((int)old.MaxStars, caught.Stars),
|
||||||
|
},
|
||||||
|
() => new()
|
||||||
|
{
|
||||||
|
FishId = caught.Fish.Id,
|
||||||
|
UserId = userId
|
||||||
|
});
|
||||||
|
|
||||||
|
return caught;
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.Error(
|
||||||
|
"Something went wrong in the fish command, no fish with sufficient chance was found, Roll: {Roll}, MaxSum: {MaxSum}",
|
||||||
|
roll,
|
||||||
|
maxSum);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public FishingSpot GetSpot(ulong channelId)
|
||||||
|
{
|
||||||
|
var cid = (channelId >> 22 >> 8) % 10;
|
||||||
|
|
||||||
|
return cid switch
|
||||||
|
{
|
||||||
|
< 1 => FishingSpot.Reef,
|
||||||
|
< 3 => FishingSpot.River,
|
||||||
|
< 5 => FishingSpot.Lake,
|
||||||
|
< 7 => FishingSpot.Swamp,
|
||||||
|
_ => FishingSpot.Ocean,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public FishingTime GetTime()
|
||||||
|
{
|
||||||
|
var hour = DateTime.UtcNow.Hour % 12;
|
||||||
|
|
||||||
|
if (hour < 3)
|
||||||
|
return FishingTime.Night;
|
||||||
|
|
||||||
|
if (hour < 4)
|
||||||
|
return FishingTime.Dawn;
|
||||||
|
|
||||||
|
if (hour < 11)
|
||||||
|
return FishingTime.Day;
|
||||||
|
|
||||||
|
return FishingTime.Dusk;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private const int WEATHER_PERIODS_PER_DAY = 12;
|
||||||
|
|
||||||
|
public IReadOnlyList<FishingWeather> GetWeatherForPeriods(int periods)
|
||||||
|
{
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
var result = new FishingWeather[periods];
|
||||||
|
|
||||||
|
for (var i = 0; i < periods; i++)
|
||||||
|
{
|
||||||
|
result[i] = GetWeather(now.AddHours(i * GetWeatherPeriodDuration()));
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public FishingWeather GetCurrentWeather()
|
||||||
|
=> GetWeather(DateTime.UtcNow);
|
||||||
|
|
||||||
|
public FishingWeather GetWeather(DateTime time)
|
||||||
|
=> GetWeather(time, fcs.Data.WeatherSeed);
|
||||||
|
|
||||||
|
private FishingWeather GetWeather(DateTime time, string seed)
|
||||||
|
{
|
||||||
|
var year = time.Year;
|
||||||
|
var dayOfYear = time.DayOfYear;
|
||||||
|
var hour = time.Hour;
|
||||||
|
|
||||||
|
var num = (year * 100_000) + (dayOfYear * 100) + (hour / GetWeatherPeriodDuration());
|
||||||
|
|
||||||
|
Span<byte> dataArray = stackalloc byte[4];
|
||||||
|
BitConverter.TryWriteBytes(dataArray, num);
|
||||||
|
|
||||||
|
Span<byte> seedArray = stackalloc byte[seed.Length];
|
||||||
|
for (var index = 0; index < seed.Length; index++)
|
||||||
|
{
|
||||||
|
var c = seed[index];
|
||||||
|
seedArray[index] = (byte)c;
|
||||||
|
}
|
||||||
|
|
||||||
|
Span<byte> arr = stackalloc byte[dataArray.Length + seedArray.Length];
|
||||||
|
|
||||||
|
dataArray.CopyTo(arr);
|
||||||
|
seedArray.CopyTo(arr[dataArray.Length..]);
|
||||||
|
|
||||||
|
using var algo = SHA512.Create();
|
||||||
|
|
||||||
|
Span<byte> hash = stackalloc byte[64];
|
||||||
|
algo.TryComputeHash(arr, hash, out _);
|
||||||
|
|
||||||
|
byte reduced = 0;
|
||||||
|
foreach (var u in hash)
|
||||||
|
reduced ^= u;
|
||||||
|
|
||||||
|
var r = reduced % 16;
|
||||||
|
|
||||||
|
// return (FishingWeather)r;
|
||||||
|
return r switch
|
||||||
|
{
|
||||||
|
< 5 => FishingWeather.Clear,
|
||||||
|
< 9 => FishingWeather.Rain,
|
||||||
|
< 13 => FishingWeather.Storm,
|
||||||
|
_ => FishingWeather.Snow
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns a random number of stars between 1 and maxStars
|
||||||
|
/// if maxStars == 1, returns 1
|
||||||
|
/// if maxStars == 2, returns 1 (66%) or 2 (33%)
|
||||||
|
/// if maxStars == 3, returns 1 (65%) or 2 (25%) or 3 (10%)
|
||||||
|
/// if maxStars == 5, returns 1 (40%) or 2 (30%) or 3 (15%) or 4 (10%) or 5 (5%)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="maxStars">Max Number of stars to generate</param>
|
||||||
|
/// <returns>Random number of stars</returns>
|
||||||
|
private int GetRandomStars(int maxStars)
|
||||||
|
{
|
||||||
|
if (maxStars == 1)
|
||||||
|
return 1;
|
||||||
|
|
||||||
|
if (maxStars == 2)
|
||||||
|
{
|
||||||
|
// 15% chance of 1 star, 85% chance of 2 stars
|
||||||
|
return _rng.NextDouble() < 0.85 ? 1 : 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (maxStars == 3)
|
||||||
|
{
|
||||||
|
// 65% chance of 1 star, 30% chance of 2 stars, 5% chance of 3 stars
|
||||||
|
var r = _rng.NextDouble();
|
||||||
|
if (r < 0.65)
|
||||||
|
return 1;
|
||||||
|
if (r < 0.95)
|
||||||
|
return 2;
|
||||||
|
return 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (maxStars == 4)
|
||||||
|
{
|
||||||
|
// this should never happen
|
||||||
|
// 50% chance of 1 star, 25% chance of 2 stars, 18% chance of 3 stars, 7% chance of 4 stars
|
||||||
|
var r = _rng.NextDouble();
|
||||||
|
if (r < 0.55)
|
||||||
|
return 1;
|
||||||
|
if (r < 0.80)
|
||||||
|
return 2;
|
||||||
|
if (r < 0.98)
|
||||||
|
return 3;
|
||||||
|
return 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (maxStars == 5)
|
||||||
|
{
|
||||||
|
// 40% chance of 1 star, 30% chance of 2 stars, 15% chance of 3 stars, 10% chance of 4 stars, 5% chance of 5 stars
|
||||||
|
var r = _rng.NextDouble();
|
||||||
|
if (r < 0.4)
|
||||||
|
return 1;
|
||||||
|
if (r < 0.7)
|
||||||
|
return 2;
|
||||||
|
if (r < 0.9)
|
||||||
|
return 3;
|
||||||
|
if (r < 0.98)
|
||||||
|
return 4;
|
||||||
|
return 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int GetWeatherPeriodDuration()
|
||||||
|
=> 24 / WEATHER_PERIODS_PER_DAY;
|
||||||
|
|
||||||
|
public async Task<List<FishData>> GetAllFish()
|
||||||
|
{
|
||||||
|
await Task.Yield();
|
||||||
|
|
||||||
|
var conf = fcs.Data;
|
||||||
|
return conf.Fish.Concat(conf.Trash).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<FishCatch>> GetUserCatches(ulong userId)
|
||||||
|
{
|
||||||
|
await using var ctx = db.GetDbContext();
|
||||||
|
|
||||||
|
var catches = await ctx.GetTable<FishCatch>()
|
||||||
|
.Where(x => x.UserId == userId)
|
||||||
|
.ToListAsyncLinqToDB();
|
||||||
|
|
||||||
|
return catches;
|
||||||
|
}
|
||||||
|
}
|
@@ -4,7 +4,7 @@
|
|||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<ImplicitUsings>true</ImplicitUsings>
|
<ImplicitUsings>true</ImplicitUsings>
|
||||||
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
|
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
|
||||||
<Version>5.3.3</Version>
|
<Version>5.3.4</Version>
|
||||||
|
|
||||||
<!-- Output/build -->
|
<!-- Output/build -->
|
||||||
<RunWorkingDirectory>$(MSBuildProjectDirectory)</RunWorkingDirectory>
|
<RunWorkingDirectory>$(MSBuildProjectDirectory)</RunWorkingDirectory>
|
||||||
|
@@ -16,7 +16,7 @@ public sealed class NadekoRandom : Random
|
|||||||
_rng.GetBytes(bytes);
|
_rng.GetBytes(bytes);
|
||||||
return Math.Abs(BitConverter.ToInt32(bytes, 0));
|
return Math.Abs(BitConverter.ToInt32(bytes, 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Generates a random integer between 0 (inclusive) and
|
/// Generates a random integer between 0 (inclusive) and
|
||||||
/// a specified exclusive upper bound using a cryptographically strong random number generator.
|
/// a specified exclusive upper bound using a cryptographically strong random number generator.
|
||||||
@@ -54,13 +54,9 @@ public sealed class NadekoRandom : Random
|
|||||||
{
|
{
|
||||||
var bytes = new byte[sizeof(double)];
|
var bytes = new byte[sizeof(double)];
|
||||||
_rng.GetBytes(bytes);
|
_rng.GetBytes(bytes);
|
||||||
return Math.Abs((BitConverter.ToDouble(bytes, 0) / double.MaxValue) + 1);
|
return Math.Abs((BitConverter.ToDouble(bytes, 0) / (double.MaxValue + 1)));
|
||||||
}
|
}
|
||||||
|
|
||||||
public override double NextDouble()
|
public override double NextDouble()
|
||||||
{
|
=> Sample();
|
||||||
var bytes = new byte[sizeof(double)];
|
|
||||||
_rng.GetBytes(bytes);
|
|
||||||
return BitConverter.ToDouble(bytes, 0);
|
|
||||||
}
|
|
||||||
}
|
}
|
@@ -82,7 +82,7 @@ public sealed class BotCredsProvider : IBotCredsProvider
|
|||||||
if (string.IsNullOrWhiteSpace(_creds.Token))
|
if (string.IsNullOrWhiteSpace(_creds.Token))
|
||||||
{
|
{
|
||||||
Log.Error("Token is missing from creds.yml or Environment variables.\nAdd it and restart the program");
|
Log.Error("Token is missing from creds.yml or Environment variables.\nAdd it and restart the program");
|
||||||
Helpers.ReadErrorAndExit(5);
|
Helpers.ReadErrorAndExit(1);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -107,7 +107,7 @@ public class RemoteGrpcCoordinator : ICoordinator, IReadyExecutor
|
|||||||
await Task.Delay(7500);
|
await Task.Delay(7500);
|
||||||
}
|
}
|
||||||
|
|
||||||
Environment.Exit(5);
|
Environment.Exit(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
|
@@ -33,7 +33,7 @@ public class SingleProcessCoordinator : ICoordinator
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void Die(bool graceful = false)
|
public void Die(bool graceful = false)
|
||||||
=> Environment.Exit(5);
|
=> Environment.Exit(0);
|
||||||
|
|
||||||
public bool RestartShard(int shardId)
|
public bool RestartShard(int shardId)
|
||||||
=> RestartBot();
|
=> RestartBot();
|
||||||
|
@@ -1559,4 +1559,17 @@ notifyclear:
|
|||||||
- notifclr
|
- notifclr
|
||||||
winlb:
|
winlb:
|
||||||
- winlb
|
- winlb
|
||||||
- wins
|
- wins
|
||||||
|
fish:
|
||||||
|
- fish
|
||||||
|
- fi
|
||||||
|
fishlist:
|
||||||
|
- fishlist
|
||||||
|
- fili
|
||||||
|
- fishes
|
||||||
|
- fil
|
||||||
|
- fishlist
|
||||||
|
fishspot:
|
||||||
|
- fishspot
|
||||||
|
- fisp
|
||||||
|
- fish?
|
43
src/NadekoBot/data/fish.yml
Normal file
43
src/NadekoBot/data/fish.yml
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
# DO NOT CHANGE
|
||||||
|
version: 1
|
||||||
|
weatherSeed: "w%29';^eGE)9oWHM(aI9I;%1[.r^z2ZS7ShV,l')o(e%#\"hVzb>oxQq^`.&/7srh"
|
||||||
|
chance:
|
||||||
|
fish: 80
|
||||||
|
trash: 15
|
||||||
|
nothing: 5
|
||||||
|
starEmojis:
|
||||||
|
- <:emptystar:1326838565786877962>
|
||||||
|
- <:onestar:1326838456739168361>
|
||||||
|
- <:twostar:1326838508198957107>
|
||||||
|
- <:threestar:1326838525601251429>
|
||||||
|
- <:fourstar:1326838552520294462>
|
||||||
|
spotEmojis:
|
||||||
|
- <:ocean:1328519734953771120>
|
||||||
|
- <:river:1328519754620862504>
|
||||||
|
- <:lake:1328315260561788989>
|
||||||
|
- <:swamp:1328519766083633224>
|
||||||
|
- <:reef:1328519744646545421>
|
||||||
|
fish:
|
||||||
|
- name: Bass
|
||||||
|
id: 0
|
||||||
|
weather:
|
||||||
|
spot:
|
||||||
|
time:
|
||||||
|
chance: 100
|
||||||
|
stars: 4
|
||||||
|
fluff: Very common.
|
||||||
|
condition:
|
||||||
|
image: https://cdn.nadeko.bot/fish/bass.png
|
||||||
|
emoji: "<:bass:1328520376892002386>"
|
||||||
|
trash:
|
||||||
|
- name: Plastic Bag
|
||||||
|
id: 1002
|
||||||
|
weather:
|
||||||
|
spot:
|
||||||
|
time:
|
||||||
|
chance: 50
|
||||||
|
stars: 4
|
||||||
|
fluff: "Trophy of your contribution to the environment."
|
||||||
|
condition:
|
||||||
|
image: https://cdn.nadeko.bot/fish/plasticbag.png
|
||||||
|
emoji: "<:plasticbag:1328520895454515211>"
|
@@ -4904,4 +4904,30 @@ winlb:
|
|||||||
- '5'
|
- '5'
|
||||||
params:
|
params:
|
||||||
- page:
|
- page:
|
||||||
desc: "The optional page to display."
|
desc: "The optional page to display."
|
||||||
|
fish:
|
||||||
|
desc: |-
|
||||||
|
Attempt to catch a fish.
|
||||||
|
Different fish live in different places, at different times of day and in different weather.
|
||||||
|
ex:
|
||||||
|
- ''
|
||||||
|
params:
|
||||||
|
- { }
|
||||||
|
fishlist:
|
||||||
|
desc: |-
|
||||||
|
Look at your fish catalogue.
|
||||||
|
Shows how many of each fish you caught and what was the highest quality.
|
||||||
|
For each caught fish, it also shows its required spot, time of day and weather.
|
||||||
|
ex:
|
||||||
|
- ''
|
||||||
|
params:
|
||||||
|
- { }
|
||||||
|
- page:
|
||||||
|
desc: "The optional page to display."
|
||||||
|
fishspot:
|
||||||
|
desc: |-
|
||||||
|
Shows information about the current fish spot, weather and time.
|
||||||
|
ex:
|
||||||
|
- ''
|
||||||
|
params:
|
||||||
|
- { }
|
@@ -1159,5 +1159,15 @@
|
|||||||
"notify_desc_removerolerew": "Triggers when a user loses a role as a reward for reaching a level (xprew).",
|
"notify_desc_removerolerew": "Triggers when a user loses a role as a reward for reaching a level (xprew).",
|
||||||
"notify_desc_not_found": "No description found for this notify event. Please report this.",
|
"notify_desc_not_found": "No description found for this notify event. Please report this.",
|
||||||
"winlb": "Biggest Wins Leaderboard",
|
"winlb": "Biggest Wins Leaderboard",
|
||||||
"no_banner": "No banner set."
|
"no_banner": "No banner set.",
|
||||||
|
"fish_nothing": "You caught nothing, try again.",
|
||||||
|
"fish_caught": "You caught a {0}!",
|
||||||
|
"fish_quality": "Quality",
|
||||||
|
"fish_spot": "Spot",
|
||||||
|
"fish_waiting": "Fishing...",
|
||||||
|
"fish_weather": "Weather",
|
||||||
|
"fish_weather_duration": "Each weather period lasts for {0} hours.",
|
||||||
|
"fish_weather_current": "Current",
|
||||||
|
"fish_weather_forecast": "Forecast",
|
||||||
|
"fish_tod": "Time of Day"
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user