Holy Big Commit!

This commit is contained in:
2026-03-15 05:44:32 +00:00
parent ab9c7223ca
commit eb06fc8102
27 changed files with 672 additions and 65 deletions

View File

@@ -0,0 +1,33 @@
using SkyBot.Models;
namespace SkyBot.Commands
{
public static class CommandRegistry
{
public static readonly Dictionary<string, ICommand> Commands = new();
public static readonly Dictionary<string, List<ICommand>> Sections = new();
static CommandRegistry()
{
var allCommands = AppDomain.CurrentDomain.GetAssemblies()
.SelectMany(a => a.GetTypes())
.Where(t => typeof(ICommand).IsAssignableFrom(t) && !t.IsInterface && !t.IsAbstract)
.Select(t => (ICommand?)Activator.CreateInstance(t))
.Select(c => c!);
foreach (var cmd in allCommands)
{
Commands[cmd.Name.ToLower()] = cmd;
foreach (var alias in cmd.Aliases)
{
Commands[alias.ToLower()] = cmd;
}
Sections = Commands.Values
.Distinct()
.GroupBy(c => c.Section.ToLower())
.ToDictionary(g => g.Key, g => g.ToList());
}
}
}
}

View File

@@ -0,0 +1,29 @@
using System.Collections.Concurrent;
using SkyBot.Models;
using Valour.Sdk.Models;
namespace SkyBot.Commands
{
public class CommandTemplate : ICommand
{
public string Name => "template";
public string[] Aliases => [];
public string Description => "";
public string Section => "template";
public string Usage => "";
public async Task Execute(CommandContext ctx)
{
ConcurrentDictionary<long, Channel> channelCache = ctx.ChannelCache;
long channelId = ctx.ChannelId;
PlanetMember member = ctx.Member;
string message = $"";
if (channelCache.TryGetValue(channelId, out var channel))
{
await MessageHelper.ReplyAsync(ctx, channel, message);
}
}
}
}

View File

@@ -0,0 +1,32 @@
using System.Collections.Concurrent;
using SkyBot.Helpers;
using SkyBot.Models;
using Valour.Sdk.Client;
using Valour.Sdk.Models;
namespace SkyBot.Commands
{
public class Test : ICommand
{
public string Name => "test";
public string[] Aliases => [];
public string Description => "Just a test command";
public string Section => "Dev";
public string Usage => "test";
public async Task Execute(CommandContext ctx)
{
ConcurrentDictionary<long, Channel> channelCache = ctx.ChannelCache;
long channelId = ctx.ChannelId;
ValourClient client = ctx.Client;
PlanetMember member = ctx.Member;
Message message = ctx.Message;
Planet planet = ctx.Planet;
if (channelCache.TryGetValue(channelId, out var channel))
{
await MessageHelper.ReplyAsync(ctx, channel, "This is a test message");
}
}
}
}

View File

@@ -0,0 +1,42 @@
using System.Collections.Concurrent;
using System.Net.NetworkInformation;
using SkyBot.Helpers;
using SkyBot.Models;
using Valour.Sdk.Models;
namespace SkyBot.Commands
{
public class Echo : ICommand
{
public string Name => "echo";
public string[] Aliases => [];
public string Description => "Echos what you said through the bot.";
public string Section => "Fun";
public string Usage => "echo <message>";
public async Task Execute(CommandContext ctx)
{
ConcurrentDictionary<long, Channel> channelCache = ctx.ChannelCache;
long channelId = ctx.ChannelId;
PlanetMember member = ctx.Member;
String[] args = ctx.Args;
Message message = ctx.Message;
string reply = string.Join(" ", args);
if (channelCache.TryGetValue(channelId, out var channel))
{
if (string.IsNullOrWhiteSpace(reply)) await channel.SendMessageAsync($"{MentionHelper.Mention(member)} Enter a message to echo.");
reply = $"{member.Name} » {reply}";
if (reply.Length > 2048)
{
reply = reply.Substring(0, 2048);
}
await MessageHelper.ReplyAsync(ctx, channel, reply);
}
}
}
}

View File

@@ -1,30 +0,0 @@
using System.Collections.Concurrent;
using SkyBot.Helpers;
using Valour.Sdk.Models;
namespace SkyBot.Commands
{
public static class HelpCommand
{
public static async Task Execute(ConcurrentDictionary<long, Channel> channelCache, long channelId, String prefix, PlanetMember member)
{
string helpMessage = $@"**Skybot Commands**:
- `s/echo <text>` - Echos text into the chat
- `s/suggest` - Shares the suggestions link
- `s/source` - Sends link for the source code
- `s/joincode` - Sends a link to a github that you can use to make your bot join your planet.
- `s/joinsite` - Sends a link to a website that you can use to make yout bot join your planet.
- `s/api|swagger` - Sends a link to the Swagger API
- `s/cmds|help` - Shows this list
- `s/usercount` - Shows the user count of Valour
- `s/devcentral` - Sends the invite link to the Dev Central Planet
- `s/mc` - Sends Unofficial ValourSMP IPs
";
if (channelCache.TryGetValue(channelId, out var channel))
{
await channel.SendMessageAsync($"{MentionHelper.Mention(member)}\n{helpMessage}");
}
}
}
}

View File

@@ -0,0 +1,29 @@
using System.Collections.Concurrent;
using SkyBot.Models;
using Valour.Sdk.Models;
namespace SkyBot.Commands
{
public class Devcentral : ICommand
{
public string Name => "devcentral";
public string[] Aliases => ["dev"];
public string Description => "Sends an invite link to the Dev Central Planet.";
public string Section => "Info";
public string Usage => "devcentral|dev";
public async Task Execute(CommandContext ctx)
{
ConcurrentDictionary<long, Channel> channelCache = ctx.ChannelCache;
long channelId = ctx.ChannelId;
PlanetMember member = ctx.Member;
string message = $"you can join the Dev Central (ID: 42439954653511681) planet here: https://app.valour.gg/I/k2tz9c4i";
if (channelCache.TryGetValue(channelId, out var channel))
{
await MessageHelper.ReplyAsync(ctx, channel, message);
}
}
}
}

View File

@@ -0,0 +1,92 @@
using System.Collections.Concurrent;
using System.Text;
using SkyBot.Helpers;
using SkyBot.Models;
using Valour.Sdk.Models;
using Valour.Shared.Authorization;
namespace SkyBot.Commands
{
public class Help : ICommand
{
public string Name => "help";
public string[] Aliases => ["h"];
public string Description => "Shows all the commands and their descriptions.";
public string Section => "Info";
public string Usage => "help|h [section] [page]";
private const int PageSize = 5;
public async Task Execute(CommandContext ctx)
{
ConcurrentDictionary<long, Channel> channelCache = ctx.ChannelCache;
long channelId = ctx.ChannelId;
string[] args = ctx.Args;
PlanetMember member = ctx.Member;
bool isOwner = await PermissionHelper.IsOwner(member);
if (!channelCache.TryGetValue(channelId, out var channel)) return;
// Show all sections.
if (args.Length == 0)
{
var sb = new StringBuilder();
sb.AppendLine("**Available Categories**");
foreach (var section in CommandRegistry.Sections.Keys)
{
if (section == "template") continue;
if (section == "dev" && !isOwner) continue;
if (section == "mod" && !PermissionHelper.HasPermAsync(member, [PlanetPermissions.Kick, PlanetPermissions.Ban, PlanetPermissions.ManageRoles]).Result) continue;
sb.AppendLine($"- `{section.ToTitleCase()}` ({CommandRegistry.Sections[section].Count})");
}
sb.AppendLine($"\nUse `{Config.Prefix}help <category>` to see commands in a category.");
await MessageHelper.ReplyAsync(ctx, channel, sb.ToString());
return;
}
// section [page]
string sectionName = args[0].ToLower();
if (!CommandRegistry.Sections.TryGetValue(sectionName, out var commands))
{
await MessageHelper.ReplyAsync(ctx, channel, $"Unknown category `{sectionName}`.");
return;
}
if (sectionName == "dev" && !isOwner)
{
await MessageHelper.ReplyAsync(ctx, channel, $"Unknown category `{sectionName}`.");
return;
}
if (sectionName == "mod" && !PermissionHelper.HasPermAsync(member, [PlanetPermissions.Kick, PlanetPermissions.Ban, PlanetPermissions.ManageRoles]).Result)
{
await MessageHelper.ReplyAsync(ctx, channel, $"Unknown category `{sectionName}`.");
return;
}
int page = 1;
if (args.Length >= 2 && int.TryParse(args[1], out int parsedPage))
{
page = parsedPage;
}
int totalPages = (int)Math.Ceiling(commands.Count / (double)PageSize);
page = Math.Clamp(page, 1, totalPages);
var pageCommands = commands.Skip((page - 1) * PageSize).Take(PageSize);
var sb2 = new StringBuilder();
sb2.AppendLine($"**{sectionName.ToTitleCase()} commands** (Page {page}/{totalPages}):");
foreach (var cmd in pageCommands)
{
var name = cmd.Aliases.Length > 0
? $"{cmd.Name}|{string.Join("|", cmd.Aliases)}"
: cmd.Name;
sb2.AppendLine($"`{Config.Prefix}{name}` - {cmd.Description}");
}
sb2.AppendLine($"\nUse `{Config.Prefix}help {sectionName} <page>` to see more.");
await MessageHelper.ReplyAsync(ctx, channel, sb2.ToString());
}
}
}

View File

@@ -0,0 +1,29 @@
using System.Collections.Concurrent;
using SkyBot.Models;
using Valour.Sdk.Models;
namespace SkyBot.Commands
{
public class JoinSite : ICommand
{
public string Name => "joinsite";
public string[] Aliases => [];
public string Description => "Links to a site to help your bots join a planet.";
public string Section => "Info";
public string Usage => "joinsite";
public async Task Execute(CommandContext ctx)
{
ConcurrentDictionary<long, Channel> channelCache = ctx.ChannelCache;
long channelId = ctx.ChannelId;
PlanetMember member = ctx.Member;
string message = $"You can use this website to easily add your bot to a planet: https://skyjoshua.xyz/planetjoiner";
if (channelCache.TryGetValue(channelId, out var channel))
{
await MessageHelper.ReplyAsync(ctx, channel, message);
}
}
}
}

View File

@@ -0,0 +1,31 @@
using System.Collections.Concurrent;
using SkyBot.Models;
using Valour.Sdk.Models;
namespace SkyBot.Commands
{
public class Minecraft : ICommand
{
public string Name => "minecraft";
public string[] Aliases => ["mc"];
public string Description => "Sends the Unofficial ValourSMP IPs";
public string Section => "Info";
public string Usage => "minecraft|mc";
public async Task Execute(CommandContext ctx)
{
ConcurrentDictionary<long, Channel> channelCache = ctx.ChannelCache;
long channelId = ctx.ChannelId;
PlanetMember member = ctx.Member;
string message = @$"you can join the Unofficial ValourSMP Minecraft Server by using this ip:
Java: `valour.sxsc.xyz`, Bedrock: `valourbr.sxsc.xyz` Both with the default ports.
Cool features can be found here: https://sxsc.xyz/servers/valour/";
if (channelCache.TryGetValue(channelId, out var channel))
{
await MessageHelper.ReplyAsync(ctx, channel, message);
}
}
}
}

View File

@@ -0,0 +1,29 @@
using System.Collections.Concurrent;
using SkyBot.Models;
using Valour.Sdk.Models;
namespace SkyBot.Commands
{
public class Source : ICommand
{
public string Name => "source";
public string[] Aliases => ["src"];
public string Description => "Shows the source code for this bot.";
public string Section => "Info";
public string Usage => "source";
public async Task Execute(CommandContext ctx)
{
ConcurrentDictionary<long, Channel> channelCache = ctx.ChannelCache;
long channelId = ctx.ChannelId;
PlanetMember member = ctx.Member;
string message = $"You can find my source code here: {Config.SourceLink}";
if (channelCache.TryGetValue(channelId, out var channel))
{
await MessageHelper.ReplyAsync(ctx, channel, message);
}
}
}
}

View File

@@ -0,0 +1,29 @@
using System.Collections.Concurrent;
using SkyBot.Models;
using Valour.Sdk.Models;
namespace SkyBot.Commands
{
public class Suggest : ICommand
{
public string Name => "suggest";
public string[] Aliases => [];
public string Description => "Shows the source code for this bot.";
public string Section => "Info";
public string Usage => "source";
public async Task Execute(CommandContext ctx)
{
ConcurrentDictionary<long, Channel> channelCache = ctx.ChannelCache;
long channelId = ctx.ChannelId;
PlanetMember member = ctx.Member;
string message = $"You can suggest a command to be added here: https://docs.google.com/spreadsheets/d/1CzcpLAuMiPL_RODrZ5x25cPj8yE-rR3mEnqrd_2Fbmk";
if (channelCache.TryGetValue(channelId, out var channel))
{
await MessageHelper.ReplyAsync(ctx, channel, message);
}
}
}
}

View File

@@ -0,0 +1,29 @@
using System.Collections.Concurrent;
using SkyBot.Models;
using Valour.Sdk.Models;
namespace SkyBot.Commands
{
public class SwaggerAPI : ICommand
{
public string Name => "swagger";
public string[] Aliases => ["api"];
public string Description => "Sends a link to the Valour.gg Swagger API.";
public string Section => "Info";
public string Usage => "swagger|api";
public async Task Execute(CommandContext ctx)
{
ConcurrentDictionary<long, Channel> channelCache = ctx.ChannelCache;
long channelId = ctx.ChannelId;
PlanetMember member = ctx.Member;
string message = $"Here is a link to the Swagger API: https://api.valour.gg/swagger";
if (channelCache.TryGetValue(channelId, out var channel))
{
await MessageHelper.ReplyAsync(ctx, channel, message);
}
}
}
}

View File

@@ -0,0 +1,31 @@
using System.Collections.Concurrent;
using SkyBot.Helpers;
using SkyBot.Models;
using Valour.Sdk.Models;
namespace SkyBot.Commands
{
public class UserCount : ICommand
{
public string Name => "usercount";
public string[] Aliases => ["users"];
public string Description => "Shows the user count of Valour.";
public string Section => "Info";
public string Usage => "usercount|users";
public async Task Execute(CommandContext ctx)
{
ConcurrentDictionary<long, Channel> channelCache = ctx.ChannelCache;
long channelId = ctx.ChannelId;
PlanetMember member = ctx.Member;
string message = @$"Current Valour user count is: {ValourUsercountHelper.ValourUsercount:N0}
You can see a graph of the user count here: /meow";
if (channelCache.TryGetValue(channelId, out var channel))
{
await MessageHelper.ReplyAsync(ctx, channel, message);
}
}
}
}

View File

@@ -0,0 +1,30 @@
using System.Collections.Concurrent;
using SkyBot.Models;
using Valour.Sdk.Models;
namespace SkyBot.Commands
{
public class Version : ICommand
{
public string Name => "version";
public string[] Aliases => [];
public string Description => "Shows the current version of the Bot and Valour.";
public string Section => "Info";
public string Usage => "";
public async Task Execute(CommandContext ctx)
{
ConcurrentDictionary<long, Channel> channelCache = ctx.ChannelCache;
long channelId = ctx.ChannelId;
PlanetMember member = ctx.Member;
string message = @$"Bot Version: {typeof(Version).Assembly.GetName().Version}
Valour Version: {typeof(Channel).Assembly.GetName().Version}";
if (channelCache.TryGetValue(channelId, out var channel))
{
await MessageHelper.ReplyAsync(ctx, channel, message);
}
}
}
}

View File

@@ -0,0 +1,36 @@
using System.Collections.Concurrent;
using SkyBot.Helpers;
using SkyBot.Models;
using Valour.Sdk.Models;
using Valour.Shared.Authorization;
namespace SkyBot.Commands
{
public class Ban : ICommand
{
public string Name => "ban";
public string[] Aliases => [];
public string Description => "Bans a user from the planet.";
public string Section => "mod";
public string Usage => "ban <user> [reason]";
public async Task Execute(CommandContext ctx)
{
ConcurrentDictionary<long, Channel> channelCache = ctx.ChannelCache;
long channelId = ctx.ChannelId;
PlanetMember member = ctx.Member;
if (channelCache.TryGetValue(channelId, out var channel))
{
if (!PermissionHelper.HasPermAsync(member, [PlanetPermissions.Ban]).Result)
{
await MessageHelper.ReplyAsync(ctx, channel, $"You don't have permission to use this command.");
return;
}
string message = $"Work in progress...";
await MessageHelper.ReplyAsync(ctx, channel, message);
}
}
}
}

View File

@@ -0,0 +1,37 @@
using System.Collections.Concurrent;
using SkyBot.Helpers;
using SkyBot.Models;
using Valour.Sdk.Models;
using Valour.Shared.Authorization;
namespace SkyBot.Commands
{
public class Kick : ICommand
{
public string Name => "kick";
public string[] Aliases => [];
public string Description => "Kicks a user from the planet.";
public string Section => "mod";
public string Usage => "kick <user> [reason]";
public async Task Execute(CommandContext ctx)
{
ConcurrentDictionary<long, Channel> channelCache = ctx.ChannelCache;
long channelId = ctx.ChannelId;
PlanetMember member = ctx.Member;
if (channelCache.TryGetValue(channelId, out var channel))
{
if (!PermissionHelper.HasPermAsync(member, [PlanetPermissions.Kick]).Result)
{
await MessageHelper.ReplyAsync(ctx, channel, $"You don't have permission to use this command.");
return;
}
string message = $"Work in progress...";
await MessageHelper.ReplyAsync(ctx, channel, message);
}
}
}
}

View File

@@ -1,9 +1,10 @@
namespace Skybot
namespace SkyBot
{
public static class Config {
public static readonly long OwnerId = 15652354820931584;
public static readonly string Prefix = "sd/";
public static readonly string SourceLink = "https://github.com/SkyJoshua/SkyBot";
}
}

View File

@@ -0,0 +1,24 @@
using SkyBot.Models;
using Valour.Sdk.Models;
public static class MessageHelper
{
public static async Task ReplyAsync(CommandContext ctx, Channel channel, string content)
{
long? replyToId = ctx.Message.ReplyToId.HasValue ? ctx.Message.ReplyToId : ctx.Message.Id;
var msg = new Message(ctx.Client)
{
Content = content,
ChannelId = channel.Id,
PlanetId = ctx.Planet.Id,
AuthorUserId = ctx.Client.Me.Id,
AuthorMemberId = channel.Planet?.MyMember.Id,
ReplyToId = replyToId,
Fingerprint = Guid.NewGuid().ToString()
};
await ctx.Client.MessageService.SendMessage(msg);
}
public static string ToTitleCase(this string str) => System.Globalization.CultureInfo.CurrentCulture.TextInfo.ToTitleCase(str);
}

View File

@@ -15,5 +15,13 @@ namespace SkyBot.Helpers
? permissions.All(permission => member.HasPermission(permission))
: permissions.Any(permission => member.HasPermission(permission));
}
public static async Task<bool> IsOwner(PlanetMember member)
{
if (member == null) return false;
if (member.UserId == Config.OwnerId) return true;
return false;
}
}
}

View File

@@ -0,0 +1,32 @@
using System.Text.Json;
namespace SkyBot.Helpers
{
public static class ValourUsercountHelper {
private static readonly HttpClient _http = new HttpClient();
private static long _valourUsercount;
public static long ValourUsercount => _valourUsercount;
public static async Task UpdateUsercount()
{
try
{
var response = await _http.GetStringAsync("https://api.valour.gg/api/users/count");
_valourUsercount = JsonSerializer.Deserialize<long>(response);
Console.WriteLine($"Valour user count updated: {_valourUsercount}");
} catch (Exception ex)
{
Console.WriteLine($"Failed to update valour user count: {ex.Message}");
}
}
public static void StartUpdater()
{
var timer = new System.Timers.Timer(300_000);
timer.Elapsed += async (_, _) => await UpdateUsercount();
timer.AutoReset = true;
timer.Start();
}
}
}

View File

@@ -0,0 +1,18 @@
using System.Collections.Concurrent;
using Valour.Sdk.Client;
using Valour.Sdk.Models;
namespace SkyBot.Models
{
public class CommandContext
{
public required ValourClient Client{ get; set; }
public required ConcurrentDictionary<long, Channel> ChannelCache { get; set; }
public required PlanetMember Member { get; set; }
public required Message Message { get; set; }
public required Planet Planet { get; set; }
public required long ChannelId { get; set; }
public required string[] Args { get; set; }
}
}

14
SkyBot/Models/ICommand.cs Normal file
View File

@@ -0,0 +1,14 @@
using System.Collections.Concurrent;
namespace SkyBot.Models
{
public interface ICommand
{
string Name { get; }
string[] Aliases { get; }
string Description { get; }
string Section { get; }
string Usage { get; }
Task Execute(CommandContext ctx);
}
}

View File

@@ -1,5 +1,6 @@
using System.Collections.Concurrent;
using DotNetEnv;
using SkyBot.Helpers;
using Valour.Sdk.Client;
using Valour.Sdk.Models;
@@ -21,6 +22,9 @@ namespace SkyBot.Services
if (!loginResult.Success) {Console.WriteLine($"Login Failed: {loginResult.Message}"); return;}
Console.WriteLine($"Logged in as {client.Me.Name} (ID: {client.Me.Id})");
await ValourUsercountHelper.UpdateUsercount();
ValourUsercountHelper.StartUpdater();
await PlanetService.InitializePlanetsAsync(client, channelCache, initalizedPlanets);
client.PlanetService.JoinedPlanetsUpdated += async () =>
{

View File

@@ -1,4 +1,5 @@
using System.Collections.Concurrent;
using System.Security.Cryptography.X509Certificates;
using Valour.Sdk.Models;
using Valour.Shared.Models;
@@ -6,32 +7,30 @@ namespace SkyBot.Services
{
public static class ChannelService
{
private static readonly SemaphoreSlim _channelSemaphore = new SemaphoreSlim(3, 3);
public static async Task InitializeChannelsAsync(
ConcurrentDictionary<long, Channel> channelCache,
Planet planet)
{
var tasks = planet.Channels.Select(async channel =>
foreach (var channel in planet.Channels)
{
channelCache[channel.Id] = channel;
if (channel.ChannelType == ChannelTypeEnum.PlanetChat)
{
await _channelSemaphore.WaitAsync();
}
_ = Task.Run(async () =>
{
foreach (var channel in planet.Channels.Where(c => c.ChannelType == ChannelTypeEnum.PlanetChat)){
try
{
await channel.OpenWithResult("SkyBot");
Console.WriteLine($"Realtime opened for: {planet.Name} (ID: {planet.Id}) -> {channel.Name} (ID: {channel.Id})");
await Task.Delay(250);
}
finally
} catch (Exception ex)
{
_channelSemaphore.Release();
Console.WriteLine($"Error opening realtime for {channel.Id}: {ex.Message}");
}
}
});
await Task.WhenAll(tasks);
Console.WriteLine($"All channels opened for {planet.Name}.");
});
}
}
}

View File

@@ -1,7 +1,7 @@
using System.Collections.Concurrent;
using Skybot;
using SkyBot.Commands;
using SkyBot.Helpers;
using SkyBot.Models;
using Valour.Sdk.Client;
using Valour.Sdk.Models;
@@ -15,36 +15,38 @@ namespace SkyBot.Services.Messages
Message message
)
{
string prefix = Config.Prefix;
if (message.AuthorUserId == client.Me.Id) return;
string prefix = Config.Prefix;
string content = message.Content ?? "";
if (string.IsNullOrWhiteSpace(content)) return;
if (!content.StartsWith(prefix)) return;
if (!content.ToLower().StartsWith(prefix)) return;
long channelId = message.ChannelId;
PlanetMember member = await message.FetchAuthorMemberAsync();
var parts = content.Substring(prefix.Length).Split(' ', StringSplitOptions.RemoveEmptyEntries);
if (parts.Length == 0) return;
string command = parts[0].ToLower();
string[] args = parts[1..];
switch (command)
if (CommandRegistry.Commands.TryGetValue(command, out var handler))
{
case "help":
await HelpCommand.Execute(channelCache, channelId, prefix, member);
break;
default:
if (channelCache.TryGetValue(channelId, out var channel))
{
await channel.SendMessageAsync($"{MentionHelper.Mention(member)} Unknown command.");
}
break;
await handler.Execute(new CommandContext
{
ChannelCache = channelCache,
ChannelId = channelId,
Member = member,
Planet = message.Planet,
Args = args,
Message = message,
Client = client
});
} else
{
if (channelCache.TryGetValue(channelId, out var channel))
{
await channel.SendMessageAsync($"{MentionHelper.Mention(member)} Unknown command.");
}
}
}
}

View File

@@ -35,10 +35,6 @@ namespace SkyBot
Console.WriteLine("Ready and listening...");
await Task.Delay(Timeout.Infinite);
} catch (InvalidOperationException ex) when (ex.Message.Contains("concurrent update"))
{
Console.WriteLine("Concurrent update detected, restarting...");
await Task.Delay(1000);
} catch (Exception ex)
{
Console.WriteLine($"Fatal error: {ex.Message}");

View File

@@ -5,6 +5,7 @@
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<Version>0.2.0.0</Version>
</PropertyGroup>
<ItemGroup>