ControlMaster and SummonMaster, when both set, are always the same mobile: BaseCreature.Summon assigns both to the caster, and every pet management flow (transfer, stable, claim, ball of summoning, GM obey, login overflow) followed SetControlMaster with an identical SummonMaster assignment. They differ only in presence - uncontrolled summons carry only a summon master, pets only a control master. So one _master reference serializes (refreshed at save, fanned back out through the Controlled/Summoned flags in AfterDeserialization; legacy loads feed the same path), and SetControlMaster now keeps SummonMaster in lockstep itself, deleting the six hand-rolled copies of that boilerplate. Also: a creature constructed without speeds (missing npc-speeds.json) now logs debug and defaults to Medium (0.25/0.5) instead of throwing - this is the place a sane default belongs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
452 lines
13 KiB
C#
452 lines
13 KiB
C#
/*************************************************************************
|
|
* ModernUO *
|
|
* Copyright 2019-2026 - ModernUO Development Team *
|
|
* Email: hi@modernuo.com *
|
|
* File: OnSpeech.cs *
|
|
* *
|
|
* This program is free software: you can redistribute it and/or modify *
|
|
* it under the terms of the GNU General Public License as published by *
|
|
* the Free Software Foundation, either version 3 of the License, or *
|
|
* (at your option) any later version. *
|
|
* *
|
|
* You should have received a copy of the GNU General Public License *
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
|
************************************************************************/
|
|
|
|
using System;
|
|
using Server.Items;
|
|
using Server.Gumps;
|
|
|
|
namespace Server.Mobiles;
|
|
|
|
public abstract partial class BaseAI
|
|
{
|
|
public virtual bool HandlesOnSpeech(Mobile from)
|
|
{
|
|
if (from.AccessLevel >= AccessLevel.GameMaster)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (from.Alive && Mobile.Controlled && Mobile.Commandable &&
|
|
(from == Mobile.ControlMaster || Mobile.IsPetFriend(from)))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
return from.Alive && from.InRange(Mobile.Location, 3) && Mobile.IsHumanInTown();
|
|
}
|
|
|
|
public virtual void OnSpeech(SpeechEventArgs e)
|
|
{
|
|
if (WasNamed(e.Speech) && e.Mobile.Alive &&
|
|
e.Mobile.InRange(Mobile.Location, 3) && Mobile.IsHumanInTown())
|
|
{
|
|
if (HandleMoveCommand(e) || HandleTimeCommand(e) || HandleTrainCommand(e))
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (Mobile.Controlled && Mobile.Commandable)
|
|
{
|
|
AllOnSpeechPet(e);
|
|
NamedOnSpeechPet(e);
|
|
return;
|
|
}
|
|
|
|
if (e.Mobile.AccessLevel >= AccessLevel.GameMaster)
|
|
{
|
|
HandleGMCommands(e);
|
|
}
|
|
}
|
|
|
|
private bool HandleMoveCommand(SpeechEventArgs e)
|
|
{
|
|
if (!e.HasKeyword(0x9D)) // *move*
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if ((Core.Now - _lastOrder).TotalSeconds < 5)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
_lastOrder = Core.Now;
|
|
|
|
var map = Mobile.Map;
|
|
var currentLoc = Mobile.Location;
|
|
|
|
var newX = currentLoc.X + Utility.RandomMinMax(-1, 1);
|
|
var newY = currentLoc.Y + Utility.RandomMinMax(-1, 1);
|
|
var newZ = currentLoc.Z;
|
|
|
|
if (map != null && map.CanFit(newX, newY, newZ, 16, false, false))
|
|
{
|
|
Mobile.PublicOverheadMessage(MessageType.Regular, 0x3B2, 501516); // Excuse me?
|
|
Mobile.Location = new Point3D(newX, newY, newZ);
|
|
}
|
|
else
|
|
{
|
|
Mobile.PublicOverheadMessage(MessageType.Regular, 0x3B2, 501487);
|
|
// You're standing too close, go away.
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private bool HandleTimeCommand(SpeechEventArgs e)
|
|
{
|
|
if (!e.HasKeyword(0x9E)) // *time*
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if ((Core.Now - _lastOrder).TotalSeconds < 5)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
_lastOrder = Core.Now;
|
|
|
|
Clock.GetTime(Mobile, out var generalNumber, out _);
|
|
Mobile.PublicOverheadMessage(MessageType.Regular, 0x3B2, generalNumber);
|
|
|
|
return true;
|
|
}
|
|
|
|
private bool HandleTrainCommand(SpeechEventArgs e)
|
|
{
|
|
if (!e.HasKeyword(0x6C)) // *train*
|
|
{
|
|
return false;
|
|
}
|
|
|
|
HandleTraining(e.Mobile);
|
|
return true;
|
|
}
|
|
|
|
public virtual void AllOnSpeechPet(SpeechEventArgs e)
|
|
{
|
|
if (!e.Mobile.InRange(Mobile.Location, 14))
|
|
{
|
|
return;
|
|
}
|
|
|
|
var isOwner = e.Mobile == Mobile.ControlMaster;
|
|
var isPetFriend = !isOwner && Mobile.IsPetFriend(e.Mobile);
|
|
|
|
if (!isOwner && !isPetFriend)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var keyword = e.GetFirstKeyword(
|
|
0x164, // all come
|
|
0x165, // all follow
|
|
0x166, // all guard
|
|
0x167, // all stop
|
|
0x168, // all kill
|
|
0x169, // all attack
|
|
0x16B, // all guard me
|
|
0x16C, // all follow me
|
|
0x170 // all stay
|
|
);
|
|
|
|
switch (keyword)
|
|
{
|
|
case 0x164: // all come
|
|
{
|
|
HandleComeCommand(e.Mobile, true);
|
|
break;
|
|
}
|
|
case 0x165: // all follow
|
|
{
|
|
BeginPickTarget(e.Mobile, OrderType.Follow);
|
|
break;
|
|
}
|
|
case 0x166: // all guard
|
|
case 0x16B: // all guard me
|
|
{
|
|
HandleGuardCommand(e.Mobile, true);
|
|
break;
|
|
}
|
|
case 0x167: // all stop
|
|
{
|
|
HandleStayStopFollowCommand(e.Mobile, OrderType.Stop);
|
|
break;
|
|
}
|
|
case 0x168: // all kill
|
|
case 0x169: // all attack
|
|
{
|
|
HandleAttackCommand(e.Mobile, true);
|
|
break;
|
|
}
|
|
case 0x16C: // all follow me
|
|
{
|
|
HandleStayStopFollowCommand(e.Mobile, OrderType.Follow, e.Mobile);
|
|
break;
|
|
}
|
|
case 0x170: // all stay
|
|
{
|
|
HandleStayStopFollowCommand(e.Mobile, OrderType.Stay);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
public virtual void NamedOnSpeechPet(SpeechEventArgs e)
|
|
{
|
|
if (!e.Mobile.InRange(Mobile.Location, 14))
|
|
{
|
|
return;
|
|
}
|
|
|
|
var isOwner = e.Mobile == Mobile.ControlMaster;
|
|
var isPetFriend = !isOwner && Mobile.IsPetFriend(e.Mobile);
|
|
|
|
if (!isOwner && !isPetFriend)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var keyword = e.GetFirstKeyword(
|
|
0x155, // *come
|
|
0x156, // *drop
|
|
0x15A, // *follow
|
|
0x15B, // *friend
|
|
0x15C, // *guard
|
|
0x15D, // *kill
|
|
0x15E, // *attack
|
|
0x161, // *stop
|
|
0x163, // *follow me
|
|
0x16D, // *release
|
|
0x16E, // *transfer
|
|
0x16F // *stay
|
|
);
|
|
|
|
switch (keyword)
|
|
{
|
|
case 0x155: // *come
|
|
{
|
|
HandleComeCommand(e.Mobile, true);
|
|
break;
|
|
}
|
|
case 0x156: // *drop
|
|
{
|
|
HandleDropCommand(e.Mobile, true, e.Speech);
|
|
break;
|
|
}
|
|
case 0x15A: // *follow
|
|
{
|
|
BeginPickTarget(e.Mobile, OrderType.Follow);
|
|
break;
|
|
}
|
|
case 0x15B: // *friend
|
|
{
|
|
HandleFriendCommand(e.Mobile, true, e.Speech);
|
|
break;
|
|
}
|
|
case 0x15C: // *guard
|
|
{
|
|
HandleGuardCommand(e.Mobile, true);
|
|
break;
|
|
}
|
|
case 0x15D: // *kill
|
|
case 0x15E: // *attack
|
|
{
|
|
HandleAttackCommand(e.Mobile, true);
|
|
break;
|
|
}
|
|
case 0x161: // *stop
|
|
{
|
|
HandleStayStopFollowCommand(e.Mobile, OrderType.Stop);
|
|
break;
|
|
}
|
|
case 0x163: // *follow me
|
|
{
|
|
HandleStayStopFollowCommand(e.Mobile, OrderType.Follow, e.Mobile);
|
|
break;
|
|
}
|
|
case 0x16D: // *release
|
|
{
|
|
HandleReleaseCommand(e.Mobile, true, e.Speech);
|
|
break;
|
|
}
|
|
case 0x16E: // *transfer
|
|
{
|
|
HandleTransferCommand(e.Mobile, true, e.Speech);
|
|
break;
|
|
}
|
|
case 0x16F: // *stay
|
|
{
|
|
HandleStayStopFollowCommand(e.Mobile, OrderType.Stay);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
private void HandleTraining(Mobile from)
|
|
{
|
|
var foundSomething = false;
|
|
|
|
foreach (var skill in Mobile.Skills)
|
|
{
|
|
if (skill.Base < 60.0 || !Mobile.CheckTeach(skill.SkillName, from))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var toTeach = Math.Min(skill.Base / 3.0, 42.0);
|
|
|
|
if (toTeach <= from.Skills[skill.SkillName].Base)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var number = 1043059 + (int)skill.SkillName; // alchemy
|
|
if (number > 1043107) // disarming traps
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (!foundSomething)
|
|
{
|
|
Mobile.Say(1043058); // I can train the following:
|
|
foundSomething = true;
|
|
}
|
|
|
|
Mobile.Say(number);
|
|
}
|
|
|
|
if (!foundSomething)
|
|
{
|
|
Mobile.Say(501505); // Alas, I cannot teach thee anything.
|
|
}
|
|
}
|
|
|
|
private void HandleComeCommand(Mobile from, bool isOwner)
|
|
{
|
|
if (isOwner && Mobile.CheckControlChance(from))
|
|
{
|
|
_commandIssuer = from;
|
|
Mobile.ControlTarget = null;
|
|
Mobile.ControlOrder = OrderType.Come;
|
|
}
|
|
}
|
|
|
|
private void HandleGuardCommand(Mobile from, bool isOwner)
|
|
{
|
|
if (isOwner && Mobile.CheckControlChance(from))
|
|
{
|
|
_commandIssuer = from;
|
|
Mobile.ControlTarget = null;
|
|
Mobile.ControlOrder = OrderType.Guard;
|
|
}
|
|
}
|
|
|
|
private void HandleStayStopFollowCommand(Mobile from, OrderType order, Mobile target = null)
|
|
{
|
|
if (Mobile.CheckControlChance(from))
|
|
{
|
|
_commandIssuer = from;
|
|
Mobile.ControlTarget = target;
|
|
Mobile.ControlOrder = order;
|
|
}
|
|
}
|
|
|
|
private void HandleAttackCommand(Mobile from, bool isOwner)
|
|
{
|
|
if (isOwner)
|
|
{
|
|
_commandIssuer = from;
|
|
BeginPickTarget(from, OrderType.Attack);
|
|
}
|
|
}
|
|
|
|
private void HandleDropCommand(Mobile from, bool isOwner, string speech)
|
|
{
|
|
if (isOwner && !Mobile.IsDeadPet && !Mobile.Summoned && WasNamed(speech)
|
|
&& Mobile.CheckControlChance(from))
|
|
{
|
|
_commandIssuer = from;
|
|
Mobile.ControlTarget = null;
|
|
Mobile.ControlOrder = OrderType.Drop;
|
|
}
|
|
}
|
|
|
|
private void HandleFriendCommand(Mobile from, bool isOwner, string speech)
|
|
{
|
|
if (isOwner && WasNamed(speech) && Mobile.CheckControlChance(from))
|
|
{
|
|
if (Mobile.Summoned || Mobile is GrizzledMare)
|
|
{
|
|
from.SendLocalizedMessage(1005481);
|
|
// Summoned creatures are loyal only to their summoners.
|
|
return;
|
|
}
|
|
|
|
if (from.HasTrade)
|
|
{
|
|
from.SendLocalizedMessage(1070947);
|
|
// You cannot friend a pet with a trade pending
|
|
return;
|
|
}
|
|
|
|
BeginPickTarget(from, OrderType.Friend);
|
|
}
|
|
}
|
|
|
|
private void HandleReleaseCommand(Mobile from, bool isOwner, string speech)
|
|
{
|
|
if (!isOwner)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (WasNamed(speech) && Mobile.CheckControlChance(from))
|
|
{
|
|
if (!Mobile.Summoned)
|
|
{
|
|
from.SendGump(new ConfirmReleaseGump(from, Mobile));
|
|
}
|
|
else
|
|
{
|
|
Mobile.ControlOrder = OrderType.Release;
|
|
}
|
|
}
|
|
}
|
|
|
|
private void HandleTransferCommand(Mobile from, bool isOwner, string speech)
|
|
{
|
|
if (isOwner && !Mobile.IsDeadPet && WasNamed(speech) && Mobile.CheckControlChance(from))
|
|
{
|
|
if (Mobile.Summoned || Mobile is GrizzledMare)
|
|
{
|
|
from.SendLocalizedMessage(1005487);
|
|
// You cannot transfer ownership of a summoned creature.
|
|
return;
|
|
}
|
|
|
|
if (from.HasTrade)
|
|
{
|
|
from.SendLocalizedMessage(1010507);
|
|
// You cannot transfer a pet with a trade pending
|
|
return;
|
|
}
|
|
|
|
BeginPickTarget(from, OrderType.Transfer);
|
|
}
|
|
}
|
|
|
|
private void HandleGMCommands(SpeechEventArgs e)
|
|
{
|
|
this.DebugSayFormatted($"Command is from GM: {e.Mobile.Name}, Target: {Mobile.ControlTarget?.Name ?? "None or Unknown"}");
|
|
|
|
if (Mobile.FindMyName(e.Speech, true) && e.Speech.InsensitiveContains("obey"))
|
|
{
|
|
Mobile.SetControlMaster(e.Mobile);
|
|
}
|
|
}
|
|
}
|