mirror of
https://gitlab.com/Kwoth/nadekobot.git
synced 2025-09-11 17:58:26 -04:00
Compare commits
37 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
7d5c4666b8 | ||
|
8d3f2f186a | ||
|
9572b9dc43 | ||
|
ca32086089 | ||
|
57f839dbcd | ||
|
71d6eeb9dd | ||
|
8c51cf8537 | ||
|
b683026cf3 | ||
|
bca2bc5af1 | ||
|
b385a83bdd | ||
|
3bf0286c81 | ||
|
98272f66e7 | ||
|
cf3788c6ea | ||
|
4b3fc53cb6 | ||
|
4e17dca856 | ||
|
82d89148f3 | ||
|
cc4c09b4d7 | ||
|
616f01f8b2 | ||
|
56f89a02bc | ||
|
48ce988d20 | ||
|
119b1cdec2 | ||
|
43fa5a22f5 | ||
|
3c715a29ca | ||
|
31e1cbb19f | ||
|
8e464e9f09 | ||
|
a190a3d933 | ||
|
bedba98130 | ||
|
d69f8435f6 | ||
|
8440b34338 | ||
|
a2715740c1 | ||
|
36b7fd2352 | ||
|
6563cb507a | ||
|
cc6128997e | ||
|
e942da4470 | ||
|
ab2bcdf00d | ||
|
d7c1dad4f0 | ||
|
8bd6b887b8 |
@@ -1,6 +1,11 @@
|
|||||||
# Ignore all files
|
# Ignore all files
|
||||||
*
|
*
|
||||||
|
|
||||||
|
# Don't ignore nugetconfig
|
||||||
|
!./NuGet.Config
|
||||||
|
# Don't ignore src projects
|
||||||
|
!src/Nadeko.Econ/**
|
||||||
|
!src/Nadeko.Common/**
|
||||||
# Use Nadeko.Medusa project
|
# Use Nadeko.Medusa project
|
||||||
!src/Nadeko.Medusa/**
|
!src/Nadeko.Medusa/**
|
||||||
# Use NadekoBot project
|
# Use NadekoBot project
|
||||||
|
46
CHANGELOG.md
46
CHANGELOG.md
@@ -2,6 +2,47 @@
|
|||||||
|
|
||||||
Experimental changelog. Mostly based on [keepachangelog](https://keepachangelog.com/en/1.0.0/) except date format. a-c-f-r-o
|
Experimental changelog. Mostly based on [keepachangelog](https://keepachangelog.com/en/1.0.0/) except date format. a-c-f-r-o
|
||||||
|
|
||||||
|
## Unreleased
|
||||||
|
|
||||||
|
## [4.3.3] - 06.08.2022
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added `betroll` option to `.bettest` command
|
||||||
|
- Added `.xpshopbuy` and `.xpshopuse` convenience commands
|
||||||
|
- Added an optional preview url to teh xp shop item config model which will be shown instead of the real Url
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Updated position of Username and Club name on the .xp card
|
||||||
|
- Improved text visibility on the .xp card
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Possibly fixed .trivia not stopping bug
|
||||||
|
- Fixed very low payout rate on `.betroll`
|
||||||
|
- Fixed an issue with youtube song resolver which caused invalid data to be cached
|
||||||
|
- Added client id to the cache key as a potential fix for VoiceXp 'bug'. The solution may be to use different redis instances for each bot, or to switch from botCache: from 'redis' to 'memory' in creds.yml
|
||||||
|
- Bot owner should now be able to buy items from the xpshop when patron requirement is set
|
||||||
|
- Fixed youtube-dl caching invalid data. Please use yt-dlp instead
|
||||||
|
|
||||||
|
## [4.3.2] - 28.07.2022
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed Reaction Roles not working properly with animated emojis
|
||||||
|
- Fixed `.slot` alignment
|
||||||
|
- Fixed `mysql` and `postgresql` reactionrole migration
|
||||||
|
- Fixed repeat loop with `postgresql` db provider
|
||||||
|
- Fixed `.bank withdraw <expression>` will now correctly use bank amount for calculation
|
||||||
|
- [dev] Fixed medusa Reply*LocalizedAsync not working with placeholders
|
||||||
|
|
||||||
|
## [4.3.1] - 27.07.2022
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Check for updates will run once per hour as it was supposed to
|
||||||
|
|
||||||
## [4.3.0] - 27.07.2022
|
## [4.3.0] - 27.07.2022
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
@@ -15,9 +56,10 @@ Experimental changelog. Mostly based on [keepachangelog](https://keepachangelog.
|
|||||||
- They payouts are very good, but seven always loses
|
- They payouts are very good, but seven always loses
|
||||||
- Added `.lula` command. Plays the same as `.wof` but looks much nicer, and is easily customizable from gambling.yml without any changes to the sourcecode needed.
|
- Added `.lula` command. Plays the same as `.wof` but looks much nicer, and is easily customizable from gambling.yml without any changes to the sourcecode needed.
|
||||||
- Added `.repeatskip` command which makes the next repeat trigger not post anything
|
- Added `.repeatskip` command which makes the next repeat trigger not post anything
|
||||||
- Added `.imageonly` which will make the bot only allow link posts in the channel. Exclusive with `.imageonly`
|
- Added `.linkonly` which will make the bot only allow link posts in the channel. Exclusive with `.imageonly`
|
||||||
- Added release notifications. Bot owners will now receive new release notifications in dms if they have `checkForUpdates` set to `true` in data/bot.yml
|
- Added release notifications. Bot owners will now receive new release notifications in dms if they have `checkForUpdates` set to `true` in data/bot.yml
|
||||||
- You can also configure it via `.conf bot checkforupdates <true/false>`
|
- You can also configure it via `.conf bot checkfor
|
||||||
|
- updates <true/false>`
|
||||||
- Added `.xpshop` which lets bot owners add xp backgrounds and xp frames for sale by configuring `data/xp.yml`
|
- Added `.xpshop` which lets bot owners add xp backgrounds and xp frames for sale by configuring `data/xp.yml`
|
||||||
- You can also toggle xpshop feature via `.conf xp shop.is_enabled`
|
- You can also toggle xpshop feature via `.conf xp shop.is_enabled`
|
||||||
|
|
||||||
|
@@ -2,10 +2,13 @@ FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build
|
|||||||
WORKDIR /source
|
WORKDIR /source
|
||||||
|
|
||||||
COPY src/Nadeko.Medusa/*.csproj src/Nadeko.Medusa/
|
COPY src/Nadeko.Medusa/*.csproj src/Nadeko.Medusa/
|
||||||
|
COPY src/Nadeko.Econ/*.csproj src/Nadeko.Econ/
|
||||||
|
COPY src/Nadeko.Common/*.csproj src/Nadeko.Common/
|
||||||
COPY src/NadekoBot/*.csproj src/NadekoBot/
|
COPY src/NadekoBot/*.csproj src/NadekoBot/
|
||||||
COPY src/NadekoBot.Coordinator/*.csproj src/NadekoBot.Coordinator/
|
COPY src/NadekoBot.Coordinator/*.csproj src/NadekoBot.Coordinator/
|
||||||
COPY src/NadekoBot.Generators/*.csproj src/NadekoBot.Generators/
|
COPY src/NadekoBot.Generators/*.csproj src/NadekoBot.Generators/
|
||||||
COPY src/ayu/Ayu.Discord.Voice/*.csproj src/ayu/Ayu.Discord.Voice/
|
COPY src/ayu/Ayu.Discord.Voice/*.csproj src/ayu/Ayu.Discord.Voice/
|
||||||
|
COPY NuGet.Config ./
|
||||||
RUN dotnet restore src/NadekoBot/
|
RUN dotnet restore src/NadekoBot/
|
||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
@@ -39,6 +42,7 @@ COPY docker-entrypoint.sh /usr/local/sbin
|
|||||||
|
|
||||||
ENV shard_id=0
|
ENV shard_id=0
|
||||||
ENV total_shards=1
|
ENV total_shards=1
|
||||||
|
ENV NadekoBot__creds=/app/data/creds.yml
|
||||||
|
|
||||||
VOLUME [ "/app/data" ]
|
VOLUME [ "/app/data" ]
|
||||||
ENTRYPOINT [ "/usr/local/sbin/docker-entrypoint.sh" ]
|
ENTRYPOINT [ "/usr/local/sbin/docker-entrypoint.sh" ]
|
||||||
|
@@ -13,7 +13,15 @@ do
|
|||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
# fix folder permissions
|
# creds.yml migration
|
||||||
|
if [ -f /app/creds.yml ]; then
|
||||||
|
echo "Default location for creds.yml is now /app/data/creds.yml."
|
||||||
|
echo "Please move your creds.yml and update your docker-compose.yml accordingly."
|
||||||
|
|
||||||
|
export Nadeko_creds=/app/creds.yml
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ensure nadeko can write on /app/data
|
||||||
chown -R nadeko:nadeko "$data"
|
chown -R nadeko:nadeko "$data"
|
||||||
|
|
||||||
# drop to regular user and launch command
|
# drop to regular user and launch command
|
||||||
|
@@ -69,7 +69,7 @@ Open Terminal (if you're on an installation with a window manager) and navigate
|
|||||||
|
|
||||||
###### Prerequisites
|
###### Prerequisites
|
||||||
|
|
||||||
1. Nadeko requires redis to function
|
1. (Optional) Installing Redis
|
||||||
- ubuntu installation command: `sudo apt-get install redis-server`
|
- ubuntu installation command: `sudo apt-get install redis-server`
|
||||||
2. Playing music requires `ffmpeg`, `libopus`, `libsodium` and `youtube-dl` (which in turn requires python3)
|
2. Playing music requires `ffmpeg`, `libopus`, `libsodium` and `youtube-dl` (which in turn requires python3)
|
||||||
- ubuntu installation command: `sudo apt-get install ffmpeg libopus0 opus-tools libopus-dev libsodium-dev -y`
|
- ubuntu installation command: `sudo apt-get install ffmpeg libopus0 opus-tools libopus-dev libsodium-dev -y`
|
||||||
|
@@ -19,7 +19,7 @@
|
|||||||
|
|
||||||
**Optional**
|
**Optional**
|
||||||
|
|
||||||
- [Notepad++] (makes it easier to edit your credentials)
|
- [Visual Studio Code](https://code.visualstudio.com/Download) (Highly suggested if you plan on editing files)
|
||||||
- [Visual C++ 2010 (x86)] and [Visual C++ 2017 (x64)] (both are required if you want Nadeko to play music - restart Windows after installation)
|
- [Visual C++ 2010 (x86)] and [Visual C++ 2017 (x64)] (both are required if you want Nadeko to play music - restart Windows after installation)
|
||||||
|
|
||||||
#### Setup
|
#### Setup
|
||||||
|
@@ -26,20 +26,6 @@ public class NadekoRandom : Random
|
|||||||
return Math.Abs(BitConverter.ToInt32(bytes, 0)) % maxValue;
|
return Math.Abs(BitConverter.ToInt32(bytes, 0)) % maxValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
public byte Next(byte minValue, byte maxValue)
|
|
||||||
{
|
|
||||||
if (minValue > maxValue)
|
|
||||||
throw new ArgumentOutOfRangeException(nameof(maxValue));
|
|
||||||
|
|
||||||
if (minValue == maxValue)
|
|
||||||
return minValue;
|
|
||||||
|
|
||||||
var bytes = new byte[1];
|
|
||||||
_rng.GetBytes(bytes);
|
|
||||||
|
|
||||||
return (byte)((bytes[0] % (maxValue - minValue)) + minValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override int Next(int minValue, int maxValue)
|
public override int Next(int minValue, int maxValue)
|
||||||
{
|
{
|
||||||
if (minValue > maxValue)
|
if (minValue > maxValue)
|
||||||
|
19
src/Nadeko.Common/ShmartBankAmount.cs
Normal file
19
src/Nadeko.Common/ShmartBankAmount.cs
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
namespace Nadeko.Common;
|
||||||
|
|
||||||
|
public readonly struct ShmartBankAmount
|
||||||
|
{
|
||||||
|
public long Amount { get; }
|
||||||
|
public ShmartBankAmount(long amount)
|
||||||
|
{
|
||||||
|
Amount = amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static implicit operator ShmartBankAmount(long num)
|
||||||
|
=> new(num);
|
||||||
|
|
||||||
|
public static implicit operator long(ShmartBankAmount num)
|
||||||
|
=> num.Amount;
|
||||||
|
|
||||||
|
public static implicit operator ShmartBankAmount(int num)
|
||||||
|
=> new(num);
|
||||||
|
}
|
@@ -3,12 +3,10 @@
|
|||||||
public readonly struct ShmartNumber : IEquatable<ShmartNumber>
|
public readonly struct ShmartNumber : IEquatable<ShmartNumber>
|
||||||
{
|
{
|
||||||
public long Value { get; }
|
public long Value { get; }
|
||||||
public string? Input { get; }
|
|
||||||
|
|
||||||
public ShmartNumber(long val, string? input = null)
|
public ShmartNumber(long val)
|
||||||
{
|
{
|
||||||
Value = val;
|
Value = val;
|
||||||
Input = input;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static implicit operator ShmartNumber(long num)
|
public static implicit operator ShmartNumber(long num)
|
||||||
|
@@ -12,7 +12,7 @@ public sealed class BetflipGame
|
|||||||
|
|
||||||
public BetflipResult Flip(byte guess, decimal amount)
|
public BetflipResult Flip(byte guess, decimal amount)
|
||||||
{
|
{
|
||||||
var side = _rng.Next(0, 2);
|
var side = (byte)_rng.Next(0, 2);
|
||||||
if (side == guess)
|
if (side == guess)
|
||||||
{
|
{
|
||||||
return new BetflipResult()
|
return new BetflipResult()
|
||||||
|
@@ -13,7 +13,7 @@ public sealed class BetrollGame
|
|||||||
|
|
||||||
public BetrollResult Roll(decimal amount = 0)
|
public BetrollResult Roll(decimal amount = 0)
|
||||||
{
|
{
|
||||||
var roll = _rng.Next(0, 101);
|
var roll = _rng.Next(1, 101);
|
||||||
|
|
||||||
for (var i = 0; i < _thresholdPairs.Length; i++)
|
for (var i = 0; i < _thresholdPairs.Length; i++)
|
||||||
{
|
{
|
||||||
|
@@ -8,9 +8,9 @@ public class SlotGame
|
|||||||
{
|
{
|
||||||
var rolls = new[]
|
var rolls = new[]
|
||||||
{
|
{
|
||||||
_rng.Next(0, 6),
|
(byte)_rng.Next(0, 6),
|
||||||
_rng.Next(0, 6),
|
(byte)_rng.Next(0, 6),
|
||||||
_rng.Next(0, 6)
|
(byte)_rng.Next(0, 6)
|
||||||
};
|
};
|
||||||
|
|
||||||
ref var a = ref rolls[0];
|
ref var a = ref rolls[0];
|
||||||
|
@@ -47,7 +47,7 @@ public static class MedusaExtensions
|
|||||||
=> ctx.Message.AddReactionAsync(new Emoji("🤔"));
|
=> ctx.Message.AddReactionAsync(new Emoji("🤔"));
|
||||||
|
|
||||||
public static Task<IUserMessage> ErrorLocalizedAsync(this AnyContext ctx, string key, params object[]? args)
|
public static Task<IUserMessage> ErrorLocalizedAsync(this AnyContext ctx, string key, params object[]? args)
|
||||||
=> ctx.SendErrorAsync(ctx.GetText(key));
|
=> ctx.SendErrorAsync(ctx.GetText(key, args));
|
||||||
|
|
||||||
public static Task<IUserMessage> PendingLocalizedAsync(this AnyContext ctx, string key, params object[]? args)
|
public static Task<IUserMessage> PendingLocalizedAsync(this AnyContext ctx, string key, params object[]? args)
|
||||||
=> ctx.SendPendingAsync(ctx.GetText(key, args));
|
=> ctx.SendPendingAsync(ctx.GetText(key, args));
|
||||||
@@ -56,11 +56,11 @@ public static class MedusaExtensions
|
|||||||
=> ctx.SendConfirmAsync(ctx.GetText(key, args));
|
=> ctx.SendConfirmAsync(ctx.GetText(key, args));
|
||||||
|
|
||||||
public static Task<IUserMessage> ReplyErrorLocalizedAsync(this AnyContext ctx, string key, params object[]? args)
|
public static Task<IUserMessage> ReplyErrorLocalizedAsync(this AnyContext ctx, string key, params object[]? args)
|
||||||
=> ctx.SendErrorAsync($"{Format.Bold(ctx.User.ToString())} {ctx.GetText(key)}");
|
=> ctx.SendErrorAsync($"{Format.Bold(ctx.User.ToString())} {ctx.GetText(key, args)}");
|
||||||
|
|
||||||
public static Task<IUserMessage> ReplyPendingLocalizedAsync(this AnyContext ctx, string key, params object[]? args)
|
public static Task<IUserMessage> ReplyPendingLocalizedAsync(this AnyContext ctx, string key, params object[]? args)
|
||||||
=> ctx.SendPendingAsync($"{Format.Bold(ctx.User.ToString())} {ctx.GetText(key)}");
|
=> ctx.SendPendingAsync($"{Format.Bold(ctx.User.ToString())} {ctx.GetText(key, args)}");
|
||||||
|
|
||||||
public static Task<IUserMessage> ReplyConfirmLocalizedAsync(this AnyContext ctx, string key, params object[]? args)
|
public static Task<IUserMessage> ReplyConfirmLocalizedAsync(this AnyContext ctx, string key, params object[]? args)
|
||||||
=> ctx.SendConfirmAsync($"{Format.Bold(ctx.User.ToString())} {ctx.GetText(key)}");
|
=> ctx.SendConfirmAsync($"{Format.Bold(ctx.User.ToString())} {ctx.GetText(key, args)}");
|
||||||
}
|
}
|
@@ -35,13 +35,13 @@ public sealed class Bot
|
|||||||
private readonly IBotCredsProvider _credsProvider;
|
private readonly IBotCredsProvider _credsProvider;
|
||||||
// private readonly InteractionService _interactionService;
|
// private readonly InteractionService _interactionService;
|
||||||
|
|
||||||
public Bot(int shardId, int? totalShards)
|
public Bot(int shardId, int? totalShards, string credPath = null)
|
||||||
{
|
{
|
||||||
if (shardId < 0)
|
if (shardId < 0)
|
||||||
throw new ArgumentOutOfRangeException(nameof(shardId));
|
throw new ArgumentOutOfRangeException(nameof(shardId));
|
||||||
|
|
||||||
ShardId = shardId;
|
ShardId = shardId;
|
||||||
_credsProvider = new BotCredsProvider(totalShards);
|
_credsProvider = new BotCredsProvider(totalShards, credPath);
|
||||||
_creds = _credsProvider.GetCreds();
|
_creds = _credsProvider.GetCreds();
|
||||||
|
|
||||||
_db = new(_credsProvider);
|
_db = new(_credsProvider);
|
||||||
|
@@ -0,0 +1,94 @@
|
|||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using NadekoBot.Db;
|
||||||
|
using NadekoBot.Modules.Gambling.Services;
|
||||||
|
using NCalc;
|
||||||
|
using OneOf;
|
||||||
|
|
||||||
|
namespace NadekoBot.Common.TypeReaders;
|
||||||
|
|
||||||
|
public class BaseShmartInputAmountReader
|
||||||
|
{
|
||||||
|
private static readonly Regex _percentRegex = new(@"^((?<num>100|\d{1,2})%)$", RegexOptions.Compiled);
|
||||||
|
protected readonly DbService _db;
|
||||||
|
protected readonly GamblingConfigService _gambling;
|
||||||
|
|
||||||
|
public BaseShmartInputAmountReader(DbService db, GamblingConfigService gambling)
|
||||||
|
{
|
||||||
|
_db = db;
|
||||||
|
_gambling = gambling;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask<OneOf<long, OneOf.Types.Error<string>>> ReadAsync(ICommandContext context, string input)
|
||||||
|
{
|
||||||
|
var i = input.Trim().ToUpperInvariant();
|
||||||
|
|
||||||
|
i = i.Replace("K", "000");
|
||||||
|
|
||||||
|
//can't add m because it will conflict with max atm
|
||||||
|
|
||||||
|
if (await TryHandlePercentage(context, i) is long num)
|
||||||
|
{
|
||||||
|
return num;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var expr = new Expression(i, EvaluateOptions.IgnoreCase);
|
||||||
|
expr.EvaluateParameter += (str, ev) => EvaluateParam(str, ev, context).GetAwaiter().GetResult();
|
||||||
|
return (long)decimal.Parse(expr.Evaluate().ToString()!);
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
return new OneOf.Types.Error<string>($"Invalid input: {input}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task EvaluateParam(string name, ParameterArgs args, ICommandContext ctx)
|
||||||
|
{
|
||||||
|
switch (name.ToUpperInvariant())
|
||||||
|
{
|
||||||
|
case "PI":
|
||||||
|
args.Result = Math.PI;
|
||||||
|
break;
|
||||||
|
case "E":
|
||||||
|
args.Result = Math.E;
|
||||||
|
break;
|
||||||
|
case "ALL":
|
||||||
|
case "ALLIN":
|
||||||
|
args.Result = await Cur(ctx);
|
||||||
|
break;
|
||||||
|
case "HALF":
|
||||||
|
args.Result = await Cur(ctx) / 2;
|
||||||
|
break;
|
||||||
|
case "MAX":
|
||||||
|
args.Result = await Max(ctx);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual async Task<long> Cur(ICommandContext ctx)
|
||||||
|
{
|
||||||
|
await using var uow = _db.GetDbContext();
|
||||||
|
return await uow.DiscordUser.GetUserCurrencyAsync(ctx.User.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual async Task<long> Max(ICommandContext ctx)
|
||||||
|
{
|
||||||
|
var settings = _gambling.Data;
|
||||||
|
var max = settings.MaxBet;
|
||||||
|
return max == 0 ? await Cur(ctx) : max;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<long?> TryHandlePercentage(ICommandContext ctx, string input)
|
||||||
|
{
|
||||||
|
var m = _percentRegex.Match(input);
|
||||||
|
|
||||||
|
if (m.Captures.Count == 0)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
if (!long.TryParse(m.Groups["num"].ToString(), out var percent))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return (long)(await Cur(ctx) * (percent / 100.0f));
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,32 @@
|
|||||||
|
#nullable disable
|
||||||
|
using NadekoBot.Modules.Gambling.Bank;
|
||||||
|
using NadekoBot.Modules.Gambling.Services;
|
||||||
|
|
||||||
|
namespace NadekoBot.Common.TypeReaders;
|
||||||
|
|
||||||
|
public sealed class ShmartBankAmountTypeReader : NadekoTypeReader<ShmartBankAmount>
|
||||||
|
{
|
||||||
|
private readonly IBankService _bank;
|
||||||
|
private readonly ShmartBankInputAmountReader _tr;
|
||||||
|
|
||||||
|
public ShmartBankAmountTypeReader(IBankService bank, DbService db, GamblingConfigService gambling)
|
||||||
|
{
|
||||||
|
_bank = bank;
|
||||||
|
_tr = new ShmartBankInputAmountReader(bank, db, gambling);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async ValueTask<TypeReaderResult<ShmartBankAmount>> ReadAsync(ICommandContext ctx, string input)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(input))
|
||||||
|
return TypeReaderResult.FromError<ShmartBankAmount>(CommandError.ParseFailed, "Input is empty.");
|
||||||
|
|
||||||
|
var result = await _tr.ReadAsync(ctx, input);
|
||||||
|
|
||||||
|
if (result.TryPickT0(out var val, out var err))
|
||||||
|
{
|
||||||
|
return TypeReaderResult.FromSuccess<ShmartBankAmount>(new(val));
|
||||||
|
}
|
||||||
|
|
||||||
|
return TypeReaderResult.FromError<ShmartBankAmount>(CommandError.Unsuccessful, err.Value);
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,21 @@
|
|||||||
|
using NadekoBot.Modules.Gambling.Bank;
|
||||||
|
using NadekoBot.Modules.Gambling.Services;
|
||||||
|
|
||||||
|
namespace NadekoBot.Common.TypeReaders;
|
||||||
|
|
||||||
|
public sealed class ShmartBankInputAmountReader : BaseShmartInputAmountReader
|
||||||
|
{
|
||||||
|
private readonly IBankService _bank;
|
||||||
|
|
||||||
|
public ShmartBankInputAmountReader(IBankService bank, DbService db, GamblingConfigService gambling)
|
||||||
|
: base(db, gambling)
|
||||||
|
{
|
||||||
|
_bank = bank;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override Task<long> Cur(ICommandContext ctx)
|
||||||
|
=> _bank.GetBalanceAsync(ctx.User.Id);
|
||||||
|
|
||||||
|
protected override Task<long> Max(ICommandContext ctx)
|
||||||
|
=> Cur(ctx);
|
||||||
|
}
|
@@ -0,0 +1,29 @@
|
|||||||
|
#nullable disable
|
||||||
|
using NadekoBot.Modules.Gambling.Services;
|
||||||
|
|
||||||
|
namespace NadekoBot.Common.TypeReaders;
|
||||||
|
|
||||||
|
public sealed class ShmartNumberTypeReader : NadekoTypeReader<ShmartNumber>
|
||||||
|
{
|
||||||
|
private readonly BaseShmartInputAmountReader _tr;
|
||||||
|
|
||||||
|
public ShmartNumberTypeReader(DbService db, GamblingConfigService gambling)
|
||||||
|
{
|
||||||
|
_tr = new BaseShmartInputAmountReader(db, gambling);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async ValueTask<TypeReaderResult<ShmartNumber>> ReadAsync(ICommandContext ctx, string input)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(input))
|
||||||
|
return TypeReaderResult.FromError<ShmartNumber>(CommandError.ParseFailed, "Input is empty.");
|
||||||
|
|
||||||
|
var result = await _tr.ReadAsync(ctx, input);
|
||||||
|
|
||||||
|
if (result.TryPickT0(out var val, out var err))
|
||||||
|
{
|
||||||
|
return TypeReaderResult.FromSuccess<ShmartNumber>(new(val));
|
||||||
|
}
|
||||||
|
|
||||||
|
return TypeReaderResult.FromError<ShmartNumber>(CommandError.Unsuccessful, err.Value);
|
||||||
|
}
|
||||||
|
}
|
@@ -1,100 +0,0 @@
|
|||||||
#nullable disable
|
|
||||||
using NadekoBot.Db;
|
|
||||||
using NadekoBot.Modules.Gambling.Services;
|
|
||||||
using NCalc;
|
|
||||||
using System.Text.RegularExpressions;
|
|
||||||
using Nadeko.Common;
|
|
||||||
|
|
||||||
namespace NadekoBot.Common.TypeReaders;
|
|
||||||
|
|
||||||
public sealed class ShmartNumberTypeReader : NadekoTypeReader<ShmartNumber>
|
|
||||||
{
|
|
||||||
private static readonly Regex _percentRegex = new(@"^((?<num>100|\d{1,2})%)$", RegexOptions.Compiled);
|
|
||||||
private readonly DbService _db;
|
|
||||||
private readonly GamblingConfigService _gambling;
|
|
||||||
|
|
||||||
public ShmartNumberTypeReader(DbService db, GamblingConfigService gambling)
|
|
||||||
{
|
|
||||||
_db = db;
|
|
||||||
_gambling = gambling;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override ValueTask<TypeReaderResult<ShmartNumber>> ReadAsync(ICommandContext context, string input)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(input))
|
|
||||||
return new(TypeReaderResult.FromError<ShmartNumber>(CommandError.ParseFailed, "Input is empty."));
|
|
||||||
|
|
||||||
var i = input.Trim().ToUpperInvariant();
|
|
||||||
|
|
||||||
i = i.Replace("K", "000");
|
|
||||||
|
|
||||||
//can't add m because it will conflict with max atm
|
|
||||||
|
|
||||||
if (TryHandlePercentage(context, i, out var num))
|
|
||||||
return new(TypeReaderResult.FromSuccess(new ShmartNumber(num, i)));
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var expr = new Expression(i, EvaluateOptions.IgnoreCase);
|
|
||||||
expr.EvaluateParameter += (str, ev) => EvaluateParam(str, ev, context);
|
|
||||||
var lon = (long)decimal.Parse(expr.Evaluate().ToString());
|
|
||||||
return new(TypeReaderResult.FromSuccess(new ShmartNumber(lon, input)));
|
|
||||||
}
|
|
||||||
catch (Exception)
|
|
||||||
{
|
|
||||||
return ValueTask.FromResult(
|
|
||||||
TypeReaderResult.FromError<ShmartNumber>(CommandError.ParseFailed, $"Invalid input: {input}"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void EvaluateParam(string name, ParameterArgs args, ICommandContext ctx)
|
|
||||||
{
|
|
||||||
switch (name.ToUpperInvariant())
|
|
||||||
{
|
|
||||||
case "PI":
|
|
||||||
args.Result = Math.PI;
|
|
||||||
break;
|
|
||||||
case "E":
|
|
||||||
args.Result = Math.E;
|
|
||||||
break;
|
|
||||||
case "ALL":
|
|
||||||
case "ALLIN":
|
|
||||||
args.Result = Cur(ctx);
|
|
||||||
break;
|
|
||||||
case "HALF":
|
|
||||||
args.Result = Cur(ctx) / 2;
|
|
||||||
break;
|
|
||||||
case "MAX":
|
|
||||||
args.Result = Max(ctx);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private long Cur(ICommandContext ctx)
|
|
||||||
{
|
|
||||||
using var uow = _db.GetDbContext();
|
|
||||||
return uow.DiscordUser.GetUserCurrency(ctx.User.Id);
|
|
||||||
}
|
|
||||||
|
|
||||||
private long Max(ICommandContext ctx)
|
|
||||||
{
|
|
||||||
var settings = _gambling.Data;
|
|
||||||
var max = settings.MaxBet;
|
|
||||||
return max == 0 ? Cur(ctx) : max;
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool TryHandlePercentage(ICommandContext ctx, string input, out long num)
|
|
||||||
{
|
|
||||||
num = 0;
|
|
||||||
var m = _percentRegex.Match(input);
|
|
||||||
if (m.Captures.Count != 0)
|
|
||||||
{
|
|
||||||
if (!long.TryParse(m.Groups["num"].ToString(), out var percent))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
num = (long)(Cur(ctx) * (percent / 100.0f));
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
@@ -108,8 +108,8 @@ public static class DiscordUserExtensions
|
|||||||
.Take(count)
|
.Take(count)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
public static long GetUserCurrency(this DbSet<DiscordUser> users, ulong userId)
|
public static async Task<long> GetUserCurrencyAsync(this DbSet<DiscordUser> users, ulong userId)
|
||||||
=> users.AsNoTracking().FirstOrDefault(x => x.UserId == userId)?.CurrencyAmount ?? 0;
|
=> (await users.FirstOrDefaultAsyncLinqToDB(x => x.UserId == userId))?.CurrencyAmount ?? 0;
|
||||||
|
|
||||||
public static void RemoveFromMany(this DbSet<DiscordUser> users, IEnumerable<ulong> ids)
|
public static void RemoveFromMany(this DbSet<DiscordUser> users, IEnumerable<ulong> ids)
|
||||||
{
|
{
|
||||||
|
@@ -20,6 +20,8 @@ public sealed class PostgreSqlContext : NadekoContext
|
|||||||
|
|
||||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
||||||
{
|
{
|
||||||
|
AppContext.SetSwitch("Npgsql.EnableLegacyTimestampBehavior", true);
|
||||||
|
|
||||||
base.OnConfiguring(optionsBuilder);
|
base.OnConfiguring(optionsBuilder);
|
||||||
optionsBuilder
|
optionsBuilder
|
||||||
.UseLowerCaseNamingConvention()
|
.UseLowerCaseNamingConvention()
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
namespace NadekoBot.Migrations;
|
namespace NadekoBot.Migrations;
|
||||||
|
|
||||||
@@ -6,11 +7,36 @@ public static class MigrationQueries
|
|||||||
{
|
{
|
||||||
public static void MigrateRero(MigrationBuilder migrationBuilder)
|
public static void MigrateRero(MigrationBuilder migrationBuilder)
|
||||||
{
|
{
|
||||||
migrationBuilder.Sql(
|
if (migrationBuilder.IsMySql())
|
||||||
@"insert or ignore into reactionroles(guildid, channelid, messageid, emote, roleid, 'group', levelreq, dateadded)
|
{
|
||||||
|
migrationBuilder.Sql(
|
||||||
|
@"INSERT IGNORE into reactionroles(guildid, channelid, messageid, emote, roleid, `group`, levelreq, dateadded)
|
||||||
select guildid, channelid, messageid, emotename, roleid, exclusive, 0, reactionrolemessage.dateadded
|
select guildid, channelid, messageid, emotename, roleid, exclusive, 0, reactionrolemessage.dateadded
|
||||||
from reactionrole
|
from reactionrole
|
||||||
left join reactionrolemessage on reactionrolemessage.id = reactionrole.reactionrolemessageid
|
left join reactionrolemessage on reactionrolemessage.id = reactionrole.reactionrolemessageid
|
||||||
left join guildconfigs on reactionrolemessage.guildconfigid = guildconfigs.id;");
|
left join guildconfigs on reactionrolemessage.guildconfigid = guildconfigs.id;");
|
||||||
|
}
|
||||||
|
else if (migrationBuilder.IsSqlite())
|
||||||
|
{
|
||||||
|
migrationBuilder.Sql(
|
||||||
|
@"insert or ignore into reactionroles(guildid, channelid, messageid, emote, roleid, 'group', levelreq, dateadded)
|
||||||
|
select guildid, channelid, messageid, emotename, roleid, exclusive, 0, reactionrolemessage.dateadded
|
||||||
|
from reactionrole
|
||||||
|
left join reactionrolemessage on reactionrolemessage.id = reactionrole.reactionrolemessageid
|
||||||
|
left join guildconfigs on reactionrolemessage.guildconfigid = guildconfigs.id;");
|
||||||
|
}
|
||||||
|
else if (migrationBuilder.IsNpgsql())
|
||||||
|
{
|
||||||
|
migrationBuilder.Sql(@"insert into reactionroles(guildid, channelid, messageid, emote, roleid, ""group"", levelreq, dateadded)
|
||||||
|
select guildid, channelid, messageid, emotename, roleid, exclusive::int, 0, reactionrolemessage.dateadded
|
||||||
|
from reactionrole
|
||||||
|
left join reactionrolemessage on reactionrolemessage.id = reactionrole.reactionrolemessageid
|
||||||
|
left join guildconfigs on reactionrolemessage.guildconfigid = guildconfigs.id
|
||||||
|
ON CONFLICT DO NOTHING;");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new NotSupportedException("This database provider doesn't have an implementation for MigrateRero");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -58,7 +58,7 @@ public sealed class ReactionRolesService : IReadyExecutor, INService, IReactionR
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async Task<(IGuildUser, IRole)> GetUserAndRoleAsync(
|
private async Task<(IGuildUser, IRole)> GetUserAndRoleAsync(
|
||||||
SocketReaction r,
|
ulong userId,
|
||||||
ReactionRoleV2 rero)
|
ReactionRoleV2 rero)
|
||||||
{
|
{
|
||||||
var guild = _client.GetGuild(rero.GuildId);
|
var guild = _client.GetGuild(rero.GuildId);
|
||||||
@@ -67,8 +67,8 @@ public sealed class ReactionRolesService : IReadyExecutor, INService, IReactionR
|
|||||||
if (role is null)
|
if (role is null)
|
||||||
return default;
|
return default;
|
||||||
|
|
||||||
var user = guild.GetUser(r.UserId) as IGuildUser
|
var user = guild.GetUser(userId) as IGuildUser
|
||||||
?? await _client.Rest.GetGuildUserAsync(guild.Id, r.UserId);
|
?? await _client.Rest.GetGuildUserAsync(guild.Id, userId);
|
||||||
|
|
||||||
if (user is null)
|
if (user is null)
|
||||||
return default;
|
return default;
|
||||||
@@ -77,20 +77,23 @@ public sealed class ReactionRolesService : IReadyExecutor, INService, IReactionR
|
|||||||
}
|
}
|
||||||
|
|
||||||
private Task ClientOnReactionRemoved(
|
private Task ClientOnReactionRemoved(
|
||||||
Cacheable<IUserMessage, ulong> msg,
|
Cacheable<IUserMessage, ulong> cmsg,
|
||||||
Cacheable<IMessageChannel, ulong> ch,
|
Cacheable<IMessageChannel, ulong> ch,
|
||||||
SocketReaction r)
|
SocketReaction r)
|
||||||
{
|
{
|
||||||
if (!_cache.TryGetValue(msg.Id, out var reros))
|
if (!_cache.TryGetValue(cmsg.Id, out var reros))
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
|
|
||||||
_ = Task.Run(async () =>
|
_ = Task.Run(async () =>
|
||||||
{
|
{
|
||||||
var rero = reros.FirstOrDefault(x => x.Emote == r.Emote.Name || x.Emote == r.Emote.ToString());
|
var emote = await GetFixedEmoteAsync(cmsg, r.Emote);
|
||||||
|
|
||||||
|
var rero = reros.FirstOrDefault(x => x.Emote == emote.Name
|
||||||
|
|| x.Emote == emote.ToString());
|
||||||
if (rero is null)
|
if (rero is null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var (user, role) = await GetUserAndRoleAsync(r, rero);
|
var (user, role) = await GetUserAndRoleAsync(r.UserId, rero);
|
||||||
|
|
||||||
if (user.IsBot)
|
if (user.IsBot)
|
||||||
return;
|
return;
|
||||||
@@ -112,6 +115,24 @@ public sealed class ReactionRolesService : IReadyExecutor, INService, IReactionR
|
|||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// had to add this because for some reason, reactionremoved event's reaction doesn't have IsAnimated set,
|
||||||
|
// causing the .ToString() to be wrong on animated custom emotes
|
||||||
|
private async Task<IEmote> GetFixedEmoteAsync(
|
||||||
|
Cacheable<IUserMessage, ulong> cmsg,
|
||||||
|
IEmote inputEmote)
|
||||||
|
{
|
||||||
|
// this should only run for emote
|
||||||
|
if (inputEmote is not Emote e)
|
||||||
|
return inputEmote;
|
||||||
|
|
||||||
|
// try to get the message and pull
|
||||||
|
var msg = await cmsg.GetOrDownloadAsync();
|
||||||
|
|
||||||
|
var emote = msg.Reactions.Keys.FirstOrDefault(x => e.Equals(x));
|
||||||
|
return emote ?? inputEmote;
|
||||||
|
}
|
||||||
|
|
||||||
private Task ClientOnReactionAdded(
|
private Task ClientOnReactionAdded(
|
||||||
Cacheable<IUserMessage, ulong> msg,
|
Cacheable<IUserMessage, ulong> msg,
|
||||||
Cacheable<IMessageChannel, ulong> ch,
|
Cacheable<IMessageChannel, ulong> ch,
|
||||||
@@ -126,7 +147,7 @@ public sealed class ReactionRolesService : IReadyExecutor, INService, IReactionR
|
|||||||
if (rero is null)
|
if (rero is null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var (user, role) = await GetUserAndRoleAsync(r, rero);
|
var (user, role) = await GetUserAndRoleAsync(r.UserId, rero);
|
||||||
|
|
||||||
if (user.IsBot)
|
if (user.IsBot)
|
||||||
return;
|
return;
|
||||||
|
@@ -27,7 +27,7 @@ public sealed class CheckForUpdatesService : INService, IReadyExecutor
|
|||||||
if (_client.ShardId != 0)
|
if (_client.ShardId != 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
using var timer = new PeriodicTimer(TimeSpan.FromSeconds(15));
|
using var timer = new PeriodicTimer(TimeSpan.FromHours(1));
|
||||||
while (await timer.WaitForNextTickAsync())
|
while (await timer.WaitForNextTickAsync())
|
||||||
{
|
{
|
||||||
var conf = _bcs.Data;
|
var conf = _bcs.Data;
|
||||||
|
@@ -1,5 +1,4 @@
|
|||||||
using Nadeko.Common;
|
using NadekoBot.Modules.Gambling.Bank;
|
||||||
using NadekoBot.Modules.Gambling.Bank;
|
|
||||||
using NadekoBot.Modules.Gambling.Common;
|
using NadekoBot.Modules.Gambling.Common;
|
||||||
using NadekoBot.Modules.Gambling.Services;
|
using NadekoBot.Modules.Gambling.Services;
|
||||||
|
|
||||||
@@ -35,7 +34,7 @@ public partial class Gambling
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Cmd]
|
[Cmd]
|
||||||
public async Task BankWithdraw(ShmartNumber amount)
|
public async Task BankWithdraw(ShmartBankAmount amount)
|
||||||
{
|
{
|
||||||
if (amount <= 0)
|
if (amount <= 0)
|
||||||
return;
|
return;
|
||||||
|
@@ -663,7 +663,6 @@ public partial class Gambling : GamblingModule<GamblingService>
|
|||||||
if (win > 0)
|
if (win > 0)
|
||||||
{
|
{
|
||||||
str = GetText(strs.br_win(N(win), result.Threshold + (result.Roll == 100 ? " 👑" : "")));
|
str = GetText(strs.br_win(N(win), result.Threshold + (result.Roll == 100 ? " 👑" : "")));
|
||||||
await _cs.AddAsync(ctx.User, win, new("betroll", "win"));
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -876,11 +875,12 @@ public partial class Gambling : GamblingModule<GamblingService>
|
|||||||
public enum GambleTestTarget
|
public enum GambleTestTarget
|
||||||
{
|
{
|
||||||
Slot,
|
Slot,
|
||||||
|
Betroll,
|
||||||
|
Betflip,
|
||||||
|
BetflipT,
|
||||||
BetDraw,
|
BetDraw,
|
||||||
BetDrawHL,
|
BetDrawHL,
|
||||||
BetDrawRB,
|
BetDrawRB,
|
||||||
Betflip,
|
|
||||||
BetflipT,
|
|
||||||
Lula,
|
Lula,
|
||||||
Rps,
|
Rps,
|
||||||
}
|
}
|
||||||
@@ -921,6 +921,7 @@ public partial class Gambling : GamblingModule<GamblingService>
|
|||||||
GambleTestTarget.BetflipT => (await _gs.BetFlipAsync(ctx.User.Id, 0, 1)).AsT0.Multiplier,
|
GambleTestTarget.BetflipT => (await _gs.BetFlipAsync(ctx.User.Id, 0, 1)).AsT0.Multiplier,
|
||||||
GambleTestTarget.Lula => (await _gs.LulaAsync(ctx.User.Id, 0)).AsT0.Multiplier,
|
GambleTestTarget.Lula => (await _gs.LulaAsync(ctx.User.Id, 0)).AsT0.Multiplier,
|
||||||
GambleTestTarget.Rps => (await _gs.RpsAsync(ctx.User.Id, 0, (byte)(i % 3))).AsT0.Multiplier,
|
GambleTestTarget.Rps => (await _gs.RpsAsync(ctx.User.Id, 0, (byte)(i % 3))).AsT0.Multiplier,
|
||||||
|
GambleTestTarget.Betroll => (await _gs.BetRollAsync(ctx.User.Id, 0)).AsT0.Multiplier,
|
||||||
_ => throw new ArgumentOutOfRangeException(nameof(target))
|
_ => throw new ArgumentOutOfRangeException(nameof(target))
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -142,7 +142,7 @@ public class GamblingService : INService, IReadyExecutor
|
|||||||
var onePercent = uow.DiscordUser.GetTopOnePercentCurrency(_client.CurrentUser.Id);
|
var onePercent = uow.DiscordUser.GetTopOnePercentCurrency(_client.CurrentUser.Id);
|
||||||
decimal planted = uow.PlantedCurrency.AsQueryable().Sum(x => x.Amount);
|
decimal planted = uow.PlantedCurrency.AsQueryable().Sum(x => x.Amount);
|
||||||
var waifus = uow.WaifuInfo.GetTotalValue();
|
var waifus = uow.WaifuInfo.GetTotalValue();
|
||||||
var bot = uow.DiscordUser.GetUserCurrency(_client.CurrentUser.Id);
|
var bot = await uow.DiscordUser.GetUserCurrencyAsync(_client.CurrentUser.Id);
|
||||||
decimal bank = await uow.GetTable<BankUser>()
|
decimal bank = await uow.GetTable<BankUser>()
|
||||||
.SumAsyncLinqToDB(x => x.Balance);
|
.SumAsyncLinqToDB(x => x.Balance);
|
||||||
|
|
||||||
|
@@ -235,7 +235,7 @@ public partial class Gambling
|
|||||||
HorizontalAlignment = HorizontalAlignment.Center,
|
HorizontalAlignment = HorizontalAlignment.Center,
|
||||||
VerticalAlignment = VerticalAlignment.Center,
|
VerticalAlignment = VerticalAlignment.Center,
|
||||||
WrappingLength = 140,
|
WrappingLength = 140,
|
||||||
Origin = new(227, 92)
|
Origin = new(298, 100)
|
||||||
},
|
},
|
||||||
((long)result.Won).ToString(),
|
((long)result.Won).ToString(),
|
||||||
fontColor));
|
fontColor));
|
||||||
@@ -247,7 +247,7 @@ public partial class Gambling
|
|||||||
HorizontalAlignment = HorizontalAlignment.Center,
|
HorizontalAlignment = HorizontalAlignment.Center,
|
||||||
VerticalAlignment = VerticalAlignment.Center,
|
VerticalAlignment = VerticalAlignment.Center,
|
||||||
WrappingLength = 135,
|
WrappingLength = 135,
|
||||||
Origin = new(129, 472)
|
Origin = new(196, 480)
|
||||||
},
|
},
|
||||||
amount.ToString(),
|
amount.ToString(),
|
||||||
fontColor));
|
fontColor));
|
||||||
@@ -256,8 +256,7 @@ public partial class Gambling
|
|||||||
{
|
{
|
||||||
HorizontalAlignment = HorizontalAlignment.Center,
|
HorizontalAlignment = HorizontalAlignment.Center,
|
||||||
VerticalAlignment = VerticalAlignment.Center,
|
VerticalAlignment = VerticalAlignment.Center,
|
||||||
WrappingLength = 135,
|
Origin = new(393, 480)
|
||||||
Origin = new(325, 472)
|
|
||||||
},
|
},
|
||||||
ownedAmount.ToString(),
|
ownedAmount.ToString(),
|
||||||
fontColor));
|
fontColor));
|
||||||
|
@@ -57,123 +57,133 @@ public sealed class TriviaGame
|
|||||||
// loop until game is stopped
|
// loop until game is stopped
|
||||||
// each iteration is one round
|
// each iteration is one round
|
||||||
var firstRun = true;
|
var firstRun = true;
|
||||||
while (!_isStopped)
|
try
|
||||||
{
|
{
|
||||||
if (errorCount >= 5)
|
while (!_isStopped)
|
||||||
{
|
{
|
||||||
Log.Warning("Trivia errored 5 times and will quit");
|
if (errorCount >= 5)
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// wait for 3 seconds before posting the next question
|
|
||||||
if (firstRun)
|
|
||||||
{
|
|
||||||
firstRun = false;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
await Task.Delay(3000);
|
|
||||||
}
|
|
||||||
|
|
||||||
var maybeQuestion = await _questionPool.GetQuestionAsync();
|
|
||||||
|
|
||||||
if(!(maybeQuestion is TriviaQuestion question))
|
|
||||||
{
|
|
||||||
// if question is null (ran out of question, or other bugg ) - stop
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
CurrentQuestion = question;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// clear out all of the past guesses
|
|
||||||
while (_inputs.Reader.TryRead(out _)) ;
|
|
||||||
|
|
||||||
await OnQuestion(this, question);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Log.Warning(ex, "Error executing OnQuestion: {Message}", ex.Message);
|
|
||||||
errorCount++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// just keep looping through user inputs until someone guesses the answer
|
|
||||||
// or the timer expires
|
|
||||||
var halfGuessTimerTask = TimeOutFactory();
|
|
||||||
var hintSent = false;
|
|
||||||
var guessed = false;
|
|
||||||
while (true)
|
|
||||||
{
|
|
||||||
var readTask = _inputs.Reader.ReadAsync().AsTask();
|
|
||||||
|
|
||||||
// wait for either someone to attempt to guess
|
|
||||||
// or for timeout
|
|
||||||
var task = await Task.WhenAny(readTask, halfGuessTimerTask);
|
|
||||||
|
|
||||||
// if the task which completed is the timeout task
|
|
||||||
if (task == halfGuessTimerTask)
|
|
||||||
{
|
{
|
||||||
// if hint is already sent, means time expired
|
Log.Warning("Trivia errored 5 times and will quit");
|
||||||
// break (end the round)
|
await OnEnded(this);
|
||||||
if (hintSent)
|
break;
|
||||||
break;
|
}
|
||||||
|
|
||||||
// else, means half time passed, send a hint
|
// wait for 3 seconds before posting the next question
|
||||||
hintSent = true;
|
if (firstRun)
|
||||||
// start a new countdown of the same length
|
{
|
||||||
halfGuessTimerTask = TimeOutFactory();
|
firstRun = false;
|
||||||
// send a hint out
|
}
|
||||||
await OnHint(this, question);
|
else
|
||||||
|
{
|
||||||
|
await Task.Delay(3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
var maybeQuestion = await _questionPool.GetQuestionAsync();
|
||||||
|
|
||||||
|
if (!(maybeQuestion is TriviaQuestion question))
|
||||||
|
{
|
||||||
|
// if question is null (ran out of question, or other bugg ) - stop
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
CurrentQuestion = question;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// clear out all of the past guesses
|
||||||
|
while (_inputs.Reader.TryRead(out _))
|
||||||
|
;
|
||||||
|
|
||||||
|
await OnQuestion(this, question);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Log.Warning(ex, "Error executing OnQuestion: {Message}", ex.Message);
|
||||||
|
errorCount++;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// otherwise, read task is successful, and we're gonna
|
|
||||||
// get the user input data
|
|
||||||
var (user, input) = await readTask;
|
|
||||||
|
|
||||||
// check the guess
|
// just keep looping through user inputs until someone guesses the answer
|
||||||
if (question.IsAnswerCorrect(input))
|
// or the timer expires
|
||||||
|
var halfGuessTimerTask = TimeOutFactory();
|
||||||
|
var hintSent = false;
|
||||||
|
var guessed = false;
|
||||||
|
while (true)
|
||||||
{
|
{
|
||||||
// add 1 point to the user
|
var readTask = _inputs.Reader.ReadAsync().AsTask();
|
||||||
var val = _users.AddOrUpdate(user.Id, 1, (_, points) => ++points);
|
|
||||||
guessed = true;
|
|
||||||
|
|
||||||
// reset inactivity counter
|
// wait for either someone to attempt to guess
|
||||||
inactivity = 0;
|
// or for timeout
|
||||||
|
var task = await Task.WhenAny(readTask, halfGuessTimerTask);
|
||||||
|
|
||||||
var isWin = false;
|
// if the task which completed is the timeout task
|
||||||
// if user won the game, tell the game to stop
|
if (task == halfGuessTimerTask)
|
||||||
if (val >= _opts.WinRequirement)
|
|
||||||
{
|
{
|
||||||
_isStopped = true;
|
// if hint is already sent, means time expired
|
||||||
isWin = true;
|
// break (end the round)
|
||||||
|
if (hintSent)
|
||||||
|
break;
|
||||||
|
|
||||||
|
// else, means half time passed, send a hint
|
||||||
|
hintSent = true;
|
||||||
|
// start a new countdown of the same length
|
||||||
|
halfGuessTimerTask = TimeOutFactory();
|
||||||
|
// send a hint out
|
||||||
|
await OnHint(this, question);
|
||||||
|
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// call onguess
|
// otherwise, read task is successful, and we're gonna
|
||||||
await OnGuess(this, user, question, isWin);
|
// get the user input data
|
||||||
break;
|
var (user, input) = await readTask;
|
||||||
|
|
||||||
|
// check the guess
|
||||||
|
if (question.IsAnswerCorrect(input))
|
||||||
|
{
|
||||||
|
// add 1 point to the user
|
||||||
|
var val = _users.AddOrUpdate(user.Id, 1, (_, points) => ++points);
|
||||||
|
guessed = true;
|
||||||
|
|
||||||
|
// reset inactivity counter
|
||||||
|
inactivity = 0;
|
||||||
|
|
||||||
|
var isWin = false;
|
||||||
|
// if user won the game, tell the game to stop
|
||||||
|
if (val >= _opts.WinRequirement)
|
||||||
|
{
|
||||||
|
_isStopped = true;
|
||||||
|
isWin = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// call onguess
|
||||||
|
await OnGuess(this, user, question, isWin);
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (!guessed)
|
if (!guessed)
|
||||||
{
|
|
||||||
await OnTimeout(this, question);
|
|
||||||
|
|
||||||
if (_opts.Timeout != 0 && ++inactivity >= _opts.Timeout)
|
|
||||||
{
|
{
|
||||||
Log.Information("Trivia game is stopping due to inactivity");
|
await OnTimeout(this, question);
|
||||||
break;
|
|
||||||
|
if (_opts.Timeout != 0 && ++inactivity >= _opts.Timeout)
|
||||||
|
{
|
||||||
|
Log.Information("Trivia game is stopping due to inactivity");
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
|
||||||
// make sure game is set as ended
|
}
|
||||||
_isStopped = true;
|
finally
|
||||||
|
{
|
||||||
await OnEnded(this);
|
// make sure game is set as ended
|
||||||
|
_isStopped = true;
|
||||||
|
_ = OnEnded(this);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public IReadOnlyList<(ulong User, int points)> GetLeaderboard()
|
public IReadOnlyList<(ulong User, int points)> GetLeaderboard()
|
||||||
|
@@ -274,7 +274,7 @@ public sealed class YtdlYoutubeResolver : IYoutubeResolver
|
|||||||
Log.Information("Resolving youtube song by search term: {YoutubeQuery}", query);
|
Log.Information("Resolving youtube song by search term: {YoutubeQuery}", query);
|
||||||
|
|
||||||
var cachedData = await _trackCacher.GetCachedDataByQueryAsync(query, MusicPlatform.Youtube);
|
var cachedData = await _trackCacher.GetCachedDataByQueryAsync(query, MusicPlatform.Youtube);
|
||||||
if (cachedData is null)
|
if (cachedData is null || string.IsNullOrWhiteSpace(cachedData.Title))
|
||||||
{
|
{
|
||||||
var stringData = await _ytdlSearchOperation.GetDataAsync(query);
|
var stringData = await _ytdlSearchOperation.GetDataAsync(query);
|
||||||
var trackData = ResolveYtdlData(stringData);
|
var trackData = ResolveYtdlData(stringData);
|
||||||
|
@@ -715,8 +715,8 @@ public sealed class PatronageService
|
|||||||
return new()
|
return new()
|
||||||
{
|
{
|
||||||
Name = key.PrettyName,
|
Name = key.PrettyName,
|
||||||
Quota = defaultValue,
|
Quota = 0,
|
||||||
IsPatronLimit = false,
|
IsPatronLimit = true,
|
||||||
};
|
};
|
||||||
|
|
||||||
return new()
|
return new()
|
||||||
|
@@ -334,14 +334,15 @@ public partial class Xp : NadekoModule<XpService>
|
|||||||
|
|
||||||
public enum XpShopInputType
|
public enum XpShopInputType
|
||||||
{
|
{
|
||||||
F = 0,
|
Backgrounds = 1,
|
||||||
Frs = 0,
|
|
||||||
Fs = 0,
|
|
||||||
Frames = 0,
|
|
||||||
B = 1,
|
B = 1,
|
||||||
Bg = 1,
|
Bg = 1,
|
||||||
Bgs = 1,
|
Bgs = 1,
|
||||||
Backgrounds = 1
|
Frames = 0,
|
||||||
|
F = 0,
|
||||||
|
Fr = 0,
|
||||||
|
Frs = 0,
|
||||||
|
Fs = 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
[Cmd]
|
[Cmd]
|
||||||
@@ -353,8 +354,11 @@ public partial class Xp : NadekoModule<XpService>
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await SendConfirmAsync(GetText(strs.available_commands), $@"`{prefix}xpshop bgs`
|
await SendConfirmAsync(GetText(strs.available_commands),
|
||||||
`{prefix}xpshop frames`");
|
$@"`{prefix}xpshop bgs`
|
||||||
|
`{prefix}xpshop frames`
|
||||||
|
|
||||||
|
*{GetText(strs.xpshop_website)}*");
|
||||||
}
|
}
|
||||||
|
|
||||||
[Cmd]
|
[Cmd]
|
||||||
@@ -394,13 +398,18 @@ public partial class Xp : NadekoModule<XpService>
|
|||||||
.WithOkColor()
|
.WithOkColor()
|
||||||
.WithTitle(item.Name)
|
.WithTitle(item.Name)
|
||||||
.AddField(GetText(strs.price), Gambling.Gambling.N(item.Price, culture), true)
|
.AddField(GetText(strs.price), Gambling.Gambling.N(item.Price, culture), true)
|
||||||
// .AddField(GetText(strs.buy), $"{prefix}xpbuy {key}", true)
|
.WithImageUrl(string.IsNullOrWhiteSpace(item.Preview)
|
||||||
.WithImageUrl(item.Url.ToString());
|
? item.Url
|
||||||
|
: item.Preview);
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(item.Desc))
|
if (!string.IsNullOrWhiteSpace(item.Desc))
|
||||||
eb.WithDescription(item.Desc);
|
eb.AddField(GetText(strs.desc), item.Desc);
|
||||||
|
|
||||||
var tier = _service.GetXpShopTierRequirement();
|
if (key == "default")
|
||||||
|
eb.WithDescription(GetText(strs.xpshop_website));
|
||||||
|
|
||||||
|
|
||||||
|
var tier = _service.GetXpShopTierRequirement(type);
|
||||||
if (tier != PatronTier.None)
|
if (tier != PatronTier.None)
|
||||||
{
|
{
|
||||||
eb.WithFooter(GetText(strs.xp_shop_buy_required_tier(tier.ToString())));
|
eb.WithFooter(GetText(strs.xp_shop_buy_required_tier(tier.ToString())));
|
||||||
@@ -455,6 +464,42 @@ public partial class Xp : NadekoModule<XpService>
|
|||||||
addPaginatedFooter: false);
|
addPaginatedFooter: false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Cmd]
|
||||||
|
public async Task XpShopBuy(XpShopInputType type, string key)
|
||||||
|
{
|
||||||
|
var result = await _service.BuyShopItemAsync(ctx.User.Id, (XpShopItemType)type, key);
|
||||||
|
|
||||||
|
if (result != BuyResult.Success)
|
||||||
|
{
|
||||||
|
var _ = result switch
|
||||||
|
{
|
||||||
|
BuyResult.InsufficientFunds => await ReplyErrorLocalizedAsync(strs.not_enough(_gss.Data.Currency.Sign)),
|
||||||
|
BuyResult.AlreadyOwned => await ReplyErrorLocalizedAsync(strs.xpshop_already_owned),
|
||||||
|
BuyResult.UnknownItem => await ReplyErrorLocalizedAsync(strs.xpshop_item_not_found),
|
||||||
|
BuyResult.InsufficientPatronTier => await ReplyErrorLocalizedAsync(strs.patron_insuff_tier),
|
||||||
|
_ => throw new ArgumentOutOfRangeException()
|
||||||
|
};
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await ReplyConfirmLocalizedAsync(strs.xpshop_buy_success(type.ToString().ToLowerInvariant(),
|
||||||
|
key.ToLowerInvariant()));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Cmd]
|
||||||
|
public async Task XpShopUse(XpShopInputType type, string key)
|
||||||
|
{
|
||||||
|
var result = await _service.UseShopItemAsync(ctx.User.Id, (XpShopItemType)type, key);
|
||||||
|
|
||||||
|
if (!result)
|
||||||
|
{
|
||||||
|
await ReplyConfirmLocalizedAsync(strs.xp_shop_item_cant_use);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await ctx.OkAsync();
|
||||||
|
}
|
||||||
|
|
||||||
private async Task OnShopUse(SocketMessageComponent smc, (string? key, XpShopItemType type)? maybeState)
|
private async Task OnShopUse(SocketMessageComponent smc, (string? key, XpShopItemType type)? maybeState)
|
||||||
{
|
{
|
||||||
if (maybeState is not { } state)
|
if (maybeState is not { } state)
|
||||||
|
@@ -10,7 +10,7 @@ namespace NadekoBot.Modules.Xp;
|
|||||||
public sealed partial class XpConfig : ICloneable<XpConfig>
|
public sealed partial class XpConfig : ICloneable<XpConfig>
|
||||||
{
|
{
|
||||||
[Comment(@"DO NOT CHANGE")]
|
[Comment(@"DO NOT CHANGE")]
|
||||||
public int Version { get; set; } = 3;
|
public int Version { get; set; } = 5;
|
||||||
|
|
||||||
[Comment(@"How much XP will the users receive per message")]
|
[Comment(@"How much XP will the users receive per message")]
|
||||||
public int XpPerMessage { get; set; } = 3;
|
public int XpPerMessage { get; set; } = 3;
|
||||||
@@ -37,9 +37,13 @@ True -> Users can access the xp shop using .xpshop command
|
|||||||
False -> Users can't access the xp shop")]
|
False -> Users can't access the xp shop")]
|
||||||
public bool IsEnabled { get; set; } = false;
|
public bool IsEnabled { get; set; } = false;
|
||||||
|
|
||||||
[Comment(@"Which patron tier do users need in order to use the .xpshop command
|
[Comment(@"Which patron tier do users need in order to use the .xpshop bgs command
|
||||||
Leave at 'None' if patron system is disabled or you don't want any restrictions")]
|
Leave at 'None' if patron system is disabled or you don't want any restrictions")]
|
||||||
public PatronTier TierRequirement { get; set; } = PatronTier.None;
|
public PatronTier BgsTierRequirement { get; set; } = PatronTier.None;
|
||||||
|
|
||||||
|
[Comment(@"Which patron tier do users need in order to use the .xpshop frames command
|
||||||
|
Leave at 'None' if patron system is disabled or you don't want any restrictions")]
|
||||||
|
public PatronTier FramesTierRequirement { get; set; } = PatronTier.None;
|
||||||
|
|
||||||
[Comment(@"Frames available for sale. Keys are unique IDs.
|
[Comment(@"Frames available for sale. Keys are unique IDs.
|
||||||
Do not change keys as they are not publicly visible. Only change properties (name, price, id)
|
Do not change keys as they are not publicly visible. Only change properties (name, price, id)
|
||||||
@@ -71,6 +75,9 @@ To remove an item from the shop, but keep previous purchases, set the price to -
|
|||||||
[Comment(@"Direct url to the .png image which will be applied to the user's XP card")]
|
[Comment(@"Direct url to the .png image which will be applied to the user's XP card")]
|
||||||
public string Url { get; set; }
|
public string Url { get; set; }
|
||||||
|
|
||||||
|
[Comment(@"Optional preview url which will show instead of the real URL in the shop ")]
|
||||||
|
public string Preview { get; set; }
|
||||||
|
|
||||||
[Comment(@"Optional description of the item")]
|
[Comment(@"Optional description of the item")]
|
||||||
public string Desc { get; set; }
|
public string Desc { get; set; }
|
||||||
}
|
}
|
||||||
|
@@ -52,11 +52,11 @@ public sealed class XpConfigService : ConfigServiceBase<XpConfig>
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.Version < 3)
|
if (data.Version < 5)
|
||||||
{
|
{
|
||||||
ModifyConfig(c =>
|
ModifyConfig(c =>
|
||||||
{
|
{
|
||||||
c.Version = 3;
|
c.Version = 5;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -593,12 +593,12 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand
|
|||||||
{
|
{
|
||||||
if (ShouldTrackVoiceChannel(channel))
|
if (ShouldTrackVoiceChannel(channel))
|
||||||
{
|
{
|
||||||
foreach (var user in channel.Users)
|
foreach (var user in channel.ConnectedUsers)
|
||||||
await ScanUserForVoiceXp(user, channel);
|
await ScanUserForVoiceXp(user, channel);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
foreach (var user in channel.Users)
|
foreach (var user in channel.ConnectedUsers)
|
||||||
await UserLeftVoiceChannel(user, channel);
|
await UserLeftVoiceChannel(user, channel);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -617,13 +617,13 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand
|
|||||||
}
|
}
|
||||||
|
|
||||||
private bool ShouldTrackVoiceChannel(SocketVoiceChannel channel)
|
private bool ShouldTrackVoiceChannel(SocketVoiceChannel channel)
|
||||||
=> channel.Users.Where(UserParticipatingInVoiceChannel).Take(2).Count() >= 2;
|
=> channel.ConnectedUsers.Where(UserParticipatingInVoiceChannel).Take(2).Count() >= 2;
|
||||||
|
|
||||||
private bool UserParticipatingInVoiceChannel(SocketGuildUser user)
|
private bool UserParticipatingInVoiceChannel(SocketGuildUser user)
|
||||||
=> !user.IsDeafened && !user.IsMuted && !user.IsSelfDeafened && !user.IsSelfMuted;
|
=> !user.IsDeafened && !user.IsMuted && !user.IsSelfDeafened && !user.IsSelfMuted;
|
||||||
|
|
||||||
private TypedKey<long> GetVoiceXpKey(ulong userId)
|
private TypedKey<long> GetVoiceXpKey(ulong userId)
|
||||||
=> new($"xp:vc_join:{userId}");
|
=> new($"xp:{_client.CurrentUser.Id}:vc_join:{userId}");
|
||||||
|
|
||||||
private async Task UserJoinedVoiceChannel(SocketGuildUser user)
|
private async Task UserJoinedVoiceChannel(SocketGuildUser user)
|
||||||
{
|
{
|
||||||
@@ -635,6 +635,18 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand
|
|||||||
overwrite: false);
|
overwrite: false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// private void UserJoinedVoiceChannel(SocketGuildUser user)
|
||||||
|
// {
|
||||||
|
// var key = $"{_creds.RedisKey()}_user_xp_vc_join_{user.Id}";
|
||||||
|
// var value = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
||||||
|
//
|
||||||
|
// _cache.Redis.GetDatabase()
|
||||||
|
// .StringSet(key,
|
||||||
|
// value,
|
||||||
|
// TimeSpan.FromMinutes(_xpConfig.Data.VoiceMaxMinutes),
|
||||||
|
// when: When.NotExists);
|
||||||
|
// }
|
||||||
|
|
||||||
private async Task UserLeftVoiceChannel(SocketGuildUser user, SocketVoiceChannel channel)
|
private async Task UserLeftVoiceChannel(SocketGuildUser user, SocketVoiceChannel channel)
|
||||||
{
|
{
|
||||||
var key = GetVoiceXpKey(user.Id);
|
var key = GetVoiceXpKey(user.Id);
|
||||||
@@ -654,6 +666,7 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand
|
|||||||
|
|
||||||
if (actualXp > 0)
|
if (actualXp > 0)
|
||||||
{
|
{
|
||||||
|
Log.Information("Adding {Amount} voice xp to {User}", actualXp, user.ToString());
|
||||||
await _xpGainQueue.Writer.WriteAsync(new()
|
await _xpGainQueue.Writer.WriteAsync(new()
|
||||||
{
|
{
|
||||||
Guild = channel.Guild,
|
Guild = channel.Guild,
|
||||||
@@ -664,6 +677,38 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* private void UserLeftVoiceChannel(SocketGuildUser user, SocketVoiceChannel channel)
|
||||||
|
{
|
||||||
|
var key = $"{_creds.RedisKey()}_user_xp_vc_join_{user.Id}";
|
||||||
|
var value = _cache.Redis.GetDatabase().StringGet(key);
|
||||||
|
_cache.Redis.GetDatabase().KeyDelete(key);
|
||||||
|
|
||||||
|
// Allow for if this function gets called multiple times when a user leaves a channel.
|
||||||
|
if (value.IsNull)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!value.TryParse(out long startUnixTime))
|
||||||
|
return;
|
||||||
|
|
||||||
|
var dateStart = DateTimeOffset.FromUnixTimeSeconds(startUnixTime);
|
||||||
|
var dateEnd = DateTimeOffset.UtcNow;
|
||||||
|
var minutes = (dateEnd - dateStart).TotalMinutes;
|
||||||
|
var xp = _xpConfig.Data.VoiceXpPerMinute * minutes;
|
||||||
|
var actualXp = (int)Math.Floor(xp);
|
||||||
|
|
||||||
|
if (actualXp > 0)
|
||||||
|
{
|
||||||
|
_addMessageXp.Enqueue(new()
|
||||||
|
{
|
||||||
|
Guild = channel.Guild,
|
||||||
|
User = user,
|
||||||
|
XpAmount = actualXp
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
private bool ShouldTrackXp(SocketGuildUser user, ulong channelId)
|
private bool ShouldTrackXp(SocketGuildUser user, ulong channelId)
|
||||||
{
|
{
|
||||||
if (_excludedChannels.TryGetValue(user.Guild.Id, out var chans) && chans.Contains(channelId))
|
if (_excludedChannels.TryGetValue(user.Guild.Id, out var chans) && chans.Contains(channelId))
|
||||||
@@ -767,8 +812,8 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand
|
|||||||
return Enumerable.Empty<ulong>();
|
return Enumerable.Empty<ulong>();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static TypedKey<bool> GetUserRewKey(ulong userId)
|
private TypedKey<bool> GetUserRewKey(ulong userId)
|
||||||
=> new($"xp:user_gain:{userId}");
|
=> new($"xp:{_client.CurrentUser.Id}:user_gain:{userId}");
|
||||||
|
|
||||||
private async Task<bool> SetUserRewardedAsync(ulong userId)
|
private async Task<bool> SetUserRewardedAsync(ulong userId)
|
||||||
=> await _c.AddAsync(GetUserRewKey(userId),
|
=> await _c.AddAsync(GetUserRewKey(userId),
|
||||||
@@ -887,6 +932,8 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand
|
|||||||
throw new ArgumentNullException(nameof(bgBytes));
|
throw new ArgumentNullException(nameof(bgBytes));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var outlinePen = new Pen(Color.Black, 1f);
|
||||||
|
|
||||||
using var img = Image.Load<Rgba32>(bgBytes, out var imageFormat);
|
using var img = Image.Load<Rgba32>(bgBytes, out var imageFormat);
|
||||||
if (template.User.Name.Show)
|
if (template.User.Name.Show)
|
||||||
{
|
{
|
||||||
@@ -909,7 +956,8 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand
|
|||||||
Origin = new(template.User.Name.Pos.X, template.User.Name.Pos.Y + 8)
|
Origin = new(template.User.Name.Pos.X, template.User.Name.Pos.Y + 8)
|
||||||
},
|
},
|
||||||
"@" + username,
|
"@" + username,
|
||||||
template.User.Name.Color);
|
Brushes.Solid(template.User.Name.Color),
|
||||||
|
outlinePen);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -929,7 +977,8 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand
|
|||||||
Origin = new(template.Club.Name.Pos.X + 50, template.Club.Name.Pos.Y - 8)
|
Origin = new(template.Club.Name.Pos.X + 50, template.Club.Name.Pos.Y - 8)
|
||||||
},
|
},
|
||||||
clubName,
|
clubName,
|
||||||
template.Club.Name.Color));
|
Brushes.Solid(template.Club.Name.Color),
|
||||||
|
outlinePen));
|
||||||
}
|
}
|
||||||
|
|
||||||
Font GetTruncatedFont(
|
Font GetTruncatedFont(
|
||||||
@@ -987,7 +1036,6 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
var pen = new Pen(Color.Black, 1.25f);
|
|
||||||
|
|
||||||
var global = stats.Global;
|
var global = stats.Global;
|
||||||
var guild = stats.Guild;
|
var guild = stats.Guild;
|
||||||
@@ -1012,7 +1060,7 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand
|
|||||||
},
|
},
|
||||||
$"{global.LevelXp}/{global.RequiredXp}",
|
$"{global.LevelXp}/{global.RequiredXp}",
|
||||||
Brushes.Solid(template.User.Xp.Global.Color),
|
Brushes.Solid(template.User.Xp.Global.Color),
|
||||||
pen));
|
outlinePen));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (template.User.Xp.Guild.Show)
|
if (template.User.Xp.Guild.Show)
|
||||||
@@ -1026,7 +1074,7 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand
|
|||||||
},
|
},
|
||||||
$"{guild.LevelXp}/{guild.RequiredXp}",
|
$"{guild.LevelXp}/{guild.RequiredXp}",
|
||||||
Brushes.Solid(template.User.Xp.Guild.Color),
|
Brushes.Solid(template.User.Xp.Guild.Color),
|
||||||
pen));
|
outlinePen));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (stats.FullGuildStats.AwardedXp != 0 && template.User.Xp.Awarded.Show)
|
if (stats.FullGuildStats.AwardedXp != 0 && template.User.Xp.Awarded.Show)
|
||||||
@@ -1038,10 +1086,11 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand
|
|||||||
img.Mutate(x => x.DrawText($"({sign}{stats.FullGuildStats.AwardedXp})",
|
img.Mutate(x => x.DrawText($"({sign}{stats.FullGuildStats.AwardedXp})",
|
||||||
_fonts.NotoSans.CreateFont(template.User.Xp.Awarded.FontSize, FontStyle.Bold),
|
_fonts.NotoSans.CreateFont(template.User.Xp.Awarded.FontSize, FontStyle.Bold),
|
||||||
Brushes.Solid(template.User.Xp.Awarded.Color),
|
Brushes.Solid(template.User.Xp.Awarded.Color),
|
||||||
pen,
|
outlinePen,
|
||||||
new(awX, awY)));
|
new(awX, awY)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var rankPen = new Pen(Color.White, 1);
|
||||||
//ranking
|
//ranking
|
||||||
if (template.User.GlobalRank.Show)
|
if (template.User.GlobalRank.Show)
|
||||||
{
|
{
|
||||||
@@ -1054,10 +1103,15 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand
|
|||||||
globalRankStr,
|
globalRankStr,
|
||||||
68);
|
68);
|
||||||
|
|
||||||
img.Mutate(x => x.DrawText(globalRankStr,
|
img.Mutate(x => x.DrawText(
|
||||||
globalRankFont,
|
new TextOptions(globalRankFont)
|
||||||
template.User.GlobalRank.Color,
|
{
|
||||||
new(template.User.GlobalRank.Pos.X, template.User.GlobalRank.Pos.Y)));
|
Origin = new(template.User.GlobalRank.Pos.X, template.User.GlobalRank.Pos.Y)
|
||||||
|
},
|
||||||
|
globalRankStr,
|
||||||
|
Brushes.Solid(template.User.GlobalRank.Color),
|
||||||
|
rankPen
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (template.User.GuildRank.Show)
|
if (template.User.GuildRank.Show)
|
||||||
@@ -1071,10 +1125,15 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand
|
|||||||
guildRankStr,
|
guildRankStr,
|
||||||
43);
|
43);
|
||||||
|
|
||||||
img.Mutate(x => x.DrawText(guildRankStr,
|
img.Mutate(x => x.DrawText(
|
||||||
guildRankFont,
|
new TextOptions(guildRankFont)
|
||||||
template.User.GuildRank.Color,
|
{
|
||||||
new(template.User.GuildRank.Pos.X, template.User.GuildRank.Pos.Y)));
|
Origin = new(template.User.GuildRank.Pos.X, template.User.GuildRank.Pos.Y)
|
||||||
|
},
|
||||||
|
guildRankStr,
|
||||||
|
Brushes.Solid(template.User.GuildRank.Color),
|
||||||
|
rankPen
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
//avatar
|
//avatar
|
||||||
@@ -1337,11 +1396,15 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand
|
|||||||
if (!conf.Shop.IsEnabled)
|
if (!conf.Shop.IsEnabled)
|
||||||
return BuyResult.UnknownItem;
|
return BuyResult.UnknownItem;
|
||||||
|
|
||||||
if (conf.Shop.TierRequirement != PatronTier.None)
|
var req = type == XpShopItemType.Background
|
||||||
|
? conf.Shop.BgsTierRequirement
|
||||||
|
: conf.Shop.FramesTierRequirement;
|
||||||
|
|
||||||
|
if (req != PatronTier.None && !_creds.IsOwner(userId))
|
||||||
{
|
{
|
||||||
var patron = await _ps.GetPatronAsync(userId);
|
var patron = await _ps.GetPatronAsync(userId);
|
||||||
|
|
||||||
if ((int)patron.Tier < (int)conf.Shop.TierRequirement)
|
if ((int)patron.Tier < (int)req)
|
||||||
return BuyResult.InsufficientPatronTier;
|
return BuyResult.InsufficientPatronTier;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1468,8 +1531,12 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public PatronTier GetXpShopTierRequirement()
|
public PatronTier GetXpShopTierRequirement(Xp.XpShopInputType type)
|
||||||
=> _xpConfig.Data.Shop.TierRequirement;
|
=> type switch
|
||||||
|
{
|
||||||
|
Xp.XpShopInputType.F => _xpConfig.Data.Shop.FramesTierRequirement,
|
||||||
|
_ => _xpConfig.Data.Shop.BgsTierRequirement,
|
||||||
|
};
|
||||||
|
|
||||||
public bool IsShopEnabled()
|
public bool IsShopEnabled()
|
||||||
=> _xpConfig.Data.Shop.IsEnabled;
|
=> _xpConfig.Data.Shop.IsEnabled;
|
||||||
|
@@ -27,4 +27,4 @@ if (args.Length > 0 && args[0] != "run")
|
|||||||
LogSetup.SetupLogger(shardId);
|
LogSetup.SetupLogger(shardId);
|
||||||
Log.Information("Pid: {ProcessId}", pid);
|
Log.Information("Pid: {ProcessId}", pid);
|
||||||
|
|
||||||
await new Bot(shardId, totalShards).RunAndBlockAsync();
|
await new Bot(shardId, totalShards, Environment.GetEnvironmentVariable("NadekoBot__creds")).RunAndBlockAsync();
|
@@ -18,11 +18,9 @@ public sealed class BotCredsProvider : IBotCredsProvider
|
|||||||
private const string CREDS_FILE_NAME = "creds.yml";
|
private const string CREDS_FILE_NAME = "creds.yml";
|
||||||
private const string CREDS_EXAMPLE_FILE_NAME = "creds_example.yml";
|
private const string CREDS_EXAMPLE_FILE_NAME = "creds_example.yml";
|
||||||
|
|
||||||
private string CredsPath
|
private string CredsPath { get; }
|
||||||
=> Path.Combine(Directory.GetCurrentDirectory(), CREDS_FILE_NAME);
|
|
||||||
|
|
||||||
private string CredsExamplePath
|
private string CredsExamplePath { get; }
|
||||||
=> Path.Combine(Directory.GetCurrentDirectory(), CREDS_EXAMPLE_FILE_NAME);
|
|
||||||
|
|
||||||
private readonly int? _totalShards;
|
private readonly int? _totalShards;
|
||||||
|
|
||||||
@@ -34,9 +32,21 @@ public sealed class BotCredsProvider : IBotCredsProvider
|
|||||||
private readonly object _reloadLock = new();
|
private readonly object _reloadLock = new();
|
||||||
private readonly IDisposable _changeToken;
|
private readonly IDisposable _changeToken;
|
||||||
|
|
||||||
public BotCredsProvider(int? totalShards = null)
|
public BotCredsProvider(int? totalShards = null, string credPath = null)
|
||||||
{
|
{
|
||||||
_totalShards = totalShards;
|
_totalShards = totalShards;
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(credPath))
|
||||||
|
{
|
||||||
|
CredsPath = credPath;
|
||||||
|
CredsExamplePath = Path.Combine(Path.GetDirectoryName(credPath), CREDS_EXAMPLE_FILE_NAME);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
CredsPath = Path.Combine(Directory.GetCurrentDirectory(), CREDS_FILE_NAME);
|
||||||
|
CredsExamplePath = Path.Combine(Directory.GetCurrentDirectory(), CREDS_EXAMPLE_FILE_NAME);
|
||||||
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (!File.Exists(CredsExamplePath))
|
if (!File.Exists(CredsExamplePath))
|
||||||
|
@@ -7,7 +7,7 @@ namespace NadekoBot.Services;
|
|||||||
|
|
||||||
public sealed class StatsService : IStatsService, IReadyExecutor, INService
|
public sealed class StatsService : IStatsService, IReadyExecutor, INService
|
||||||
{
|
{
|
||||||
public const string BOT_VERSION = "4.3.0";
|
public const string BOT_VERSION = "4.3.4";
|
||||||
|
|
||||||
public string Author
|
public string Author
|
||||||
=> "Kwoth#2452";
|
=> "Kwoth#2452";
|
||||||
|
@@ -1064,6 +1064,10 @@ xpadd:
|
|||||||
- xpadd
|
- xpadd
|
||||||
xpshop:
|
xpshop:
|
||||||
- xpshop
|
- xpshop
|
||||||
|
xpshopbuy:
|
||||||
|
- xpshopbuy
|
||||||
|
xpshopuse:
|
||||||
|
- xpshopuse
|
||||||
clubcreate:
|
clubcreate:
|
||||||
- clubcreate
|
- clubcreate
|
||||||
clubtransfer:
|
clubtransfer:
|
||||||
|
Binary file not shown.
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 13 KiB |
@@ -2128,6 +2128,16 @@ xpshop:
|
|||||||
- "bgs"
|
- "bgs"
|
||||||
- "frames"
|
- "frames"
|
||||||
- "bgs 3"
|
- "bgs 3"
|
||||||
|
xpshopbuy:
|
||||||
|
desc: "Buy an item from the xp shop by specifying the type and the key of the item."
|
||||||
|
args:
|
||||||
|
- "bg open_sea"
|
||||||
|
- "fr gold"
|
||||||
|
xpshopuse:
|
||||||
|
desc: "Use a previously purchased item from the xp shop by specifying the type and the key of the item."
|
||||||
|
args:
|
||||||
|
- "bg synth"
|
||||||
|
- "fr default"
|
||||||
bible:
|
bible:
|
||||||
desc: "Shows bible verse. You need to supply book name and chapter:verse"
|
desc: "Shows bible verse. You need to supply book name and chapter:verse"
|
||||||
args:
|
args:
|
||||||
|
@@ -1003,7 +1003,7 @@
|
|||||||
"feature_limit_reached_you": "You've reached the limit of {0} for the {1} feature. You may be able to increase this limit by upgrading your patron tier.",
|
"feature_limit_reached_you": "You've reached the limit of {0} for the {1} feature. You may be able to increase this limit by upgrading your patron tier.",
|
||||||
"feature_limit_reached_owner": "Server owner has reached the limit of {0} for the {1} feature. Server owner may be able to upgrade this limit by upgrading patron tier.",
|
"feature_limit_reached_owner": "Server owner has reached the limit of {0} for the {1} feature. Server owner may be able to upgrade this limit by upgrading patron tier.",
|
||||||
"feature_limit_reached_either": "The limit of {0} for the {1} feature has been reached. Either you or the server owner may able to upgrade this limit by upgrading the patron tier.",
|
"feature_limit_reached_either": "The limit of {0} for the {1} feature has been reached. Either you or the server owner may able to upgrade this limit by upgrading the patron tier.",
|
||||||
"xp_shop_buy_required_tier": "Buying items from the shop requires Patron Tier {0} or higher.",
|
"xp_shop_buy_required_tier": "Buying items from this shop requires Patron Tier {0} or higher.",
|
||||||
"available_commands": "Available Commands",
|
"available_commands": "Available Commands",
|
||||||
"tier": "Tier",
|
"tier": "Tier",
|
||||||
"pledge": "Pledge",
|
"pledge": "Pledge",
|
||||||
@@ -1017,5 +1017,10 @@
|
|||||||
"patron_not_enabled": "Patron system is disabled.",
|
"patron_not_enabled": "Patron system is disabled.",
|
||||||
"results_in": "{0} results in {1}s",
|
"results_in": "{0} results in {1}s",
|
||||||
"patron_msg_sent": "Done sending messages to patrons at and above tier {1}. {1} successfully sent and {2} failed.",
|
"patron_msg_sent": "Done sending messages to patrons at and above tier {1}. {1} successfully sent and {2} failed.",
|
||||||
"xpadd_users": "Added {0} server XP to {1} users."
|
"xpadd_users": "Added {0} server XP to {1} users.",
|
||||||
|
"xpshop_buy_success": "Successfully purchased `{0}/{1}`",
|
||||||
|
"patron_insuff_tier": "Your Patron Tier insufficient to perform this action.",
|
||||||
|
"xpshop_already_owned": "You already own this item.",
|
||||||
|
"xpshop_item_not_found": "An item with that key doesn't exist.",
|
||||||
|
"xpshop_website": "You can see the list of all Xp Shop items here: <https://xpshop.nadeko.bot>"
|
||||||
}
|
}
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
# DO NOT CHANGE
|
# DO NOT CHANGE
|
||||||
version: 3
|
version: 5
|
||||||
# How much XP will the users receive per message
|
# How much XP will the users receive per message
|
||||||
xpPerMessage: 3
|
xpPerMessage: 3
|
||||||
# How often can the users receive XP in minutes
|
# How often can the users receive XP in minutes
|
||||||
@@ -16,9 +16,12 @@ shop:
|
|||||||
# True -> Users can access the xp shop using .xpshop command
|
# True -> Users can access the xp shop using .xpshop command
|
||||||
# False -> Users can't access the xp shop
|
# False -> Users can't access the xp shop
|
||||||
isEnabled: false
|
isEnabled: false
|
||||||
# Which patron tier do users need in order to use the .xpshop command
|
# Which patron tier do users need in order to use the .xpshop bgs command
|
||||||
# Leave at 'None' if patron system is disabled or you don't want any restrictions
|
# Leave at 'None' if patron system is disabled or you don't want any restrictions
|
||||||
tierRequirement: None
|
bgsTierRequirement: None
|
||||||
|
# Which patron tier do users need in order to use the .xpshop frames command
|
||||||
|
# Leave at 'None' if patron system is disabled or you don't want any restrictions
|
||||||
|
framesTierRequirement: None
|
||||||
# Frames available for sale. Keys are unique IDs.
|
# Frames available for sale. Keys are unique IDs.
|
||||||
# Do not change keys as they are not publicly visible. Only change properties (name, price, id)
|
# Do not change keys as they are not publicly visible. Only change properties (name, price, id)
|
||||||
# Removing a key which previously existed means that all previous purchases will also be unusable.
|
# Removing a key which previously existed means that all previous purchases will also be unusable.
|
||||||
@@ -31,6 +34,8 @@ shop:
|
|||||||
price: 0
|
price: 0
|
||||||
# Direct url to the .png image which will be applied to the user's XP card
|
# Direct url to the .png image which will be applied to the user's XP card
|
||||||
url: ''
|
url: ''
|
||||||
|
# Optional preview url which will show instead of the real URL in the shop
|
||||||
|
preview:
|
||||||
# Optional description of the item
|
# Optional description of the item
|
||||||
desc:
|
desc:
|
||||||
# Backgrounds available for sale. Keys are unique IDs.
|
# Backgrounds available for sale. Keys are unique IDs.
|
||||||
@@ -45,5 +50,7 @@ shop:
|
|||||||
price: 0
|
price: 0
|
||||||
# Direct url to the .png image which will be applied to the user's XP card
|
# Direct url to the .png image which will be applied to the user's XP card
|
||||||
url: ''
|
url: ''
|
||||||
|
# Optional preview url which will show instead of the real URL in the shop
|
||||||
|
preview:
|
||||||
# Optional description of the item
|
# Optional description of the item
|
||||||
desc:
|
desc:
|
||||||
|
@@ -10,8 +10,8 @@
|
|||||||
"Show": true,
|
"Show": true,
|
||||||
"FontSize": 50,
|
"FontSize": 50,
|
||||||
"Pos": {
|
"Pos": {
|
||||||
"X": 130,
|
"X": 105,
|
||||||
"Y": 17
|
"Y": 25
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Icon": {
|
"Icon": {
|
||||||
@@ -65,7 +65,7 @@
|
|||||||
"Bar": {
|
"Bar": {
|
||||||
"Show": true,
|
"Show": true,
|
||||||
"Global": {
|
"Global": {
|
||||||
"Color": "00000066",
|
"Color": "00000095",
|
||||||
"PointA": {
|
"PointA": {
|
||||||
"X": 321,
|
"X": 321,
|
||||||
"Y": 104
|
"Y": 104
|
||||||
@@ -78,7 +78,7 @@
|
|||||||
"Direction": 3
|
"Direction": 3
|
||||||
},
|
},
|
||||||
"Guild": {
|
"Guild": {
|
||||||
"Color": "00000066",
|
"Color": "00000095",
|
||||||
"PointA": {
|
"PointA": {
|
||||||
"X": 282,
|
"X": 282,
|
||||||
"Y": 248
|
"Y": 248
|
||||||
@@ -114,7 +114,7 @@
|
|||||||
"Show": true,
|
"Show": true,
|
||||||
"FontSize": 25,
|
"FontSize": 25,
|
||||||
"Pos": {
|
"Pos": {
|
||||||
"X": 490,
|
"X": 450,
|
||||||
"Y": 345
|
"Y": 345
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -138,7 +138,7 @@
|
|||||||
"FontSize": 35,
|
"FontSize": 35,
|
||||||
"Pos": {
|
"Pos": {
|
||||||
"X": 650,
|
"X": 650,
|
||||||
"Y": 49
|
"Y": 55
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user