Readers of `ControlMaster` that act on "this creature is a player's pet" now also require `Controlled`. Nothing sets a master on an uncontrolled creature today, so behavior does not change, except that the ML notoriety branch was the one read with nothing in front of it. The guard keeps these readers correct if a creature ever keeps a master without being controlled (a suspended-control state such as a charm or turn-pet ability, or folding SummonMaster into one master field). - Notoriety (ML): a creature only takes its master's notoriety while controlled - BaseCreature.AggressiveAction: the aggressor link to the master only applies to controlled pets - PlayerMobile.AutoStablePets: uncontrolled summons (blade spirits, energy vortexes) are skipped - CrystalCaveBarrier: only a controlled pet passes on its owner's quest progress - Discordance: the own-pet exception only applies to a controlled pet - Dismount (ML): the remount block only goes to the owner of a controlled pet Also removes the "Summons from monsters can attack players" branch in Mobile_AllowHarmful. It has been unreachable since #2000: a creature with no player master returns from the NPC check above it. Tests: UOContent.Tests 1158 passed.
577 lines
19 KiB
C#
577 lines
19 KiB
C#
using System.Collections.Generic;
|
|
using Server.Engines.ConPVP;
|
|
using Server.Engines.PartySystem;
|
|
using Server.Factions;
|
|
using Server.Guilds;
|
|
using Server.Items;
|
|
using Server.Mobiles;
|
|
using Server.Multis;
|
|
using Server.SkillHandlers;
|
|
using Server.Spells.Seventh;
|
|
|
|
namespace Server.Misc
|
|
{
|
|
public static class NotorietyHandlers
|
|
{
|
|
public static void Initialize()
|
|
{
|
|
Notoriety.Hues[Notoriety.Innocent] = 0x59;
|
|
Notoriety.Hues[Notoriety.Ally] = 0x3F;
|
|
Notoriety.Hues[Notoriety.CanBeAttacked] = 0x3B2;
|
|
Notoriety.Hues[Notoriety.Criminal] = 0x3B2;
|
|
Notoriety.Hues[Notoriety.Enemy] = 0x90;
|
|
Notoriety.Hues[Notoriety.Murderer] = 0x22;
|
|
Notoriety.Hues[Notoriety.Invulnerable] = 0x35;
|
|
|
|
Notoriety.Handler = MobileNotoriety;
|
|
|
|
Mobile.AllowBeneficialHandler = Mobile_AllowBeneficial;
|
|
Mobile.AllowHarmfulHandler = Mobile_AllowHarmful;
|
|
}
|
|
|
|
private static GuildStatus GetGuildStatus(Mobile m)
|
|
{
|
|
if (m.Guild == null)
|
|
{
|
|
return GuildStatus.None;
|
|
}
|
|
|
|
if (((Guild)m.Guild).Enemies.Count == 0 && m.Guild.Type == GuildType.Regular)
|
|
{
|
|
return GuildStatus.Peaceful;
|
|
}
|
|
|
|
return GuildStatus.Waring;
|
|
}
|
|
|
|
private static bool CheckBeneficialStatus(GuildStatus from, GuildStatus target) =>
|
|
from != GuildStatus.Waring && target != GuildStatus.Waring;
|
|
|
|
/*private static bool CheckHarmfulStatus( GuildStatus from, GuildStatus target )
|
|
{
|
|
if (from == GuildStatus.Waring && target == GuildStatus.Waring)
|
|
return true;
|
|
|
|
return false;
|
|
}*/
|
|
|
|
public static bool Mobile_AllowBeneficial(Mobile from, Mobile target)
|
|
{
|
|
if (from == null || target == null || from.AccessLevel > AccessLevel.Player ||
|
|
target.AccessLevel > AccessLevel.Player)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (from.Region.IsPartOf<SafeZone>() || target.Region.IsPartOf<SafeZone>())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var bcFrom = from as BaseCreature;
|
|
var bcTarg = target as BaseCreature;
|
|
|
|
var pmFrom = (bcFrom?.GetMaster() ?? from) as PlayerMobile;
|
|
var pmTarg = (bcTarg?.GetMaster() ?? target) as PlayerMobile;
|
|
|
|
if (pmFrom != null && pmTarg != null)
|
|
{
|
|
if (pmFrom.DuelContext != pmTarg.DuelContext &&
|
|
(pmFrom.DuelContext?.Started == true || pmTarg.DuelContext?.Started == true))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (pmFrom.DuelContext != null && pmFrom.DuelContext == pmTarg.DuelContext &&
|
|
(pmFrom.DuelContext.StartedReadyCountdown && !pmFrom.DuelContext.Started || pmFrom.DuelContext.Tied ||
|
|
pmFrom.DuelPlayer.Eliminated || pmTarg.DuelPlayer.Eliminated))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (pmFrom.DuelPlayer?.Eliminated == false && pmFrom.DuelContext?.IsSuddenDeath == true)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (pmFrom.DuelContext != null && pmFrom.DuelContext == pmTarg.DuelContext &&
|
|
pmFrom.DuelContext.m_Tournament?.IsNotoRestricted == true &&
|
|
pmFrom.DuelPlayer != null && pmTarg.DuelPlayer != null &&
|
|
pmFrom.DuelPlayer.Participant != pmTarg.DuelPlayer.Participant)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (pmFrom.DuelContext?.Started == true && pmFrom.DuelContext == pmTarg.DuelContext)
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
if (pmFrom?.DuelContext?.Started == true || pmTarg?.DuelContext?.Started == true)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var map = from.Map;
|
|
|
|
var targetFaction = Faction.Find(target, true);
|
|
|
|
if ((!Core.ML || map == Faction.Facet) && targetFaction != null && Faction.Find(from, true) != targetFaction)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if ((map?.Rules & MapRules.BeneficialRestrictions) == 0)
|
|
{
|
|
return true; // In felucca, anything goes
|
|
}
|
|
|
|
if (!from.Player && pmFrom?.AccessLevel != AccessLevel.Player)
|
|
{
|
|
return true; // NPCs have no restrictions
|
|
}
|
|
|
|
if (bcTarg?.Controlled == false)
|
|
{
|
|
return false; // Players cannot heal uncontrolled mobiles
|
|
}
|
|
|
|
if (pmFrom?.Young == true && pmTarg?.Young == false)
|
|
{
|
|
return false; // Young players cannot perform beneficial actions towards non-young players or pets
|
|
}
|
|
|
|
if (from.Guild is Guild fromGuild && target.Guild is Guild targetGuild &&
|
|
(targetGuild == fromGuild || fromGuild.IsAlly(targetGuild)))
|
|
{
|
|
return true; // Guild members can be beneficial
|
|
}
|
|
|
|
return CheckBeneficialStatus(GetGuildStatus(from), GetGuildStatus(target));
|
|
}
|
|
|
|
public static bool Mobile_AllowHarmful(Mobile from, Mobile target)
|
|
{
|
|
if (from == null || target == null || from.AccessLevel > AccessLevel.Player ||
|
|
target.AccessLevel > AccessLevel.Player)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (from.Region.IsPartOf<SafeZone>() || target.Region.IsPartOf<SafeZone>())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var bcFrom = from as BaseCreature;
|
|
var bcTarg = target as BaseCreature;
|
|
|
|
var pmFrom = (bcFrom?.GetMaster() ?? from) as PlayerMobile;
|
|
var pmTarg = (bcTarg?.GetMaster() ?? target) as PlayerMobile;
|
|
|
|
if (pmFrom != null && pmTarg != null)
|
|
{
|
|
if (pmFrom.DuelContext != pmTarg.DuelContext &&
|
|
(pmFrom.DuelContext?.Started == true || pmTarg.DuelContext?.Started == true))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (pmFrom.DuelContext != null && pmFrom.DuelContext == pmTarg.DuelContext &&
|
|
(pmFrom.DuelContext.StartedReadyCountdown && !pmFrom.DuelContext.Started || pmFrom.DuelContext.Tied ||
|
|
pmFrom.DuelPlayer.Eliminated || pmTarg.DuelPlayer.Eliminated))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (pmFrom.DuelContext != null && pmFrom.DuelContext == pmTarg.DuelContext &&
|
|
pmFrom.DuelContext.m_Tournament?.IsNotoRestricted == true &&
|
|
pmFrom.DuelPlayer != null && pmTarg.DuelPlayer != null &&
|
|
pmFrom.DuelPlayer.Participant == pmTarg.DuelPlayer.Participant)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (pmFrom.DuelContext?.Started == true && pmFrom.DuelContext == pmTarg.DuelContext)
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
if (pmFrom?.DuelContext?.Started == true || pmTarg?.DuelContext?.Started == true)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var map = from.Map;
|
|
|
|
if ((map?.Rules & MapRules.HarmfulRestrictions) == 0)
|
|
{
|
|
return true; // In felucca, anything goes
|
|
}
|
|
|
|
if (!from.Player && pmFrom?.AccessLevel != AccessLevel.Player)
|
|
{
|
|
// Uncontrolled NPCs are only restricted by the young system
|
|
return CheckAggressor(from.Aggressors, target) || CheckAggressed(from.Aggressed, target) ||
|
|
(target as PlayerMobile)?.CheckYoungProtection(from) != true;
|
|
}
|
|
|
|
var fromGuild = GetGuildFor(from.Guild as Guild, from);
|
|
var targetGuild = GetGuildFor(target.Guild as Guild, target);
|
|
|
|
if (fromGuild != null && targetGuild != null &&
|
|
(fromGuild == targetGuild || fromGuild.IsAlly(targetGuild) || fromGuild.IsEnemy(targetGuild)))
|
|
{
|
|
return true; // Guild allies or enemies can be harmful
|
|
}
|
|
|
|
if (bcTarg?.Controlled == true
|
|
|| bcTarg?.Summoned == true && bcTarg.SummonMaster != from && bcTarg.SummonMaster?.Player == true)
|
|
{
|
|
return false; // Cannot harm other controlled mobiles from players
|
|
}
|
|
|
|
if (target.Player)
|
|
{
|
|
return false; // Cannot harm other players
|
|
}
|
|
|
|
return bcTarg?.InitialInnocent == true || Notoriety.Compute(from, target) != Notoriety.Innocent;
|
|
}
|
|
|
|
public static Guild GetGuildFor(Guild def, Mobile m)
|
|
{
|
|
if (m is not BaseCreature c || !c.Controlled || c.ControlMaster == null)
|
|
{
|
|
return def;
|
|
}
|
|
|
|
c.DisplayGuildTitle = false;
|
|
|
|
if (c.Map != Map.Internal && (Core.AOS || Guild.NewGuildSystem || c.ControlOrder is OrderType.Attack or OrderType.Guard))
|
|
{
|
|
return (Guild)(c.Guild = c.ControlMaster.Guild);
|
|
}
|
|
|
|
if (c.Map == Map.Internal || c.ControlMaster.Guild == null)
|
|
{
|
|
return (Guild)(c.Guild = null);
|
|
}
|
|
|
|
return def;
|
|
}
|
|
|
|
public static int CorpseNotoriety(Mobile source, Corpse target)
|
|
{
|
|
if (target.AccessLevel > AccessLevel.Player)
|
|
{
|
|
return Notoriety.CanBeAttacked;
|
|
}
|
|
|
|
Body body = target.Amount;
|
|
|
|
var sourceGuild = GetGuildFor(source.Guild as Guild, source);
|
|
var targetGuild = GetGuildFor(target.Guild, target.Owner);
|
|
|
|
var srcFaction = Faction.Find(source, true, true);
|
|
var trgFaction = Faction.Find(target.Owner, true, true);
|
|
var list = target.Aggressors;
|
|
|
|
if (sourceGuild != null && targetGuild != null)
|
|
{
|
|
if (sourceGuild == targetGuild || sourceGuild.IsAlly(targetGuild))
|
|
{
|
|
return Notoriety.Ally;
|
|
}
|
|
|
|
if (sourceGuild.IsEnemy(targetGuild))
|
|
{
|
|
return Notoriety.Enemy;
|
|
}
|
|
}
|
|
|
|
// BaseCreatures are deleted on death, so target.Owner is null after a server restart.
|
|
// The OwnerWasBaseCreature flag is the persisted snapshot that survives the live mobile.
|
|
var creature = target.Owner as BaseCreature;
|
|
|
|
if (target.OwnerWasBaseCreature || creature != null)
|
|
{
|
|
if (srcFaction != null && trgFaction != null && srcFaction != trgFaction && source.Map == Faction.Facet)
|
|
{
|
|
return Notoriety.Enemy;
|
|
}
|
|
|
|
if (creature != null && CheckHouseFlag(source, creature, target.Location, target.Map))
|
|
{
|
|
return Notoriety.CanBeAttacked;
|
|
}
|
|
|
|
var actual = Notoriety.CanBeAttacked;
|
|
|
|
if (target.Murderer || body.IsMonster && target.OwnerWasSummoned || target.OwnerWasAnimatedDead)
|
|
{
|
|
actual = Notoriety.Murderer;
|
|
}
|
|
|
|
if (Core.Now >= target.TimeOfDeath + Corpse.MonsterLootRightSacrifice)
|
|
{
|
|
return actual;
|
|
}
|
|
|
|
var sourceParty = Party.Get(source);
|
|
|
|
for (var i = 0; i < list.Count; ++i)
|
|
{
|
|
if (list[i] == source || sourceParty != null && Party.Get(list[i]) == sourceParty)
|
|
{
|
|
return actual;
|
|
}
|
|
}
|
|
|
|
return Notoriety.Innocent;
|
|
}
|
|
|
|
if (target.Murderer || body.IsMonster)
|
|
{
|
|
return Notoriety.Murderer;
|
|
}
|
|
|
|
if (target.Criminal && (target.Map?.Rules & MapRules.HarmfulRestrictions) == 0)
|
|
{
|
|
return Notoriety.Criminal;
|
|
}
|
|
|
|
if (srcFaction != null && trgFaction != null && srcFaction != trgFaction && source.Map == Faction.Facet)
|
|
{
|
|
for (var i = 0; i < list.Count; ++i)
|
|
{
|
|
if (list[i] == source || list[i] is BaseFactionGuard)
|
|
{
|
|
return Notoriety.Enemy;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (CheckHouseFlag(source, target.Owner, target.Location, target.Map))
|
|
{
|
|
return Notoriety.CanBeAttacked;
|
|
}
|
|
|
|
if (target.Owner is not PlayerMobile)
|
|
{
|
|
return Notoriety.CanBeAttacked;
|
|
}
|
|
|
|
for (var i = 0; i < list.Count; ++i)
|
|
{
|
|
if (list[i] == source)
|
|
{
|
|
return Notoriety.CanBeAttacked;
|
|
}
|
|
}
|
|
|
|
return Notoriety.Innocent;
|
|
}
|
|
|
|
/* Must be thread-safe */
|
|
public static int MobileNotoriety(Mobile source, Mobile target)
|
|
{
|
|
var bcTarg = target as BaseCreature;
|
|
|
|
if (Core.AOS && (target.Blessed || bcTarg?.IsInvulnerable == true || target is PlayerVendor or TownCrier))
|
|
{
|
|
return Notoriety.Invulnerable;
|
|
}
|
|
|
|
// Moved above AccessLevel check so staff summons are red
|
|
if (bcTarg?.AlwaysMurderer == true)
|
|
{
|
|
return Notoriety.Murderer;
|
|
}
|
|
|
|
var pmFrom = source as PlayerMobile;
|
|
var pmTarg = target as PlayerMobile;
|
|
|
|
if (pmFrom != null && pmTarg != null)
|
|
{
|
|
if (pmFrom.DuelContext?.StartedBeginCountdown == true && !pmFrom.DuelContext.Finished &&
|
|
pmFrom.DuelContext == pmTarg.DuelContext)
|
|
{
|
|
return pmFrom.DuelContext.IsAlly(pmFrom, pmTarg) ? Notoriety.Ally : Notoriety.Enemy;
|
|
}
|
|
}
|
|
|
|
if (target.AccessLevel > AccessLevel.Player)
|
|
{
|
|
return Notoriety.CanBeAttacked;
|
|
}
|
|
|
|
if (source.Player && !target.Player && pmFrom != null && bcTarg != null)
|
|
{
|
|
var master = bcTarg.GetMaster();
|
|
|
|
if (master?.AccessLevel > AccessLevel.Player)
|
|
{
|
|
return Notoriety.CanBeAttacked;
|
|
}
|
|
|
|
master = bcTarg.Controlled ? bcTarg.ControlMaster : null;
|
|
|
|
if (Core.ML && master != null)
|
|
{
|
|
if (source == master && CheckAggressor(bcTarg.Aggressors, source) ||
|
|
CheckAggressor(source.Aggressors, bcTarg))
|
|
{
|
|
return Notoriety.CanBeAttacked;
|
|
}
|
|
|
|
return MobileNotoriety(source, master);
|
|
}
|
|
|
|
if (!bcTarg.Summoned && !bcTarg.Controlled && pmFrom.EnemyOfOneType == bcTarg.GetType())
|
|
{
|
|
return Notoriety.Enemy;
|
|
}
|
|
}
|
|
|
|
if (target.Murderer ||
|
|
target.Body.IsMonster && IsSummoned(bcTarg) && target is not BaseFamiliar && target is not ArcaneFey &&
|
|
target is not Golem || bcTarg?.IsAnimatedDead == true)
|
|
{
|
|
return Notoriety.Murderer;
|
|
}
|
|
|
|
if (target.Criminal)
|
|
{
|
|
return Notoriety.Criminal;
|
|
}
|
|
|
|
var sourceGuild = GetGuildFor(source.Guild as Guild, source);
|
|
var targetGuild = GetGuildFor(target.Guild as Guild, target);
|
|
|
|
if (sourceGuild != null && targetGuild != null)
|
|
{
|
|
if (sourceGuild == targetGuild || sourceGuild.IsAlly(targetGuild))
|
|
{
|
|
return Notoriety.Ally;
|
|
}
|
|
|
|
if (sourceGuild.IsEnemy(targetGuild))
|
|
{
|
|
return Notoriety.Enemy;
|
|
}
|
|
}
|
|
|
|
var srcFaction = Faction.Find(source, true, true);
|
|
var trgFaction = Faction.Find(target, true, true);
|
|
|
|
if (srcFaction != null && trgFaction != null && srcFaction != trgFaction && source.Map == Faction.Facet)
|
|
{
|
|
return Notoriety.Enemy;
|
|
}
|
|
|
|
if (Stealing.ClassicMode && pmTarg?.PermaFlags.Contains(source) == true ||
|
|
bcTarg?.AlwaysAttackable == true || CheckHouseFlag(source, target, target.Location, target.Map))
|
|
{
|
|
return Notoriety.CanBeAttacked;
|
|
}
|
|
|
|
if (bcTarg?.InitialInnocent != true)
|
|
{
|
|
if (!target.Body.IsHuman && !target.Body.IsGhost && !IsPet(bcTarg) && pmTarg == null ||
|
|
!Core.ML && !target.CanBeginAction<PolymorphSpell>())
|
|
{
|
|
return Notoriety.CanBeAttacked;
|
|
}
|
|
}
|
|
|
|
if (CheckAggressor(source.Aggressors, target))
|
|
{
|
|
return Notoriety.CanBeAttacked;
|
|
}
|
|
|
|
if (CheckAggressed(source.Aggressed, target))
|
|
{
|
|
return Notoriety.CanBeAttacked;
|
|
}
|
|
|
|
if (bcTarg?.Controlled == true && bcTarg.ControlOrder == OrderType.Guard &&
|
|
bcTarg.ControlTarget == source)
|
|
{
|
|
return Notoriety.CanBeAttacked;
|
|
}
|
|
|
|
if (source is BaseCreature bc)
|
|
{
|
|
var master = bc.GetMaster();
|
|
|
|
if (master != null && (CheckAggressor(master.Aggressors, target) ||
|
|
MobileNotoriety(master, target) == Notoriety.CanBeAttacked || bcTarg != null))
|
|
{
|
|
return Notoriety.CanBeAttacked;
|
|
}
|
|
}
|
|
|
|
return Notoriety.Innocent;
|
|
}
|
|
|
|
public static bool CheckHouseFlag(Mobile from, Mobile m, Point3D p, Map map)
|
|
{
|
|
var house = BaseHouse.FindHouseAt(p, map, 16);
|
|
|
|
if (house?.Public != false || !house.IsFriend(from))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (m != null && house.IsFriend(m))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return m is not BaseCreature c || c.Deleted || !c.Controlled || c.ControlMaster == null ||
|
|
!house.IsFriend(c.ControlMaster);
|
|
}
|
|
|
|
public static bool IsPet(BaseCreature c) => c?.Controlled == true;
|
|
|
|
public static bool IsSummoned(BaseCreature c) => c?.Summoned == true;
|
|
|
|
public static bool CheckAggressor(List<AggressorInfo> list, Mobile target)
|
|
{
|
|
for (var i = 0; i < list.Count; ++i)
|
|
{
|
|
if (list[i].Attacker == target)
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
public static bool CheckAggressed(List<AggressorInfo> list, Mobile target)
|
|
{
|
|
for (var i = 0; i < list.Count; ++i)
|
|
{
|
|
var info = list[i];
|
|
|
|
if (!info.CriminalAggression && info.Defender == target)
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private enum GuildStatus
|
|
{
|
|
None,
|
|
Peaceful,
|
|
Waring
|
|
}
|
|
}
|
|
}
|