Chatbot async improvements (#446)

* Add better chatbot handling by making them asynchronous

Removes the need for having RunInTimespan as await Task.Delay now can handle this

* Remove now unused classes

* Handle commando's commands with ValueTask

* Set values as not nullable, client sends all of these
This commit is contained in:
Jesse
2025-07-04 19:04:37 +02:00
committed by GitHub
parent 762d42c009
commit b6692fead4
14 changed files with 90 additions and 128 deletions
@@ -199,15 +199,13 @@ public class DialogueCallbacks(
/// Handle client/mail/msg/send
/// </summary>
/// <returns></returns>
public virtual ValueTask<string> SendMessage(
public virtual async ValueTask<string> SendMessage(
string url,
SendMessageRequest request,
string sessionID
)
{
return new ValueTask<string>(
_httpResponseUtil.GetBody(_dialogueController.SendMessage(sessionID, request))
);
return _httpResponseUtil.GetBody(await _dialogueController.SendMessage(sessionID, request));
}
/// <summary>
@@ -538,15 +538,22 @@ public class DialogueController(
/// <param name="sessionId">Session/Player id</param>
/// <param name="request"></param>
/// <returns></returns>
public virtual string SendMessage(string sessionId, SendMessageRequest request)
public virtual async ValueTask<string> SendMessage(string sessionId, SendMessageRequest request)
{
_mailSendService.SendPlayerMessageToNpc(sessionId, request.DialogId!, request.Text!);
return (
_dialogueChatBots
.FirstOrDefault(cb => cb.GetChatBot().Id == request.DialogId)
?.HandleMessage(sessionId, request) ?? request.DialogId
) ?? string.Empty;
var chatBot = _dialogueChatBots.FirstOrDefault(cb =>
cb.GetChatBot().Id == request.DialogId
);
if (chatBot is not null)
{
return await chatBot.HandleMessage(sessionId, request);
}
else
{
return string.Empty;
}
}
/// <summary>
@@ -3,7 +3,6 @@ using SPTarkov.Server.Core.Models.Eft.Dialog;
using SPTarkov.Server.Core.Models.Eft.Profile;
using SPTarkov.Server.Core.Models.Utils;
using SPTarkov.Server.Core.Services;
using SPTarkov.Server.Core.Utils.Callbacks;
namespace SPTarkov.Server.Core.Helpers.Dialogue;
@@ -20,7 +19,7 @@ public abstract class AbstractDialogChatBot(
public abstract UserDialogInfo GetChatBot();
public string? HandleMessage(string sessionId, SendMessageRequest request)
public async ValueTask<string> HandleMessage(string sessionId, SendMessageRequest request)
{
if ((request.Text ?? "").Length == 0)
{
@@ -37,14 +36,14 @@ public abstract class AbstractDialogChatBot(
&& commando.GetCommands().Contains(splitCommand[1])
)
{
return commando.Handle(splitCommand[1], GetChatBot(), sessionId, request);
return await commando.Handle(splitCommand[1], GetChatBot(), sessionId, request);
}
if (
string.Equals(splitCommand.FirstOrDefault(), "help", StringComparison.OrdinalIgnoreCase)
)
{
return SendPlayerHelpMessage(sessionId, request);
return await SendPlayerHelpMessage(sessionId, request);
}
_mailSendService.SendUserMessageToPlayer(
@@ -55,10 +54,13 @@ public abstract class AbstractDialogChatBot(
null
);
return null;
return string.Empty;
}
protected string? SendPlayerHelpMessage(string sessionId, SendMessageRequest request)
protected async ValueTask<string> SendPlayerHelpMessage(
string sessionId,
SendMessageRequest request
)
{
_mailSendService.SendUserMessageToPlayer(
sessionId,
@@ -67,40 +69,34 @@ public abstract class AbstractDialogChatBot(
[],
null
);
// due to BSG being dumb with messages we need a mandatory timeout between messages so they get out on the right order
TimeoutCallback.RunInTimespan(
() =>
{
foreach (var chatCommand in _chatCommands.Values)
{
_mailSendService.SendUserMessageToPlayer(
sessionId,
GetChatBot(),
$"Commands available for \"{chatCommand.GetCommandPrefix()}\" prefix:",
[],
null
);
foreach (var chatCommand in _chatCommands.Values)
{
// due to BSG being dumb with messages we need a mandatory timeout between messages so they get out on the right order
await Task.Delay(TimeSpan.FromSeconds(1));
TimeoutCallback.RunInTimespan(
() =>
{
foreach (var subCommand in chatCommand.GetCommands())
{
_mailSendService.SendUserMessageToPlayer(
sessionId,
GetChatBot(),
$"Subcommand {subCommand}:\n{chatCommand.GetCommandHelp(subCommand)}",
[],
null
);
}
},
TimeSpan.FromSeconds(1)
);
}
},
TimeSpan.FromSeconds(1)
);
_mailSendService.SendUserMessageToPlayer(
sessionId,
GetChatBot(),
$"Commands available for \"{chatCommand.GetCommandPrefix()}\" prefix:",
[],
null
);
await Task.Delay(TimeSpan.FromSeconds(1));
foreach (var subCommand in chatCommand.GetCommands())
{
_mailSendService.SendUserMessageToPlayer(
sessionId,
GetChatBot(),
$"Subcommand {subCommand}:\n{chatCommand.GetCommandHelp(subCommand)}",
[],
null
);
await Task.Delay(TimeSpan.FromSeconds(1));
}
}
return request.DialogId;
}
@@ -8,7 +8,7 @@ public interface IChatCommand
public string GetCommandPrefix();
public string GetCommandHelp(string command);
public List<string> GetCommands();
public string Handle(
public ValueTask<string> Handle(
string command,
UserDialogInfo commandHandler,
string sessionId,
@@ -50,14 +50,14 @@ public class SptCommandoCommands : IChatCommand
return _sptCommands.Keys.ToList();
}
public string Handle(
public async ValueTask<string> Handle(
string command,
UserDialogInfo commandHandler,
string sessionId,
SendMessageRequest request
)
{
return _sptCommands[command].PerformAction(commandHandler, sessionId, request);
return await _sptCommands[command].PerformAction(commandHandler, sessionId, request);
}
public void RegisterSptCommandoCommand(ISptCommand command)
@@ -52,7 +52,7 @@ public class GiveSptCommand(
+ "give [locale] [\"item name\"] [quantity]\n\t\tEx: spt give fr \"figurine de chat\" 3";
}
public string PerformAction(
public ValueTask<string> PerformAction(
UserDialogInfo commandHandler,
string sessionId,
SendMessageRequest request
@@ -65,7 +65,7 @@ public class GiveSptCommand(
commandHandler,
"Invalid use of give command. Use 'help' for more information."
);
return request.DialogId;
return new ValueTask<string>(request.DialogId);
}
var result = _commandRegex.Match(request.Text);
@@ -86,7 +86,7 @@ public class GiveSptCommand(
commandHandler,
"Invalid use of give command. Use 'help' for more information."
);
return request.DialogId;
return new ValueTask<string>(request.DialogId);
}
_savedCommand.TryGetValue(sessionId, out var savedCommand);
@@ -98,7 +98,7 @@ public class GiveSptCommand(
commandHandler,
"Invalid selection. Outside of bounds! Use 'help' for more information."
);
return request.DialogId;
return new ValueTask<string>(request.DialogId);
}
item = savedCommand.PotentialItemNames[locationSixValue - 1];
@@ -128,7 +128,7 @@ public class GiveSptCommand(
commandHandler,
"Invalid quantity! Must be 1 or higher. Use 'help' for more information."
);
return request.DialogId;
return new ValueTask<string>(request.DialogId);
}
if (isItemName)
@@ -197,7 +197,7 @@ public class GiveSptCommand(
$"Could not find exact match. Closest are:\n{string.Join("\n", itemList)}\n\nUse 'spt give [above number]' to select one."
);
return request.DialogId;
return new ValueTask<string>(request.DialogId);
}
}
}
@@ -224,7 +224,7 @@ public class GiveSptCommand(
commandHandler,
"That item could not be found. Please refine your request and try again."
);
return request.DialogId;
return new ValueTask<string>(request.DialogId);
}
List<Item> itemsToSend = [];
@@ -286,7 +286,7 @@ public class GiveSptCommand(
"Too many items requested. Please lower the amount and try again."
);
return request.DialogId;
return new ValueTask<string>(request.DialogId);
}
}
}
@@ -300,7 +300,7 @@ public class GiveSptCommand(
itemsToSend
);
return request.DialogId;
return new ValueTask<string>(request.DialogId);
}
/// <summary>
@@ -7,7 +7,7 @@ public interface ISptCommand
{
public string GetCommand();
public string GetCommandHelp();
public string PerformAction(
public ValueTask<string> PerformAction(
UserDialogInfo commandHandler,
string sessionId,
SendMessageRequest request
@@ -42,7 +42,7 @@ public class ProfileSptCommand(
+ "spt profile skill metabolism 51";
}
public string PerformAction(
public ValueTask<string> PerformAction(
UserDialogInfo commandHandler,
string sessionId,
SendMessageRequest request
@@ -58,7 +58,7 @@ public class ProfileSptCommand(
commandHandler,
"Invalid use of trader command. Use 'help' for more information."
);
return request.DialogId;
return new ValueTask<string>(request.DialogId);
}
var result = _commandRegex.Match(request.Text);
@@ -82,7 +82,7 @@ public class ProfileSptCommand(
commandHandler,
"Invalid use of profile command, the level was outside bounds: 1 to 70. Use 'help' for more information."
);
return request.DialogId;
return new ValueTask<string>(request.DialogId);
}
profileChangeEvent = HandleLevelCommand(quantity);
@@ -102,7 +102,7 @@ public class ProfileSptCommand(
commandHandler,
"Invalid use of profile command, the skill was not found. Use 'help' for more information."
);
return request.DialogId;
return new ValueTask<string>(request.DialogId);
}
if (quantity is < 0 or > 51)
@@ -112,7 +112,7 @@ public class ProfileSptCommand(
commandHandler,
"Invalid use of profile command, the skill level was outside bounds: 1 to 51. Use 'help' for more information."
);
return request.DialogId;
return new ValueTask<string>(request.DialogId);
}
profileChangeEvent = HandleSkillCommand(enumSkill, quantity);
@@ -129,7 +129,7 @@ public class ProfileSptCommand(
commandHandler,
$"If you are reading this, this is bad. Please report this to SPT staff with a screenshot. Command: {command}."
);
return request.DialogId;
return new ValueTask<string>(request.DialogId);
}
_mailSendService.SendSystemMessageToPlayer(
@@ -149,7 +149,7 @@ public class ProfileSptCommand(
[profileChangeEvent]
);
return request.DialogId;
return new ValueTask<string>(request.DialogId);
}
protected ProfileChangeEvent HandleSkillCommand(SkillTypes? skill, int level)
@@ -34,7 +34,7 @@ public class TraderSptCommand(
return "spt trader \n ======== \n Sets the reputation or money spent to the input quantity through the message system.\n\n\tspt trader [trader] rep [quantity]\n\t\tEx: spt trader prapor rep 2\n\n\tspt trader [trader] spend [quantity]\n\t\tEx: spt trader therapist spend 1000000";
}
public string PerformAction(
public ValueTask<string> PerformAction(
UserDialogInfo commandHandler,
string sessionId,
SendMessageRequest request
@@ -47,7 +47,7 @@ public class TraderSptCommand(
commandHandler,
"Invalid use of trader command. Use 'help' for more information."
);
return request.DialogId;
return new ValueTask<string>(request.DialogId);
}
var result = _commandRegex.Match(request.Text);
@@ -75,7 +75,7 @@ public class TraderSptCommand(
"Invalid use of trader command, the trader was not found. Use 'help' for more information."
);
return request.DialogId;
return new ValueTask<string>(request.DialogId);
}
NotificationEventType profileChangeEventType;
@@ -96,7 +96,7 @@ public class TraderSptCommand(
"Invalid use of trader command, ProfileChangeEventType was not found. Use 'help' for more information."
);
return request.DialogId;
return new ValueTask<string>(request.DialogId);
}
}
@@ -117,7 +117,7 @@ public class TraderSptCommand(
[CreateProfileChangeEvent(profileChangeEventType, quantity, dbTrader.Id)]
);
return request.DialogId;
return new ValueTask<string>(request.DialogId);
}
protected ProfileChangeEvent CreateProfileChangeEvent(
@@ -6,5 +6,10 @@ namespace SPTarkov.Server.Core.Helpers.Dialogue;
public interface IDialogueChatBot
{
public UserDialogInfo GetChatBot();
public string? HandleMessage(string sessionId, SendMessageRequest request);
/// <summary>
/// Handles messages for the chatbot. If a message can't be handled, <see cref="string.Empty"/> should be used.
/// </summary>
/// <returns>The response of the bot, or <see cref="string.Empty"/> if the request could not be handled.</returns>
public ValueTask<string> HandleMessage(string sessionId, SendMessageRequest request);
}
@@ -13,9 +13,7 @@ namespace SPTarkov.Server.Core.Helpers.Dialogue;
[Injectable]
public class SptDialogueChatBot(
ISptLogger<AbstractDialogChatBot> _logger,
MailSendService _mailSendService,
IEnumerable<IChatCommand> _chatCommands,
ConfigServer _configServer,
ProfileHelper _profileHelper,
IEnumerable<IChatMessageHandler> chatMessageHandlers
@@ -42,7 +40,7 @@ public class SptDialogueChatBot(
};
}
public string? HandleMessage(string sessionId, SendMessageRequest request)
public ValueTask<string> HandleMessage(string sessionId, SendMessageRequest request)
{
var sender = _profileHelper.GetPmcProfile(sessionId);
var sptFriendUser = GetChatBot();
@@ -57,7 +55,7 @@ public class SptDialogueChatBot(
{
handler.Process(sessionId, sptFriendUser, sender, request);
return request.DialogId;
return new ValueTask<string>(request.DialogId);
}
_mailSendService.SendUserMessageToPlayer(
@@ -68,7 +66,7 @@ public class SptDialogueChatBot(
null
);
return request.DialogId;
return new ValueTask<string>(request.DialogId);
}
protected static List<IChatMessageHandler> ChatMessageHandlerSetup(
@@ -86,7 +84,7 @@ public class SptDialogueChatBot(
return "Unknown command.";
}
protected string? SendPlayerHelpMessage(string sessionId, SendMessageRequest request)
protected ValueTask<string> SendPlayerHelpMessage(string sessionId, SendMessageRequest request)
{
_mailSendService.SendUserMessageToPlayer(
sessionId,
@@ -96,6 +94,6 @@ public class SptDialogueChatBot(
null
);
return request.DialogId;
return new ValueTask<string>(request.DialogId);
}
}
@@ -10,14 +10,14 @@ public record SendMessageRequest : IRequestData
public Dictionary<string, object>? ExtensionData { get; set; }
[JsonPropertyName("dialogId")]
public string? DialogId { get; set; }
public required string DialogId { get; set; }
[JsonPropertyName("type")]
public MessageType? Type { get; set; }
public required MessageType Type { get; set; }
[JsonPropertyName("text")]
public string? Text { get; set; }
public required string Text { get; set; }
[JsonPropertyName("replyTo")]
public string? ReplyTo { get; set; }
public required string ReplyTo { get; set; }
}
@@ -1,13 +0,0 @@
namespace SPTarkov.Server.Core.Utils.Callbacks;
public static class TimeoutCallback
{
public static Task RunInTimespan(Action action, TimeSpan timeSpan)
{
return Task.Factory.StartNew(() =>
{
Thread.Sleep(timeSpan);
action();
});
}
}
@@ -1,29 +0,0 @@
using System.Diagnostics;
using SPTarkov.DI.Annotations;
namespace SPTarkov.Server.Core.Utils;
[Injectable]
public class TimerUtil
{
protected readonly Stopwatch _stopwatch;
public TimerUtil()
{
_stopwatch = new Stopwatch();
_stopwatch.Start();
}
public int Stop(string unit = "sec")
{
_stopwatch.Stop();
var timePassed = _stopwatch.Elapsed;
return unit switch
{
"ns" => timePassed.Nanoseconds,
"ms" => timePassed.Milliseconds,
_ => timePassed.Seconds,
};
}
}