#W# Confict: Fixed BaseCreature git merge conflict.

This commit is contained in:
WarrentyExpired 2026-10-10 23:17:41 -04:00
commit ad38314ca7
112 changed files with 5397 additions and 616 deletions

View file

@ -6,11 +6,22 @@ using MimeKit;
using Server.Accounting;
using Server.Configurations;
using Server.Engines.Help;
using Server.Logging;
namespace Server.Misc
{
public static class Email
{
private static readonly ILogger _logger = LogFactory.GetLogger(typeof(Email));
// How long SendCrashEmail holds up the exit after a crash: at the default settings, room for the first
// attempts (3 and 6 s apart; a fourth starts at 21 s when they fail fast), and no longer when the SMTP
// server is dead.
private static readonly TimeSpan CrashEmailTimeout = TimeSpan.FromSeconds(30);
// A retry wait stops doubling at an hour: no multi-day waits, and nothing left to overflow.
private const int MaxRetryDelaySeconds = 3600;
/// <summary>
/// Sends Queue-Page request using Email
/// </summary>
@ -69,16 +80,17 @@ namespace Server.Misc
}.ToMessageBody();
}
SendAsync(message);
_ = SendAsync(message);
}
/// <summary>
/// Sends crash email
/// Sends crash email, blocking for up to the crash timeout (30 s) while it is sent: the core exits
/// as soon as the crash handlers return, and a send left running dies with it.
/// </summary>
/// <param name="filePath"></param>
public static void SendCrashEmail(string filePath)
{
if (EmailConfiguration.EmailEnabled)
if (!EmailConfiguration.EmailEnabled)
{
return;
}
@ -94,27 +106,37 @@ namespace Server.Misc
};
builder.Attachments.Add(filePath);
message.Body = builder.ToMessageBody();
try
{
if (!SendAsync(message).Wait(CrashEmailTimeout))
{
_logger.Warning("Crash email not sent after {Timeout}, giving up", CrashEmailTimeout);
}
}
catch (Exception ex)
{
_logger.Error(ex, "Sending crash email failed");
}
}
/// <summary>
/// Sends emails async
/// </summary>
/// <param name="message"></param>
private static async void SendAsync(MimeMessage message)
private static async Task SendAsync(MimeMessage message)
{
if (!EmailConfiguration.EmailEnabled)
{
return;
}
var now = Core.Now;
var messageID = $"<{now:yyyyMMdd}.{now:HHmmssff}@{EmailConfiguration.EmailServer}>";
message.Headers.Add("Message-ID", messageID);
message.From.Add(EmailConfiguration.FromAddress);
var attempts = EmailConfiguration.EmailSendRetryCount;
var retryDelays = RetryDelays(attempts, EmailConfiguration.EmailSendRetryDelay);
Exception lastException = null;
var delay = EmailConfiguration.EmailSendRetryDelay;
for (var i = 0; i < EmailConfiguration.EmailSendRetryCount; i++)
// SendCrashEmail can block the game loop on this task: every await needs ConfigureAwait(false).
for (var i = 0; i < attempts; i++)
{
try
{
@ -130,17 +152,40 @@ namespace Server.Misc
}
catch (Exception ex)
{
lastException = ex;
if (i == 0)
{
Console.WriteLine(ex.Message);
Console.WriteLine(ex.StackTrace);
_logger.Warning(ex, "Sending email {Subject} failed", message.Subject);
}
delay *= delay;
await Task.Delay(delay * 1000).ConfigureAwait(false);
if (i < retryDelays.Length)
{
await Task.Delay(retryDelays[i]).ConfigureAwait(false);
}
}
}
_logger.Error(lastException, "Email {Subject} not sent after {Attempts} attempts", message.Subject, attempts);
}
/// <summary>
/// The waits between send attempts: the configured delay, doubling each time, and none after the last.
/// </summary>
/// <param name="attempts"></param>
/// <param name="firstDelaySeconds"></param>
internal static TimeSpan[] RetryDelays(int attempts, int firstDelaySeconds)
{
var delays = new TimeSpan[Math.Max(attempts, 1) - 1];
var seconds = Math.Clamp(firstDelaySeconds, 0, MaxRetryDelaySeconds);
for (var i = 0; i < delays.Length; i++)
{
delays[i] = TimeSpan.FromSeconds(seconds);
seconds = Math.Min(seconds * 2, MaxRetryDelaySeconds);
}
return delays;
}
}
}

View file

@ -228,16 +228,11 @@ namespace Server.Misc
}
if (bcTarg?.Controlled == true
|| bcTarg?.Summoned == true && bcTarg.SummonMaster != from && bcTarg.SummonMaster?.Player == true)
|| bcTarg?.SummonMaster is { Player: true } summoner && summoner != from)
{
return false; // Cannot harm other controlled mobiles from players
}
if (pmFrom == null && bcFrom != null && bcFrom.Summoned && target.Player)
{
return true; // Summons from monsters can attack players
}
if (target.Player)
{
return false; // Cannot harm other players
@ -275,7 +270,24 @@ namespace Server.Misc
return Notoriety.CanBeAttacked;
}
Body body = target.Amount;
// BaseCreatures are deleted on death, so target.Owner is null after a server restart.
// The OwnerWasBaseCreature flag is the persisted snapshot that survives the live mobile.
var creature = target.Owner as BaseCreature;
var ownerWasCreature = target.OwnerWasBaseCreature || creature != null;
// A player's murderer and criminal status outrank guild standing, as on the live mobile.
if (!ownerWasCreature)
{
if (target.Murderer)
{
return Notoriety.Murderer;
}
if (target.Criminal && (target.Map?.Rules & MapRules.HarmfulRestrictions) == 0)
{
return Notoriety.Criminal;
}
}
var sourceGuild = GetGuildFor(source.Guild as Guild, source);
var targetGuild = GetGuildFor(target.Guild, target.Owner);
@ -297,11 +309,7 @@ namespace Server.Misc
}
}
// BaseCreatures are deleted on death, so target.Owner is null after a server restart.
// The OwnerWasBaseCreature flag is the persisted snapshot that survives the live mobile.
var creature = target.Owner as BaseCreature;
if (target.OwnerWasBaseCreature || creature != null)
if (ownerWasCreature)
{
if (srcFaction != null && trgFaction != null && srcFaction != trgFaction && source.Map == Faction.Facet)
{
@ -314,6 +322,7 @@ namespace Server.Misc
}
var actual = Notoriety.CanBeAttacked;
Body body = target.Amount;
if (target.Murderer || body.IsMonster && target.OwnerWasSummoned || target.OwnerWasAnimatedDead)
{
@ -338,16 +347,6 @@ namespace Server.Misc
return Notoriety.Innocent;
}
if (target.Murderer || body.IsMonster)
{
return Notoriety.Murderer;
}
if (target.Criminal && (target.Map?.Rules & MapRules.HarmfulRestrictions) == 0)
{
return Notoriety.Criminal;
}
if (srcFaction != null && trgFaction != null && srcFaction != trgFaction && source.Map == Faction.Facet)
{
for (var i = 0; i < list.Count; ++i)
@ -422,7 +421,7 @@ namespace Server.Misc
return Notoriety.CanBeAttacked;
}
master = bcTarg.ControlMaster;
master = bcTarg.Controlled ? bcTarg.ControlMaster : null;
if (Core.ML && master != null)
{