#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

@ -225,7 +225,7 @@ public sealed class AITimer : Timer
private void CacheDetectHiddenDelays()
{
var delay = Math.Min(30000 / _owner.Mobile.Int, 120);
var delay = Math.Min(30000 / Math.Max(1, _owner.Mobile.Int), 120);
_detectHiddenMinDelay = delay * 900; // 26s to 108s
_detectHiddenMaxDelay = delay * 1100; // 32s to 132s
}

View file

@ -131,21 +131,57 @@ public abstract partial class BaseAI
}
}
public virtual void OnAggressiveAction(Mobile aggressor)
/// <summary>
/// The retaliation policy: how the creature answers <paramref name="aggressor"/>. Returns false when it
/// stands down, and <see cref="BaseCreature.AggressiveAction"/> then skips its combat bookkeeping.
/// </summary>
public virtual bool OnAggressiveAction(Mobile aggressor)
{
if (Mobile.RefusesGuardTarget(aggressor))
{
DebugSay("Friendly fire; I keep guarding.");
return false;
}
// Only a creature somebody can command has been told anything; wild creatures rest on None.
var toldToStandDown = Mobile.ControlMaster != null && Mobile.Commandable &&
IsStandDownOrder(Mobile.ControlOrder);
if (Mobile.StandsDownOnCommand && toldToStandDown)
{
DebugSay("I'm being attacked but my master told me not to fight.");
Mobile.Warmode = false;
return false;
}
PreferCloserAggressor(aggressor);
if (aggressor.ChangingCombatant && toldToStandDown)
{
// With stand-down off, a resting pet answers a direct attack with a full Attack order.
Mobile.IssueOrder(OrderType.Attack, null, aggressor);
}
else if (Mobile.Combatant == null && !Mobile.BardPacified)
{
Mobile.Warmode = true;
Mobile.Combatant = aggressor;
}
return true;
}
// A visible aggressor closer than the current combatant takes over.
protected void PreferCloserAggressor(Mobile aggressor)
{
if (aggressor.Hidden)
{
return;
}
var currentCombat = Mobile.Combatant;
var current = Mobile.Combatant;
if (currentCombat == null || currentCombat == aggressor)
{
return;
}
if (Mobile.GetDistanceToSqrt(aggressor) < Mobile.GetDistanceToSqrt(currentCombat))
if (current != null && current != aggressor &&
Mobile.GetDistanceToSqrt(aggressor) < Mobile.GetDistanceToSqrt(current))
{
Mobile.Combatant = aggressor;
}
@ -826,7 +862,19 @@ public abstract partial class BaseAI
return false;
}
if (HandleBardProvoked() || HandleControlled() || HandleConstantFocus())
if (HandleBardProvoked())
{
return true;
}
// A controlled creature must not fall through to wild target selection when
// its order has no target. Guard in particular normally has no ControlTarget.
if (Mobile.Controlled)
{
return HandleControlled();
}
if (HandleConstantFocus())
{
return true;
}
@ -881,8 +929,17 @@ public abstract partial class BaseAI
private bool HandleControlled()
{
if (!Mobile.Controlled)
if (Mobile.ControlOrder == OrderType.Guard)
{
Mobile.FocusMob = FindGuardTarget();
return Mobile.FocusMob != null;
}
// Follow and other non-combat orders may target the owner or a friend.
// Only an explicit attack order supplies a combat target.
if (Mobile.ControlOrder != OrderType.Attack)
{
Mobile.FocusMob = null;
return false;
}
@ -939,6 +996,7 @@ public abstract partial class BaseAI
Mobile enemySummonMob = null;
var val = double.MinValue;
var enemySummonVal = double.MinValue;
Mobile summonMaster = null;
foreach (var m in map.GetMobilesInRange(Mobile.Location, iRange))
{
@ -948,14 +1006,24 @@ public abstract partial class BaseAI
}
var bc = m as BaseCreature;
var pm = m as PlayerMobile;
if (IsInvalidSummonTarget(m, bc, pm) || IsInvalidFactionTarget(m, bFacFriend, bFacFoe)
|| IsInvalidFightModeTarget(m, acqType, bc))
if (IsInvalidSummonTarget(m, bc) || IsInvalidFactionTarget(m, bFacFriend, bFacFoe)
|| IsInvalidFightModeTarget(m, acqType, bc))
{
continue;
}
// Only a summon that ignores the acquire rules gets here with its caster; anyone else comes first.
if (m == Mobile.SummonMaster)
{
if (Mobile.InLOS(m))
{
summonMaster = m;
}
continue;
}
var theirVal = Mobile.GetFightModeRanking(m, acqType, bPlayerOnly);
if (theirVal > val && Mobile.InLOS(m))
@ -963,15 +1031,14 @@ public abstract partial class BaseAI
newFocusMob = m;
val = theirVal;
}
else if (Core.AOS && theirVal > enemySummonVal
&& Mobile.InLOS(m) && bc?.Summoned == true && bc.Controlled != true)
else if (Core.AOS && theirVal > enemySummonVal && Mobile.InLOS(m) && bc is { Summoned: true, Controlled: false })
{
enemySummonMob = m;
enemySummonVal = theirVal;
}
}
Mobile.FocusMob = newFocusMob ?? enemySummonMob;
Mobile.FocusMob = newFocusMob ?? enemySummonMob ?? summonMaster;
return Mobile.FocusMob != null;
}
@ -979,21 +1046,28 @@ public abstract partial class BaseAI
m.Deleted || m.Blessed || m == Mobile || m is BaseFamiliar || !m.Alive || m.IsDeadBondedPet ||
m.AccessLevel > AccessLevel.Player || bPlayerOnly && !m.Player || !Mobile.CanSee(m);
private bool IsInvalidSummonTarget(Mobile m, BaseCreature bc, PlayerMobile pm)
private bool IsInvalidSummonTarget(Mobile m, BaseCreature bc)
{
if (Core.AOS && bc?.Summoned == true &&
(bc.SummonMaster == Mobile || !bc.SummonMaster.Player && IsHostile(bc.SummonMaster)))
// A summon whose caster was deleted before a reload comes back with no master.
if (Core.AOS &&
(bc?.SummonMaster == Mobile || bc?.SummonMaster is { Player: false } summonMaster && IsHostile(summonMaster)))
{
return true;
}
if (!Mobile.Summoned || Mobile.SummonMaster == null)
var master = Mobile.SummonMaster;
if (master == null)
{
return false;
}
return m == Mobile.SummonMaster || !SpellHelper.ValidIndirectTarget(Mobile.SummonMaster, m) ||
Mobile.IsAnimatedDead && (pm != null || bc?.IsAnimatedDead == true || bc?.Controlled == true);
if (Mobile.IsAnimatedDead && (m.Player || bc?.IsAnimatedDead == true || bc?.Controlled == true))
{
return true;
}
return Mobile.FollowsAcquireRules && (m == master || !SpellHelper.ValidIndirectTarget(master, m));
}
private bool IsInvalidFactionTarget(Mobile m, bool bFacFriend, bool bFacFoe) =>

View file

@ -451,11 +451,6 @@ public abstract partial class BaseAI
Mobile.SetControlMaster(e.Mobile);
if (Mobile.SummonMaster != null)
{
Mobile.SummonMaster = e.Mobile;
}
return true;
}
}

View file

@ -769,6 +769,15 @@ public abstract partial class BaseAI
}
}
// Uncontrolled summons (energy vortex, blade spirits) turn on their caster, so a guard still defends against them.
internal bool IsGuardAlly(Mobile target)
{
var master = Mobile.ControlMaster;
return target == Mobile || master != null &&
(target == master || target is BaseCreature creature && creature.ControlMaster == master);
}
/// <summary>
/// Selects the aggressor closest to the master. The current combatant is kept
/// unless a strictly closer one exists. Never mutates order state.
@ -779,12 +788,12 @@ public abstract partial class BaseAI
var anchor = controlMaster ?? Mobile;
var current = Mobile.Combatant;
var best = current != controlMaster && IsValidCombatant(current) ? current : null;
var best = !IsGuardAlly(current) && IsValidCombatant(current) ? current : null;
var bestDist = best?.GetDistanceToSqrt(anchor) ?? double.MaxValue;
foreach (var aggr in Mobile.GetMobilesInRange(Mobile.RangePerception))
{
if (aggr == best || aggr == Mobile || aggr == controlMaster ||
if (aggr == best || IsGuardAlly(aggr) ||
aggr.IsDeadBondedPet || !aggr.Alive ||
aggr.Combatant != Mobile && (controlMaster == null || aggr.Combatant != controlMaster))
{
@ -808,7 +817,7 @@ public abstract partial class BaseAI
{
var aggressor = aggressors[i].Attacker;
if (aggressor == best || aggressor?.Deleted != false || !aggressor.Alive ||
if (aggressor == best || aggressor?.Deleted != false || IsGuardAlly(aggressor) || !aggressor.Alive ||
aggressor.IsDeadBondedPet || !Mobile.InRange(aggressor, Mobile.RangePerception))
{
continue;

View file

@ -156,11 +156,6 @@ internal sealed partial class TransferItem : Item
private void TransferPetOwnership(Mobile from, Mobile to)
{
if (_creature.SummonMaster != null)
{
_creature.SummonMaster = to;
}
_creature.ClearPetFriends();
_creature.IssueOrder(OrderType.Follow, null, to);
_creature.BondingBegin = DateTime.MinValue;

View file

@ -47,21 +47,25 @@ public class FamiliarAI : BaseAI
return Mobile.Controlled ? OrderType.Come : OrderType.None;
}
public override void OnAggressiveAction(Mobile aggressor)
// No orders to stand down on; never fights for a hidden caster.
public override bool OnAggressiveAction(Mobile aggressor)
{
if (!Familiar.AssistsMaster || aggressor.Hidden || Familiar.ControlMaster?.Hidden == true)
{
return;
return false;
}
if (Mobile.Combatant == null)
{
Mobile.Warmode = true;
Mobile.Combatant = aggressor;
return;
}
else
{
PreferCloserAggressor(aggressor);
}
base.OnAggressiveAction(aggressor);
return true;
}
private bool Act()
@ -148,7 +152,7 @@ public class FamiliarAI : BaseAI
var combatant = target.Combatant;
return combatant == Mobile || combatant == master ||
combatant is BaseCreature { Controlled: true } pet && pet.ControlMaster == master;
combatant is BaseCreature pet && pet.ControlMaster == master;
}
// Closest mobile in a fight with the caster's side.

View file

@ -48,7 +48,7 @@ public abstract class SummonUndeadCounter : MonsterAbility
summon = CreateSummon(source);
summon.Team = source.Team;
summon.Summoned = true;
summon.SummonMaster = source;
summon.Master = source;
summon.FightMode = FightMode.Closest;
summon.Combatant = target;
summon.Home = source.Home;

View file

@ -137,7 +137,7 @@ namespace Server.Mobiles
public static bool CheckAccess(BaseCreature animal, Mobile from) =>
from == animal || from.AccessLevel >= AccessLevel.GameMaster || from.Alive && animal.Controlled &&
!animal.IsDeadPet &&
(from == animal.ControlMaster || from == animal.SummonMaster || animal.IsPetFriend(from));
(from == animal.GetMaster() || animal.IsPetFriend(from));
public static void CombineBackpacks(BaseCreature animal)
{

View file

@ -239,8 +239,7 @@ public abstract partial class BaseMount : BaseCreature, IMount
if (from.InRange(this, 1))
{
var canAccess = from.AccessLevel >= AccessLevel.GameMaster
|| Controlled && ControlMaster == from
|| Summoned && SummonMaster == from;
|| GetMaster() == from;
if (canAccess)
{

View file

@ -71,7 +71,7 @@ namespace Server.Mobiles
public override void OnHarmfulSpell(Mobile from)
{
if (!Controlled && ControlMaster == null)
if (!Controlled)
{
CurrentSpeed = BoostedSpeed;
}
@ -79,7 +79,7 @@ namespace Server.Mobiles
public override void OnCombatantChange()
{
if (Combatant == null && !Controlled && ControlMaster == null)
if (Combatant == null && !Controlled)
{
CurrentSpeed = PassiveSpeed;
}

View file

@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using ModernUO.CodeGeneratedEvents;
using ModernUO.Serialization;
using Server.Collections;
using Server.ContextMenus;
@ -136,7 +135,7 @@ namespace Server.Mobiles
public int CompareTo(DamageStore ds) => (ds?.m_Damage ?? 0).CompareTo(m_Damage);
}
[SerializationGenerator(23, false)]
[SerializationGenerator(24, false)]
public abstract partial class BaseCreature : Mobile, IHonorTarget, IQuestGiver
{
private static readonly ILogger logger = LogFactory.GetLogger(typeof(BaseCreature));
@ -451,10 +450,10 @@ namespace Server.Mobiles
InvalidateProperties();
}
// Follower bookkeeping brackets the assignment, so the property is hand-written.
private Mobile _controlMaster;
// Follower bookkeeping brackets the assignment, so Master is hand-written.
private Mobile _master;
private bool ShouldSerializeControlMaster() => _controlMaster != null;
private bool ShouldSerializeMaster() => _master != null;
[SerializableField(16)]
[SaveFlag(nameof(ShouldSerializeControlTarget))]
@ -508,13 +507,8 @@ namespace Server.Mobiles
private bool ShouldSerializeSummonEnd() => _summoned;
// Follower bookkeeping brackets the assignment, so the property is hand-written.
private Mobile _summonMaster;
private bool ShouldSerializeSummonMaster() => _summonMaster != null;
[EncodedInt]
[SerializableField(24)]
[SerializableField(23)]
[SaveFlag(nameof(ShouldSerializeControlSlots), nameof(ControlSlotsDefaultValue))]
[SerializedCommandProperty(AccessLevel.Administrator)]
private int _controlSlots = 1;
@ -524,7 +518,7 @@ namespace Server.Mobiles
private int ControlSlotsDefaultValue() => 1;
[EncodedInt]
[SerializableField(25, allowFieldChange: nameof(ClampLoyalty))]
[SerializableField(24, allowFieldChange: nameof(ClampLoyalty))]
[SaveFlag(nameof(ShouldSerializeLoyalty), nameof(LoyaltyDefaultValue))]
[SerializedCommandProperty(AccessLevel.GameMaster)]
private int _loyalty;
@ -539,7 +533,7 @@ namespace Server.Mobiles
return true;
}
[SerializableField(26)]
[SerializableField(25)]
[SaveFlag(nameof(ShouldSerializeCurrentWayPoint))]
[SerializedCommandProperty(AccessLevel.GameMaster)]
private WayPoint _currentWayPoint;
@ -547,7 +541,7 @@ namespace Server.Mobiles
private bool ShouldSerializeCurrentWayPoint() => _currentWayPoint != null;
[EncodedInt]
[SerializableField(27)]
[SerializableField(26)]
[SaveFlag(nameof(ShouldSerializeHitsMaxSeed), nameof(HitsMaxSeedDefaultValue))]
[SerializedCommandProperty(AccessLevel.GameMaster)]
private int _hitsMaxSeed = -1;
@ -557,7 +551,7 @@ namespace Server.Mobiles
private int HitsMaxSeedDefaultValue() => -1;
[EncodedInt]
[SerializableField(28)]
[SerializableField(27)]
[SaveFlag(nameof(ShouldSerializeStamMaxSeed), nameof(StamMaxSeedDefaultValue))]
[SerializedCommandProperty(AccessLevel.GameMaster)]
private int _stamMaxSeed = -1;
@ -567,7 +561,7 @@ namespace Server.Mobiles
private int StamMaxSeedDefaultValue() => -1;
[EncodedInt]
[SerializableField(29)]
[SerializableField(28)]
[SaveFlag(nameof(ShouldSerializeManaMaxSeed), nameof(ManaMaxSeedDefaultValue))]
[SerializedCommandProperty(AccessLevel.GameMaster)]
private int _manaMaxSeed = -1;
@ -577,7 +571,7 @@ namespace Server.Mobiles
private int ManaMaxSeedDefaultValue() => -1;
[EncodedInt]
[SerializableField(30)]
[SerializableField(29)]
[SaveFlag(nameof(ShouldSerializeDamageMin), nameof(DamageMinDefaultValue))]
[SerializedCommandProperty(AccessLevel.GameMaster)]
private int _damageMin = -1;
@ -587,7 +581,7 @@ namespace Server.Mobiles
private int DamageMinDefaultValue() => -1;
[EncodedInt]
[SerializableField(31)]
[SerializableField(30)]
[SaveFlag(nameof(ShouldSerializeDamageMax), nameof(DamageMaxDefaultValue))]
[SerializedCommandProperty(AccessLevel.GameMaster)]
private int _damageMax = -1;
@ -597,7 +591,7 @@ namespace Server.Mobiles
private int DamageMaxDefaultValue() => -1;
[EncodedInt]
[SerializableField(32, fieldChanged: nameof(OnResistanceSeedChange))]
[SerializableField(31, fieldChanged: nameof(OnResistanceSeedChange))]
[SaveFlag(nameof(ShouldSerializePhysicalResistanceSeed))]
[SerializedCommandProperty(AccessLevel.GameMaster)]
private int _physicalResistanceSeed;
@ -607,7 +601,7 @@ namespace Server.Mobiles
private void OnResistanceSeedChange(int oldValue, int newValue) => UpdateResistances();
[EncodedInt]
[SerializableField(33, fieldChanged: nameof(OnResistanceSeedChange))]
[SerializableField(32, fieldChanged: nameof(OnResistanceSeedChange))]
[SaveFlag(nameof(ShouldSerializeFireResistSeed))]
[SerializedCommandProperty(AccessLevel.GameMaster)]
private int _fireResistSeed;
@ -615,7 +609,7 @@ namespace Server.Mobiles
private bool ShouldSerializeFireResistSeed() => _fireResistSeed != 0;
[EncodedInt]
[SerializableField(34, fieldChanged: nameof(OnResistanceSeedChange))]
[SerializableField(33, fieldChanged: nameof(OnResistanceSeedChange))]
[SaveFlag(nameof(ShouldSerializeColdResistSeed))]
[SerializedCommandProperty(AccessLevel.GameMaster)]
private int _coldResistSeed;
@ -623,7 +617,7 @@ namespace Server.Mobiles
private bool ShouldSerializeColdResistSeed() => _coldResistSeed != 0;
[EncodedInt]
[SerializableField(35, fieldChanged: nameof(OnResistanceSeedChange))]
[SerializableField(34, fieldChanged: nameof(OnResistanceSeedChange))]
[SaveFlag(nameof(ShouldSerializePoisonResistSeed))]
[SerializedCommandProperty(AccessLevel.GameMaster)]
private int _poisonResistSeed;
@ -631,7 +625,7 @@ namespace Server.Mobiles
private bool ShouldSerializePoisonResistSeed() => _poisonResistSeed != 0;
[EncodedInt]
[SerializableField(36, fieldChanged: nameof(OnResistanceSeedChange))]
[SerializableField(35, fieldChanged: nameof(OnResistanceSeedChange))]
[SaveFlag(nameof(ShouldSerializeEnergyResistSeed))]
[SerializedCommandProperty(AccessLevel.GameMaster)]
private int _energyResistSeed;
@ -639,7 +633,7 @@ namespace Server.Mobiles
private bool ShouldSerializeEnergyResistSeed() => _energyResistSeed != 0;
[EncodedInt]
[SerializableField(37)]
[SerializableField(36)]
[SaveFlag(nameof(ShouldSerializePhysicalDamage), nameof(PhysicalDamageDefaultValue))]
[SerializedCommandProperty(AccessLevel.GameMaster)]
private int _physicalDamage = 100;
@ -649,7 +643,7 @@ namespace Server.Mobiles
private int PhysicalDamageDefaultValue() => 100;
[EncodedInt]
[SerializableField(38)]
[SerializableField(37)]
[SaveFlag(nameof(ShouldSerializeFireDamage))]
[SerializedCommandProperty(AccessLevel.GameMaster)]
private int _fireDamage;
@ -657,7 +651,7 @@ namespace Server.Mobiles
private bool ShouldSerializeFireDamage() => _fireDamage != 0;
[EncodedInt]
[SerializableField(39)]
[SerializableField(38)]
[SaveFlag(nameof(ShouldSerializeColdDamage))]
[SerializedCommandProperty(AccessLevel.GameMaster)]
private int _coldDamage;
@ -665,7 +659,7 @@ namespace Server.Mobiles
private bool ShouldSerializeColdDamage() => _coldDamage != 0;
[EncodedInt]
[SerializableField(40)]
[SerializableField(39)]
[SaveFlag(nameof(ShouldSerializePoisonDamage))]
[SerializedCommandProperty(AccessLevel.GameMaster)]
private int _poisonDamage;
@ -673,7 +667,7 @@ namespace Server.Mobiles
private bool ShouldSerializePoisonDamage() => _poisonDamage != 0;
[EncodedInt]
[SerializableField(41)]
[SerializableField(40)]
[SaveFlag(nameof(ShouldSerializeEnergyDamage))]
[SerializedCommandProperty(AccessLevel.GameMaster)]
private int _energyDamage;
@ -681,7 +675,7 @@ namespace Server.Mobiles
private bool ShouldSerializeEnergyDamage() => _energyDamage != 0;
[Tidy]
[SerializableField(42, setter: "private")]
[SerializableField(41, setter: "private")]
[SaveFlag(nameof(ShouldSerializeOwners), nameof(OwnersDefaultValue))]
private List<Mobile> _owners;
@ -693,13 +687,13 @@ namespace Server.Mobiles
private List<Mobile> OwnersDefaultValue() => new();
[SerializableField(43)]
[SerializableField(42)]
[SaveFlag(nameof(ShouldSerializeIsDeadPet))]
private bool _isDeadPet;
private bool ShouldSerializeIsDeadPet() => _isDeadPet;
[SerializableField(44, fieldChanged: nameof(OnBondedChange))]
[SerializableField(43, fieldChanged: nameof(OnBondedChange))]
[SaveFlag(nameof(ShouldSerializeIsBonded))]
[SerializedCommandProperty(AccessLevel.GameMaster)]
private bool _isBonded;
@ -708,21 +702,21 @@ namespace Server.Mobiles
private void OnBondedChange(bool oldValue, bool newValue) => InvalidateProperties();
[SerializableField(45)]
[SerializableField(44)]
[SaveFlag(nameof(ShouldSerializeBondingBegin))]
[SerializedCommandProperty(AccessLevel.GameMaster)]
private DateTime _bondingBegin;
private bool ShouldSerializeBondingBegin() => _bondingBegin != DateTime.MinValue;
[SerializableField(46)]
[SerializableField(45)]
[SaveFlag(nameof(ShouldSerializeOwnerAbandonTime))]
[SerializedCommandProperty(AccessLevel.GameMaster)]
private DateTime _ownerAbandonTime;
private bool ShouldSerializeOwnerAbandonTime() => _ownerAbandonTime != DateTime.MinValue;
[SerializableField(47)]
[SerializableField(46)]
[SaveFlag(nameof(ShouldSerializeHasGeneratedLoot))]
private bool _hasGeneratedLoot;
@ -734,7 +728,7 @@ namespace Server.Mobiles
private bool ShouldSerializeIsParagon() => _isParagon;
[Tidy]
[SerializableField(49, setter: "private")]
[SerializableField(48, setter: "private")]
[SaveFlag(nameof(ShouldSerializeFriends))]
private List<Mobile> _friends;
@ -744,7 +738,7 @@ namespace Server.Mobiles
return _friends?.Count > 0;
}
[SerializableField(50)]
[SerializableField(49)]
[SaveFlag(nameof(ShouldSerializeRemoveIfUntamed))]
[SerializedCommandProperty(AccessLevel.GameMaster)]
private bool _removeIfUntamed;
@ -752,21 +746,21 @@ namespace Server.Mobiles
private bool ShouldSerializeRemoveIfUntamed() => _removeIfUntamed;
[EncodedInt]
[SerializableField(51)]
[SerializableField(50)]
[SaveFlag(nameof(ShouldSerializeRemoveStep))]
[SerializedCommandProperty(AccessLevel.GameMaster)]
private int _removeStep;
private bool ShouldSerializeRemoveStep() => _removeStep != 0;
[SerializableField(52, setter: "private")]
[SerializableField(51, setter: "private")]
[SaveFlag(nameof(ShouldSerializePendingDeleteTimer))]
[DeserializeTimer(nameof(DeserializePendingDeleteTimer))]
private Timer _pendingDeleteTimer;
// Stabled and controlled pets never resume a delete countdown.
private bool ShouldSerializePendingDeleteTimer() =>
_pendingDeleteTimer?.Running == true && !IsStabled && !(_controlled && _controlMaster != null);
_pendingDeleteTimer?.Running == true && !IsStabled && ControlMaster == null;
private void DeserializePendingDeleteTimer(TimeSpan delay)
{
@ -774,7 +768,7 @@ namespace Server.Mobiles
_pendingDeleteTimer.Start();
}
[SerializableField(53)]
[SerializableField(52)]
[SaveFlag(nameof(ShouldSerializeCorpseNameOverride))]
[SerializedCommandProperty(AccessLevel.GameMaster)]
private string _corpseNameOverride;
@ -852,7 +846,6 @@ namespace Server.Mobiles
Debug = false;
_controlled = false;
_controlMaster = null;
ControlTarget = null;
_controlOrder = OrderType.None;
@ -925,6 +918,10 @@ namespace Server.Mobiles
[CommandProperty(AccessLevel.GameMaster)]
public bool IsPrisoner { get; set; }
/// <summary>
/// When false, a summon may target its own caster and anyone the caster's harmful spells would spare.
/// It still prefers any other target over the caster.
/// </summary>
public virtual bool FollowsAcquireRules => true;
public virtual Faction FactionAllegiance => null;
@ -932,7 +929,7 @@ namespace Server.Mobiles
public virtual double WeaponAbilityChance => 0.4;
[SerializableProperty(48, useField: nameof(_isParagon))]
[SerializableProperty(47, useField: nameof(_isParagon))]
[SaveFlag(nameof(ShouldSerializeIsParagon))]
[CommandProperty(AccessLevel.GameMaster)]
public bool IsParagon
@ -1029,8 +1026,8 @@ namespace Server.Mobiles
}
public virtual bool IsNecroFamiliar =>
Summoned && _controlMaster != null &&
SummonFamiliarSpell.Table.TryGetValue(_controlMaster, out var bc) && bc == this;
Summoned && ControlMaster != null &&
SummonFamiliarSpell.Table.TryGetValue(ControlMaster, out var bc) && bc == this;
public virtual bool DeleteCorpseOnDeath => !Core.AOS && _summoned;
@ -1158,23 +1155,26 @@ namespace Server.Mobiles
}
}
[SerializableProperty(15, useField: nameof(_controlMaster))]
[SaveFlag(nameof(ShouldSerializeControlMaster))]
// Whoever this creature answers to. ControlMaster, SummonMaster and GetMaster() gate it by
// Controlled and Summoned; an enraged creature has a master with neither flag.
[SerializableProperty(15, useField: nameof(_master))]
[SaveFlag(nameof(ShouldSerializeMaster))]
[CommandProperty(AccessLevel.GameMaster)]
public Mobile ControlMaster
public Mobile Master
{
get => _controlMaster;
get => _master;
set
{
if (_controlMaster == value || this == value)
if (_master == value || this == value)
{
return;
}
RemoveFollowers();
_controlMaster = value;
_master = value;
AddFollowers();
if (_controlMaster != null)
if (value != null)
{
StopDeleteTimer();
}
@ -1184,26 +1184,20 @@ namespace Server.Mobiles
}
}
[SerializableProperty(23, useField: nameof(_summonMaster))]
[SaveFlag(nameof(ShouldSerializeSummonMaster))]
/// <summary>The owner. Setting it sets <see cref="Master"/>.</summary>
[CommandProperty(AccessLevel.GameMaster)]
public Mobile ControlMaster
{
get => _controlled ? _master : null;
set => Master = value;
}
/// <summary>The summoner, controlled or not. Setting it sets <see cref="Master"/>.</summary>
[CommandProperty(AccessLevel.GameMaster)]
public Mobile SummonMaster
{
get => _summonMaster;
set
{
if (_summonMaster == value || this == value)
{
return;
}
RemoveFollowers();
_summonMaster = value;
AddFollowers();
Delta(MobileDelta.Noto);
this.MarkDirty();
}
get => _summoned ? _master : null;
set => Master = value;
}
// Fires on every assignment, not only changes: a reissued order is a command (retarget, re-anchor).
@ -1272,7 +1266,7 @@ namespace Server.Mobiles
}
InvalidateProperties();
_controlMaster?.InvalidateProperties();
ControlMaster?.InvalidateProperties();
this.MarkDirty();
}
@ -2197,9 +2191,75 @@ namespace Server.Mobiles
}
}
// Absent fields resolve through the same default-value methods the live reader uses.
private void MigrateFrom(V23Content content)
{
_defaultAI = content.DefaultAI;
_currentAI = content.CurrentAI ?? CurrentAIDefaultValue();
_rangePerception = content.RangePerception ?? RangePerceptionDefaultValue();
_rangeFight = content.RangeFight ?? RangeFightDefaultValue();
_rangeHome = content.RangeHome ?? RangeHomeDefaultValue();
_team = content.Team ?? 0;
_fightMode = content.FightMode ?? FightModeDefaultValue();
_activeSpeed = content.ActiveSpeed ?? ActiveSpeedDefaultValue();
_passiveSpeed = content.PassiveSpeed ?? PassiveSpeedDefaultValue();
_currentSpeed = content.CurrentSpeed ?? CurrentSpeedDefaultValue();
_activeMoveSpeed = content.ActiveMoveSpeed ?? ActiveMoveSpeedDefaultValue();
_passiveMoveSpeed = content.PassiveMoveSpeed ?? PassiveMoveSpeedDefaultValue();
_home = content.Home ?? Point3D.Zero;
_homeMap = content.HomeMap;
_controlled = content.Controlled;
// A traded summon could still carry its caster as summon master; the owner wins.
_master = content.ControlMaster ?? content.SummonMaster;
_controlTarget = content.ControlTarget;
_controlDest = content.ControlDest ?? Point3D.Zero;
_controlOrder = content.ControlOrder ?? OrderType.None;
_minTameSkill = content.MinTameSkill ?? 0;
_tamable = content.Tamable;
_summoned = content.Summoned;
_summonEnd = content.SummonEnd ?? DateTime.MinValue;
_controlSlots = content.ControlSlots ?? ControlSlotsDefaultValue();
_loyalty = content.Loyalty ?? LoyaltyDefaultValue();
_currentWayPoint = content.CurrentWayPoint;
_hitsMaxSeed = content.HitsMaxSeed ?? HitsMaxSeedDefaultValue();
_stamMaxSeed = content.StamMaxSeed ?? StamMaxSeedDefaultValue();
_manaMaxSeed = content.ManaMaxSeed ?? ManaMaxSeedDefaultValue();
_damageMin = content.DamageMin ?? DamageMinDefaultValue();
_damageMax = content.DamageMax ?? DamageMaxDefaultValue();
_physicalResistanceSeed = content.PhysicalResistanceSeed ?? 0;
_fireResistSeed = content.FireResistSeed ?? 0;
_coldResistSeed = content.ColdResistSeed ?? 0;
_poisonResistSeed = content.PoisonResistSeed ?? 0;
_energyResistSeed = content.EnergyResistSeed ?? 0;
_physicalDamage = content.PhysicalDamage ?? PhysicalDamageDefaultValue();
_fireDamage = content.FireDamage ?? 0;
_coldDamage = content.ColdDamage ?? 0;
_poisonDamage = content.PoisonDamage ?? 0;
_energyDamage = content.EnergyDamage ?? 0;
_owners = content.Owners ?? OwnersDefaultValue();
_isDeadPet = content.IsDeadPet;
_isBonded = content.IsBonded;
_bondingBegin = content.BondingBegin ?? DateTime.MinValue;
_ownerAbandonTime = content.OwnerAbandonTime ?? DateTime.MinValue;
_hasGeneratedLoot = content.HasGeneratedLoot;
_isParagon = content.IsParagon;
_friends = content.Friends;
_removeIfUntamed = content.RemoveIfUntamed;
_removeStep = content.RemoveStep ?? 0;
if (content.PendingDeleteTimerDelay != TimeSpan.MinValue)
{
DeserializePendingDeleteTimer(content.PendingDeleteTimerDelay);
}
_corpseNameOverride = content.CorpseNameOverride;
}
// Pre-codegen loads only (versions 0-22); post-codegen bumps use MigrateFrom.
private void Deserialize(IGenericReader reader, int version)
{
Mobile controlMaster = null;
_currentAI = (AIType)reader.ReadInt();
_defaultAI = (AIType)reader.ReadInt();
@ -2247,7 +2307,7 @@ namespace Server.Mobiles
_fightMode = (FightMode)reader.ReadInt();
_controlled = reader.ReadBool();
_controlMaster = reader.ReadEntity<Mobile>();
controlMaster = reader.ReadEntity<Mobile>();
_controlTarget = reader.ReadEntity<Mobile>();
_controlDest = reader.ReadPoint3D();
_controlOrder = (OrderType)reader.ReadInt();
@ -2275,7 +2335,6 @@ namespace Server.Mobiles
_fightMode = FightMode.Closest;
_controlled = false;
_controlMaster = null;
_controlTarget = null;
_controlOrder = OrderType.None;
}
@ -2296,9 +2355,12 @@ namespace Server.Mobiles
if (version >= 5)
{
_summonMaster = reader.ReadEntity<Mobile>();
_master = reader.ReadEntity<Mobile>();
}
// A transferred summon kept its caster as summon master; the owner wins.
_master = controlMaster ?? _master;
if (version >= 6)
{
_hitsMaxSeed = reader.ReadInt();
@ -2459,7 +2521,7 @@ namespace Server.Mobiles
if (IsAnimatedDead)
{
AnimateDeadSpell.Register(_summonMaster, this);
AnimateDeadSpell.Register(SummonMaster, this);
}
}
@ -2565,7 +2627,7 @@ namespace Server.Mobiles
public void RemoveFollowers()
{
var master = _controlMaster ?? _summonMaster;
var master = _master;
if (master != null)
{
master.Followers -= Math.Min(ControlSlots, master.Followers);
@ -2579,7 +2641,7 @@ namespace Server.Mobiles
public void AddFollowers()
{
var master = _controlMaster ?? _summonMaster;
var master = _master;
if (master != null)
{
master.Followers += ControlSlots;
@ -2658,16 +2720,6 @@ namespace Server.Mobiles
FocusMob = null;
if (IsAnimatedDead)
{
AnimateDeadSpell.Unregister(_summonMaster, this);
}
if (Summoned && SummonMaster != null)
{
SummonFamiliarSpell.Unregister(SummonMaster, this);
}
if (MLQuestSystem.Enabled)
{
MLQuestSystem.HandleDeletion(this);
@ -2738,20 +2790,10 @@ namespace Server.Mobiles
aggressor.Aggressors.Add(AggressorInfo.Create(this, aggressor, true));
}
var ct = _controlOrder;
if (AIObject != null)
// The AI owns the retaliation policy; a creature that stands down keeps its flee and acquire state.
if (AIObject?.OnAggressiveAction(aggressor) != true)
{
if (!StandsDownOnCommand || !BaseAI.IsStandDownOrder(ct))
{
AIObject.OnAggressiveAction(aggressor);
}
else
{
AIObject.DebugSay("I'm being attacked but my master told me not to fight.");
Warmode = false;
return;
}
return;
}
StopFlee();
@ -2767,17 +2809,6 @@ namespace Server.Mobiles
pl.FinishShield();
}
}
// Only reachable when the pet does not stand down: the orders above returned early.
if (aggressor.ChangingCombatant && (_controlled || _summoned) && BaseAI.IsStandDownOrder(ct))
{
IssueOrder(OrderType.Attack, null, aggressor);
}
else if (Combatant == null && !BardPacified)
{
Warmode = true;
Combatant = aggressor;
}
}
public override bool OnMoveOver(Mobile m)
@ -2862,7 +2893,7 @@ namespace Server.Mobiles
}
public override bool IsHarmfulCriminal(Mobile target) =>
(!Controlled || target != _controlMaster) && (!Summoned || target != _summonMaster) &&
target != GetMaster() &&
(target is not BaseCreature { InitialInnocent: true } creature || creature.Controlled) &&
(target is not PlayerMobile mobile || mobile.PermaFlags.Count <= 0) && base.IsHarmfulCriminal(target);
@ -2870,16 +2901,9 @@ namespace Server.Mobiles
{
base.CriminalAction(message);
if (Controlled || Summoned)
if (GetMaster() is { Player: true } master)
{
if (_controlMaster?.Player == true)
{
_controlMaster.CriminalAction(false);
}
else if (_summonMaster?.Player == true)
{
_summonMaster.CriminalAction(false);
}
master.CriminalAction(false);
}
}
@ -2887,7 +2911,7 @@ namespace Server.Mobiles
{
base.DoHarmful(target, indirect);
if (target == this || target == _controlMaster || target == _summonMaster || !Controlled && !Summoned)
if (target == this || GetMaster() is not { } owner || target == owner)
{
return;
}
@ -3301,20 +3325,8 @@ namespace Server.Mobiles
return bonus;
}
public Mobile GetMaster()
{
if (Controlled && ControlMaster != null)
{
return ControlMaster;
}
if (Summoned && SummonMaster != null)
{
return SummonMaster;
}
return null;
}
// Who answers for this creature: its owner or its summoner. Never an enraged creature's meer.
public Mobile GetMaster() => ControlMaster ?? SummonMaster;
public virtual bool IsMonster => !Controlled || (GetMaster() as BaseCreature)?.IsMonster == true;
@ -3728,16 +3740,47 @@ namespace Server.Mobiles
{
CreatureEvents.CreatureDeletedEvent(this);
var m = _controlMaster;
// Both registries are keyed by the master, which is cleared below.
if (IsAnimatedDead)
{
AnimateDeadSpell.Unregister(SummonMaster, this);
}
if (SummonMaster != null)
{
SummonFamiliarSpell.Unregister(SummonMaster, this);
}
var m = ControlMaster;
SetControlMaster(null);
SummonMaster = null;
Master = null;
ReceivedHonorContext?.Cancel();
base.OnDelete();
m?.InvalidateProperties();
}
// A guard never fights its master or the master's other pets. An explicit Attack order and bard
// provocation still may.
public bool RefusesGuardTarget(Mobile target) =>
_controlled && _controlOrder == OrderType.Guard && !BardProvoked && AIObject?.IsGuardAlly(target) == true;
// Mobile.AggressiveAction assigns the first aggressor as Combatant before any retaliation policy runs.
public override Mobile Combatant
{
get => base.Combatant;
set
{
if (value != null && RefusesGuardTarget(value))
{
return;
}
base.Combatant = value;
}
}
public override bool CanBeHarmful(Mobile target, bool message, bool ignoreOurBlessedness)
{
if (target is BaseFactionGuard)
@ -3766,14 +3809,19 @@ namespace Server.Mobiles
}
public override bool CanBeRenamedBy(Mobile from) =>
Controlled && from == ControlMaster && !from.Region.IsPartOf<JailRegion>() ||
from == ControlMaster && !from.Region.IsPartOf<JailRegion>() ||
base.CanBeRenamedBy(from);
public bool SetControlMaster(Mobile m)
{
if (m == null)
{
ControlMaster = null;
// An uncontrolled summon keeps its caster.
if (_controlled)
{
Master = null;
}
Controlled = false;
ControlTarget = null;
ControlOrder = OrderType.None;
@ -3796,7 +3844,7 @@ namespace Server.Mobiles
Home = Point3D.Zero;
ControlMaster = m;
Master = m;
Controlled = true;
ControlTarget = null;
ControlOrder = OrderType.Come;
@ -3858,7 +3906,7 @@ namespace Server.Mobiles
creature.RangeHome = 10;
creature.Summoned = true;
creature.SummonMaster = caster;
creature.Master = caster;
var pack = creature.Backpack;
@ -4060,14 +4108,11 @@ namespace Server.Mobiles
return BardMaster;
}
if (_controlled && _controlMaster != null)
{
return _controlMaster;
}
var master = GetMaster();
if (_summoned && _summonMaster != null)
if (master != null)
{
return _summonMaster;
return master;
}
return base.GetDamageMaster(damagee);
@ -4413,16 +4458,27 @@ namespace Server.Mobiles
public virtual void AddPetFriend(Mobile m)
{
if (IsPetFriend(m))
{
return;
}
Friends ??= [];
Friends.Add(m);
this.MarkDirty();
AddToFriends(m);
}
public virtual void RemovePetFriend(Mobile m)
{
if (Friends?.Remove(m) == true)
if (Friends == null)
{
this.MarkDirty();
return;
}
RemoveFromFriends(m);
if (Friends.Count == 0)
{
Friends = null;
}
}
@ -4647,7 +4703,7 @@ namespace Server.Mobiles
if (IsBondable && !IsBonded)
{
var master = _controlMaster;
var master = ControlMaster;
if (master != null && master == from) // So friends can't start the bonding process
{

View file

@ -94,7 +94,7 @@ public abstract partial class BaseFamiliar : BaseCreature
{
base.GetContextMenuEntries(from, ref list);
if (from.Alive && Controlled && from == ControlMaster && from.InRange(this, 14))
if (from.Alive && from == ControlMaster && from.InRange(this, 14))
{
list.Add(new ReleaseEntry());
}

View file

@ -52,7 +52,7 @@ public partial class DarkWolfFamiliar : BaseFamiliar
m_NextRestore = Core.Now + TimeSpan.FromSeconds(2.0);
var caster = ControlMaster ?? SummonMaster;
var caster = GetMaster();
if (caster != null)
{

View file

@ -62,7 +62,7 @@ public partial class ShadowWispFamiliar : BaseFamiliar
private void Flare()
{
var caster = ControlMaster ?? SummonMaster;
var caster = GetMaster();
if (caster == null)
{

View file

@ -21,14 +21,9 @@ namespace Server.Mobiles
{
if (m is BaseCreature bc)
{
if (bc.Controlled && bc.ControlMaster is PlayerMobile)
if (bc.GetMaster() is PlayerMobile master)
{
return CheckRedFriendship(bc.ControlMaster);
}
if (bc.Summoned && bc.SummonMaster is PlayerMobile)
{
return CheckRedFriendship(bc.SummonMaster);
return CheckRedFriendship(master);
}
}
@ -39,14 +34,9 @@ namespace Server.Mobiles
{
if (m is BaseCreature bc)
{
if (bc.Controlled && bc.ControlMaster is PlayerMobile)
if (bc.GetMaster() is PlayerMobile master)
{
return CheckBlackFriendship(bc.ControlMaster);
}
if (bc.Summoned && bc.SummonMaster is PlayerMobile)
{
return CheckBlackFriendship(bc.SummonMaster);
return CheckBlackFriendship(master);
}
}
@ -57,13 +47,9 @@ namespace Server.Mobiles
{
if (from is BaseCreature bc)
{
if (bc.Controlled && bc.ControlMaster is PlayerMobile)
if (bc.GetMaster() is PlayerMobile master)
{
OnRedDamage(bc.ControlMaster);
}
else if (bc.Summoned && bc.SummonMaster is PlayerMobile)
{
OnRedDamage(bc.SummonMaster);
OnRedDamage(master);
}
}
@ -83,13 +69,9 @@ namespace Server.Mobiles
{
if (from is BaseCreature bc)
{
if (bc.Controlled && bc.ControlMaster is PlayerMobile)
if (bc.GetMaster() is PlayerMobile master)
{
OnBlackDamage(bc.ControlMaster);
}
else if (bc.Summoned && bc.SummonMaster is PlayerMobile)
{
OnBlackDamage(bc.SummonMaster);
OnBlackDamage(master);
}
}

View file

@ -126,6 +126,10 @@ namespace Server.Mobiles
SpellHelper.Turn(this, toBuff);
var maxHits = toBuff.HitsMaxSeed;
var rawStr = toBuff.RawStr;
var rawDex = toBuff.RawDex;
var toScale = toBuff.HitsMaxSeed;
if (toScale > 0)
@ -154,9 +158,6 @@ namespace Server.Mobiles
toBuff.FixedParticles(0x375A, 10, 15, 5017, EffectLayer.Waist);
toBuff.PlaySound(0x1EE);
var maxHits = toBuff.HitsMaxSeed;
var rawStr = toBuff.RawStr;
var rawDex = toBuff.RawDex;
Timer.StartTimer(
TimeSpan.FromSeconds(20.0),

View file

@ -92,12 +92,15 @@ namespace Server.Mobiles
Karma = -1000;
Tamable = false;
SummonMaster = summoner;
Master = summoner;
}
// The meer's FightMode.Evil against our negative karma would otherwise make us enemies.
public override bool IsEnemy(Mobile m) => m != Master && base.IsEnemy(m);
public override void OnThink()
{
if (SummonMaster?.Deleted != false)
if (Master?.Deleted != false)
{
Delete();
}
@ -108,7 +111,7 @@ namespace Server.Mobiles
*/
else if (!Combat(this))
{
AIObject?.MoveTo(SummonMaster, 5);
AIObject?.MoveTo(Master, 5);
}
/*
On OSI, if the summon attacks a mobile, the summoner meer also
@ -116,11 +119,11 @@ namespace Server.Mobiles
is a player or controlled/summoned, and the summoner is not already
engaged in combat.
*/
else if (!Combat(SummonMaster))
else if (!Combat(Master))
{
if (Combatant.Player || Combatant is BaseCreature bc && (bc.Controlled || bc.SummonMaster != null))
if (Combatant.Player || Combatant is BaseCreature bc && bc.GetMaster() != null)
{
SummonMaster.Combatant = Combatant;
Master.Combatant = Combatant;
}
}
else

View file

@ -59,6 +59,9 @@ namespace Server.Mobiles
public override bool InitialInnocent => true;
// FightMode.Evil would otherwise target our own enraged creatures for their negative karma.
public override bool IsEnemy(Mobile m) => (m as BaseEnraged)?.Master != this && base.IsEnemy(m);
public override void GenerateLoot()
{
AddLoot(LootPack.FilthyRich);
@ -85,7 +88,7 @@ namespace Server.Mobiles
if (combatant is BaseCreature bc)
{
if (bc.Controlled && bc.ControlMaster?.Deleted == false && bc.ControlMaster.Alive)
if (bc.ControlMaster is { Deleted: false, Alive: true })
{
if (bc.ControlMaster.Map == Map && bc.ControlMaster.InRange(this, 12) &&
!UnderEffect(bc.ControlMaster))

View file

@ -55,7 +55,8 @@ namespace Server.Mobiles
public override bool BleedImmune => true;
public override Poison PoisonImmune => Poison.Lethal;
public override bool FollowsAcquireRules => Core.AOS || !Summoned || SummonMaster?.Player != true || Map != Map.Felucca;
public override bool FollowsAcquireRules => Core.AOS || SummonMaster?.Player != true ||
(Map?.Rules & MapRules.HarmfulRestrictions) != 0;
public override double GetFightModeRanking(Mobile m, FightMode acqType, bool bPlayerOnly) =>
(m.Str + m.Skills.Tactics.Value) / Math.Max(this.GetDistanceToSqrt(m), 1.0);

View file

@ -63,7 +63,8 @@ namespace Server.Mobiles
public override bool BleedImmune => true;
public override Poison PoisonImmune => Poison.Lethal;
public override bool FollowsAcquireRules => Core.AOS || !Summoned || SummonMaster?.Player != true || Map != Map.Felucca;
public override bool FollowsAcquireRules => Core.AOS || SummonMaster?.Player != true ||
(Map?.Rules & MapRules.HarmfulRestrictions) != 0;
public override double GetFightModeRanking(Mobile m, FightMode acqType, bool bPlayerOnly) =>
(m.Int + m.Skills.Magery.Value) / Math.Max(this.GetDistanceToSqrt(m), 1.0);

View file

@ -142,22 +142,17 @@ namespace Server.Mobiles
public override void OnDamage(int amount, Mobile from, bool willKill)
{
if (Controlled || Summoned)
if (GetMaster() is { Player: true } master && master.Map == Map && master.InRange(Location, 20))
{
var master = ControlMaster ?? SummonMaster;
if (master?.Player == true && master.Map == Map && master.InRange(Location, 20))
if (master.Mana >= amount)
{
if (master.Mana >= amount)
{
master.Mana -= amount;
}
else
{
amount -= master.Mana;
master.Mana = 0;
master.Damage(amount);
}
master.Mana -= amount;
}
else
{
amount -= master.Mana;
master.Mana = 0;
master.Damage(amount);
}
}

View file

@ -50,7 +50,6 @@ namespace Server.Mobiles
CanSwim = true;
CantWalk = true;
PackItem(new MessageInABottle());
PackItem(new Rope { ItemID = 0x14F8 });
PackItem(new Rope { ItemID = 0x14FA });
}
@ -63,6 +62,13 @@ namespace Server.Mobiles
public override int TreasureMapLevel => 5;
public override bool OnBeforeDeath()
{
PackItem(new MessageInABottle(Map == Map.Felucca ? Map.Felucca : Map.Trammel));
return base.OnBeforeDeath();
}
public static Type[] Artifacts { get; } =
{
// Decorations

View file

@ -46,12 +46,10 @@ namespace Server.Mobiles
rope.ItemID = 0x14F8;
PackItem(rope);
if (Utility.RandomDouble() < .05)
if (Core.UOR)
{
PackItem(new MessageInABottle());
PackItem(new SpecialFishingNet()); // Confirm?
}
PackItem(new SpecialFishingNet()); // Confirm?
}
public override string CorpseName => "a krakens corpse";
@ -59,6 +57,16 @@ namespace Server.Mobiles
public override int TreasureMapLevel => 4;
public override bool OnBeforeDeath()
{
if (Core.T2A && Utility.RandomDouble() < .05)
{
PackItem(new MessageInABottle(Map == Map.Felucca ? Map.Felucca : Map.Trammel));
}
return base.OnBeforeDeath();
}
public override void GenerateLoot()
{
AddLoot(LootPack.Rich);

View file

@ -59,7 +59,7 @@ namespace Server.Mobiles
public override void OnHarmfulSpell(Mobile from)
{
if (!Controlled && ControlMaster == null)
if (!Controlled)
{
CurrentSpeed = BoostedSpeed;
}
@ -67,7 +67,7 @@ namespace Server.Mobiles
public override void OnCombatantChange()
{
if (Combatant == null && !Controlled && ControlMaster == null)
if (Combatant == null && !Controlled)
{
CurrentSpeed = PassiveSpeed;
}

View file

@ -3612,7 +3612,7 @@ namespace Server.Mobiles
foreach (var follower in allFollowers)
{
if (follower is not BaseCreature pet || pet.ControlMaster == null)
if (follower is not BaseCreature { Controlled: true } pet || pet.ControlMaster == null)
{
continue;
}
@ -3648,7 +3648,6 @@ namespace Server.Mobiles
pet.Internalize();
pet.SetControlMaster(null);
pet.SummonMaster = null;
pet.IsStabled = true;
pet.StabledBy = this;
@ -3697,11 +3696,6 @@ namespace Server.Mobiles
{
pet.SetControlMaster(this);
if (pet.SummonMaster != null)
{
pet.SummonMaster = this;
}
pet.ControlTarget = this;
pet.ControlOrder = OrderType.Follow;
@ -3953,6 +3947,35 @@ namespace Server.Mobiles
Stabled = null;
}
if (_allFollowers?.Count > 0)
{
// Releasing or deleting a follower removes it from _allFollowers.
using var followers = PooledRefQueue<BaseCreature>.Create(_allFollowers.Count);
foreach (var follower in _allFollowers)
{
// Escorts and hirelings notice a deleted master and walk off on their own.
if (follower is BaseCreature bc and not (BaseEscortable or BaseHire))
{
followers.Enqueue(bc);
}
}
while (followers.Count > 0)
{
var bc = followers.Dequeue();
if (bc.Summoned || bc.IsBonded || bc.IsDeadPet)
{
bc.Delete();
}
else
{
// An unbonded pet goes wild and despawns on the abandoned-pet timer.
bc.ControlOrder = OrderType.Release;
}
}
}
}
public override int ComputeMovementSpeed(Direction dir, bool checkTurning = true)

View file

@ -31,7 +31,10 @@ namespace Server.Mobiles
private static readonly ILogger logger = LogFactory.GetLogger(typeof(BaseVendor));
private const int MaxSell = 500;
private static readonly TimeSpan InventoryDecayTime = TimeSpan.FromHours(1.0);
// One (not all) of the buy packets counts entries in a byte.
private const int BuyListLimit = 250;
private TimerExecutionToken _buybackPurgeToken;
private readonly List<IBuyItemInfo> _buyInfo = new();
private readonly List<IShopSellInfo> _sellInfo = new();
@ -71,11 +74,8 @@ namespace Server.Mobiles
SetSpeed(0.5, 2.0);
// these packs MUST exist, or the client will crash when the packets are sent
Container pack = new Backpack { Layer = Layer.ShopBuy, Movable = false, Visible = false };
AddItem(pack);
pack = new Backpack { Layer = Layer.ShopResale, Movable = false, Visible = false };
AddItem(pack);
AddItem(new VendorBuybackPack());
AddItem(new Backpack { Layer = Layer.ShopResale, Movable = false, Visible = false });
LastRestock = Core.Now;
}
@ -105,20 +105,79 @@ namespace Server.Mobiles
public override bool ShowFameTitle => false;
public Container BuyPack
public VendorBuybackPack BuyPack
{
get
{
if (FindItemOnLayer(Layer.ShopBuy) is not Container pack)
var existing = FindItemOnLayer(Layer.ShopBuy);
if (existing is VendorBuybackPack pack)
{
pack = new Backpack { Layer = Layer.ShopBuy, Visible = false };
AddItem(pack);
return pack;
}
// Legacy pack; deleting it stays linear even with a huge backlog.
existing?.Delete();
pack = new VendorBuybackPack();
AddItem(pack);
return pack;
}
}
internal bool BuybackPurgeScheduled => _buybackPurgeToken.Running;
internal static DateTime GetNextBuybackPurge(DateTime lastRestock, TimeSpan restockDelay, DateTime now)
{
if (restockDelay <= TimeSpan.Zero)
{
return now;
}
// A backwards clock adjustment can future-date the anchor past RestockDelay.
if (lastRestock > now)
{
lastRestock = now;
}
var next = lastRestock + restockDelay;
if (next > now)
{
return next;
}
// Restock is lazy and may be hours overdue; stay on its grid.
var periods = (now - lastRestock).Ticks / restockDelay.Ticks + 1;
return lastRestock + TimeSpan.FromTicks(restockDelay.Ticks * periods);
}
private void AddToBuyback(Item item, int capacity)
{
BuyPack.AddBuyback(item, capacity);
if (!item.Deleted && !_buybackPurgeToken.Running)
{
var delay = GetNextBuybackPurge(LastRestock, RestockDelay, Core.Now) - Core.Now;
Timer.StartTimer(delay, CancelAndPurgeBuyback, out _buybackPurgeToken);
}
}
private void PurgeBuyback() => (FindItemOnLayer(Layer.ShopBuy) as VendorBuybackPack)?.Purge();
// A fired token-bearing timer isn't returned to its pool until its token is cancelled.
private void CancelAndPurgeBuyback()
{
_buybackPurgeToken.Cancel();
PurgeBuyback();
}
public override void OnDelete()
{
_buybackPurgeToken.Cancel();
base.OnDelete();
}
public virtual bool IsTokunoVendor => Map == Map.Tokuno;
public virtual bool IsTerMurVendor => Map == Map.TerMur;
@ -132,6 +191,7 @@ namespace Server.Mobiles
public virtual void Restock()
{
LastRestock = Core.Now;
CancelAndPurgeBuyback();
var buyInfo = GetBuyInfo();
@ -427,6 +487,7 @@ namespace Server.Mobiles
var info = GetSellInfo();
var buyInfo = GetBuyInfo();
var buybackCapacity = Math.Max(0, BuyListLimit - buyInfo.Length);
var GiveGold = 0;
var Sold = 0;
@ -498,28 +559,14 @@ namespace Server.Mobiles
if (!found)
{
var cont = BuyPack;
var sold = resp.Item;
if (amount < resp.Item.Amount)
{
var item = LiftItemDupe(resp.Item, resp.Item.Amount - amount);
sold = LiftItemDupe(resp.Item, resp.Item.Amount - amount) ?? resp.Item;
}
if (item != null)
{
item.SetLastMoved();
cont.DropItem(item);
}
else
{
resp.Item.SetLastMoved();
cont.DropItem(resp.Item);
}
}
else
{
resp.Item.SetLastMoved();
cont.DropItem(resp.Item);
}
AddToBuyback(sold, buybackCapacity);
}
}
else
@ -738,6 +785,9 @@ namespace Server.Mobiles
CheckMorph();
LoadSBInfo();
// LoadSBInfo re-anchored the restock grid. Not done there since it also runs on deserialize.
CancelAndPurgeBuyback();
}
public virtual int GetRandomNecromancerHue()
@ -882,7 +932,7 @@ namespace Server.Mobiles
{
var buyItem = buyInfo[idx];
if (buyItem.Amount <= 0 || list.Count >= 250)
if (buyItem.Amount <= 0 || list.Count >= BuyListLimit)
{
continue;
}
@ -914,21 +964,6 @@ namespace Server.Mobiles
var playerItems = cont.Items;
for (var i = playerItems.Count - 1; i >= 0; --i)
{
if (i >= playerItems.Count)
{
continue;
}
var item = playerItems[i];
if (item.LastMoved + InventoryDecayTime <= Core.Now)
{
item.Delete();
}
}
for (var i = 0; i < playerItems.Count; ++i)
{
var item = playerItems[i];
@ -946,17 +981,13 @@ namespace Server.Mobiles
}
}
if (name != null && list.Count < 250)
if (name != null && list.Count < BuyListLimit)
{
list.Add(new BuyItemState(name, cont.Serial, item.Serial, price, item.Amount, item.ItemID, item.Hue));
opls.Enqueue(item.PropertyList);
}
}
// one (not all) of the packets uses a byte to describe number of items in the list. Osi = dumb.
// if (list.Count > 255)
// Console.WriteLine( "Vendor Warning: Vendor {0} has more than 255 buy items, may cause client errors!", this );
if (list.Count <= 0)
{
return;
@ -988,17 +1019,9 @@ namespace Server.Mobiles
public virtual void SendPacksTo(Mobile from)
{
var pack = FindItemOnLayer(Layer.ShopBuy);
from.NetState.SendEquipUpdate(BuyPack);
if (pack == null)
{
pack = new Backpack { Layer = Layer.ShopBuy, Movable = false, Visible = false };
AddItem(pack);
}
from.NetState.SendEquipUpdate(pack);
pack = FindItemOnLayer(Layer.ShopSell);
var pack = FindItemOnLayer(Layer.ShopSell);
if (pack != null)
{
@ -1306,6 +1329,20 @@ namespace Server.Mobiles
{
LoadSBInfo();
if (FindItemOnLayer(Layer.ShopBuy) is not VendorBuybackPack)
{
// Deleting during deserialization is unsafe; replace on the first tick.
Timer.StartTimer(
() =>
{
if (!Deleted)
{
_ = BuyPack;
}
}
);
}
if (IsParagon)
{
IsParagon = false;

View file

@ -256,7 +256,6 @@ namespace Server.Mobiles
pet.Internalize();
pet.SetControlMaster(null);
pet.SummonMaster = null;
pet.IsStabled = true;
pet.StabledBy = from;
@ -358,11 +357,6 @@ namespace Server.Mobiles
{
pet.SetControlMaster(from);
if (pet.SummonMaster != null)
{
pet.SummonMaster = from;
}
pet.ControlTarget = from;
pet.ControlOrder = OrderType.Follow;

View file

@ -0,0 +1,56 @@
using ModernUO.Serialization;
namespace Server.Items;
/// <summary>
/// Holds items players sold to an NPC vendor until the next restock. The contents are never saved
/// and their removal reaches only the vendor and openers, like any private container.
/// </summary>
[SerializationGenerator(0)]
public partial class VendorBuybackPack : Backpack
{
[Constructible]
public VendorBuybackPack()
{
Layer = Layer.ShopBuy;
Movable = false;
Visible = false;
}
public override bool SkipsChildSerialization => true;
// DropItem appends, so Items[0] is always the oldest entry.
public void AddBuyback(Item item, int capacity)
{
if (capacity <= 0)
{
item.Delete();
return;
}
while (Items.Count >= capacity)
{
var oldest = Items[0];
oldest.Delete();
// Delete() is a no-op on an already-deleted item, which would spin this loop.
if (Items.Count > 0 && Items[0] == oldest)
{
Items.RemoveAt(0);
}
}
DropItem(item);
}
public void Purge()
{
for (var i = Items.Count - 1; i >= 0; --i)
{
if (i < Items.Count)
{
Items[i].Delete();
}
}
}
}