#W# Confict: Fixed BaseCreature git merge conflict.
This commit is contained in:
commit
ad38314ca7
112 changed files with 5397 additions and 616 deletions
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) =>
|
||||
|
|
|
|||
|
|
@ -451,11 +451,6 @@ public abstract partial class BaseAI
|
|||
|
||||
Mobile.SetControlMaster(e.Mobile);
|
||||
|
||||
if (Mobile.SummonMaster != null)
|
||||
{
|
||||
Mobile.SummonMaster = e.Mobile;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ public partial class ShadowWispFamiliar : BaseFamiliar
|
|||
|
||||
private void Flare()
|
||||
{
|
||||
var caster = ControlMaster ?? SummonMaster;
|
||||
var caster = GetMaster();
|
||||
|
||||
if (caster == null)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
56
Projects/UOContent/Mobiles/Vendors/VendorBuybackPack.cs
Normal file
56
Projects/UOContent/Mobiles/Vendors/VendorBuybackPack.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue