ModernUO/Projects/UOContent/Engines/Quests/Collector/Objectives.cs
Kamron Batman b90ac0d481
perf: Migrate Quest gumps to DynamicGump (#2416)
## Summary

Migrates the Quest system gumps from legacy `Gump` to `DynamicGump` / `StaticGump<T>`.

**Renames** `Engines/ML Quests/Gumps/BaseQuestGump` to **`BaseMLQuestGump`** to
disambiguate from the Core quest abstract (`Engines/Quests/Core/QuestSystem.cs`).
Both abstracts now extend `DynamicGump`.

**Abstract bases**:
- `Server.Engines.Quests.BaseQuestGump` (Core) - now `abstract DynamicGump`. Holds constants and a static `AddHtmlObject(ref DynamicGumpBuilder, ...)` helper. Concrete subclasses each provide their own `BuildLayout`.
- `Server.Engines.MLQuests.Gumps.BaseMLQuestGump` (ML, renamed) - now `abstract DynamicGump` with a `protected abstract BuildContent(ref DynamicGumpBuilder)` hook. The base's `BuildLayout` draws shared chrome (background art, frame, label header) and then invokes `BuildContent`; subclasses use `BuildPage`, `SetTitle`, `RegisterButton`, `SetPageCount`, and content helpers (`AddDescription`, `AddObjectives`, `AddObjectivesProgress`, `AddRewardsPage`, `AddRewards`, `AddConversation`).

**Concrete Core gumps** migrated to `DynamicGump`:
- `QuestCancelGump`, `QuestOfferGump` (Core), `QuestObjectivesGump`, `QuestConversationsGump`, `QuestLogUpdatedGump`, `QuestItemInfoGump`, `SheetMusicOfferGump` (Impresario), `PaintedImageGump` (renamed from `PaintedImage.InternalGump`).

**Concrete ML gumps** migrated to `DynamicGump` (extending `BaseMLQuestGump` or directly):
- `InfoNPCGump`, `QuestConversationGump`, `QuestLogDetailedGump`, `QuestLogGump`, `QuestOfferGump` (ML), `QuestReportBackGump`, `QuestRewardGump`, `QuestCancelConfirmGump`, `RaceChangeConfirmGump`.

**`StaticGump<T>` migrations**:
- `ScrollOfAbraxusGump` (Dark Tides). Its layout is a single hard-coded cliloc (1060116) with no per-instance dynamic content - safe to cache.

**Cliloc rule**: Every other quest dialog bakes per-instance cliloc IDs into its layout (quest titles, NPC names, race-specific prompts, era-conditional progress messages, escort destinations). Per the cliloc rule, baking different cliloc numbers into a cached layout would defeat `StaticGump<T>` caching - so all of these are `DynamicGump`.

**Signature changes** to support the migration:
- `QuestObjective.RenderMessage`/`RenderProgress` now take `ref DynamicGumpBuilder builder` (15 overrides updated across Collector, Solen Matriarch, Ambitious Solen Queen, Study of the Solen Hive, Terrible Hatchlings, The Summoning, Uzeraan Turmoil, Witch Apprentice, Emino's Undertaking).
- ML `BaseObjective.WriteToGump` / `BaseObjectiveInstance.WriteToGump` / `BaseReward.WriteToGump` now take `ref DynamicGumpBuilder` (KillObjective, GainSkillObjective, EscortObjective, CollectObjective, DeliverObjective, BaseReward).

**Empty-gump and `Singleton` rules**: All concrete gumps now have private constructors with static `DisplayTo` entry points that null-check the player NetState before allocation. All gumps that shouldn't stack are `Singleton => true`.

**External callers updated**: `MLQuest.SendOffer`/`OnRefuse`, `MLQuestEntry.SendProgressGump`/`SendRewardGump`/`SendReportBackGump`, `MLQuestSystem.QuestGumpRequest` and `ViewQuestsCommand`, `BoonCollector` (Darius/Nedrick), `SirHelper.OnDoubleClick` (no longer caches a single shared `InfoNPCGump` instance - `DisplayTo` constructs one per click and the gump's `Singleton => true` handles deduplication), `RaceChangeDeed.OnDoubleClick`, `PaintedImage.OnDoubleClick`, `ScrollOfAbraxus.OnDoubleClick`, `Impresario.OnTalk`, and `PlayerMobile`'s `BaseQuestGump` alias is now `BaseMLQuestGump`.

**Concrete subclasses found beyond the listed entry points**: `SheetMusicOfferGump` (in Impresario.cs), `PaintedImageGump` (was `PaintedImage.InternalGump`), `QuestObjectivesGump`, `QuestConversationsGump`, `QuestLogUpdatedGump`, `QuestItemInfoGump` (the Core base has these embedded across `QuestSystem.cs`/`QuestObjective.cs`/`QuestConversation.cs`/`QuestItemInfo.cs`).

**Code-standards cleanup**: Renamed legacy `m_X` private fields to `_x` in rewritten files; braces on all control flow.
2026-04-25 20:23:13 -07:00

398 lines
10 KiB
C#

using System;
using Server.Gumps;
namespace Server.Engines.Quests.Collector
{
public class FishPearlsObjective : QuestObjective
{
public override int Message => 1055084;
public override int MaxProgress => 6;
public override void RenderProgress(ref DynamicGumpBuilder builder)
{
if (!Completed)
{
// Rainbow pearls collected:
BaseQuestGump.AddHtmlObject(ref builder, 70, 260, 270, 100, 1055085, BaseQuestGump.Blue, false, false);
builder.AddLabel(70, 280, 0x64, $"{CurProgress}");
builder.AddLabel(100, 280, 0x64, "/");
builder.AddLabel(130, 280, 0x64, $"{MaxProgress}");
}
else
{
base.RenderProgress(ref builder);
}
}
public override void OnComplete()
{
System.AddObjective(new ReturnPearlsObjective());
}
}
public class ReturnPearlsObjective : QuestObjective
{
public override int Message => 1055088;
public override void OnComplete()
{
System.AddConversation(new ReturnPearlsConversation());
}
}
public class FindAlbertaObjective : QuestObjective
{
public override int Message => 1055091;
public override void OnComplete()
{
System.AddConversation(new AlbertaPaintingConversation());
}
}
public class SitOnTheStoolObjective : QuestObjective
{
private static readonly Point3D m_StoolLocation = new(2899, 706, 0);
private static readonly Map m_StoolMap = Map.Trammel;
private DateTime m_Begin;
public SitOnTheStoolObjective() => m_Begin = DateTime.MaxValue;
public override int Message => 1055093;
public override void CheckProgress()
{
var pm = System.From;
if (pm.Map == m_StoolMap && pm.Location == m_StoolLocation)
{
if (m_Begin == DateTime.MaxValue)
{
m_Begin = Core.Now;
}
else if (Core.Now - m_Begin > TimeSpan.FromSeconds(30.0))
{
Complete();
}
}
else if (m_Begin != DateTime.MaxValue)
{
m_Begin = DateTime.MaxValue;
pm.SendLocalizedMessage(
1055095,
"",
0x26
); // You must remain seated on the stool until the portrait is complete. Alberta will now have to start again with a fresh canvas.
}
}
public override void OnComplete()
{
System.AddConversation(new AlbertaEndPaintingConversation());
}
}
public class ReturnPaintingObjective : QuestObjective
{
public override int Message => 1055099;
public override void OnComplete()
{
System.AddConversation(new ReturnPaintingConversation());
}
}
public class FindGabrielObjective : QuestObjective
{
public override int Message => 1055101;
public override void OnComplete()
{
System.AddConversation(new GabrielAutographConversation());
}
}
public enum Theater
{
Britain,
Nujelm,
Jhelom
}
public class FindSheetMusicObjective : QuestObjective
{
private Theater m_Theater;
public FindSheetMusicObjective(bool init)
{
if (init)
{
InitTheater();
}
}
public FindSheetMusicObjective()
{
}
public override int Message => 1055104;
public void InitTheater()
{
m_Theater = Utility.Random(3) switch
{
1 => Theater.Britain,
2 => Theater.Nujelm,
_ => Theater.Jhelom
};
}
public bool IsInRightTheater()
{
var player = System.From;
var region = Region.Find(player.Location, player.Map);
if (region == null)
{
return false;
}
return m_Theater switch
{
Theater.Britain => region.IsPartOf("Britain"),
Theater.Nujelm => region.IsPartOf("Nujel'm"),
Theater.Jhelom => region.IsPartOf("Jhelom"),
_ => false
};
}
public override void OnComplete()
{
System.AddConversation(new GetSheetMusicConversation());
}
public override void ChildDeserialize(IGenericReader reader)
{
var version = reader.ReadEncodedInt();
m_Theater = (Theater)reader.ReadEncodedInt();
}
public override void ChildSerialize(IGenericWriter writer)
{
writer.WriteEncodedInt(0); // version
writer.WriteEncodedInt((int)m_Theater);
}
}
public class ReturnSheetMusicObjective : QuestObjective
{
public override int Message => 1055110;
public override void OnComplete()
{
System.AddConversation(new GabrielSheetMusicConversation());
}
}
public class ReturnAutographObjective : QuestObjective
{
public override int Message => 1055114;
public override void OnComplete()
{
System.AddConversation(new ReturnAutographConversation());
}
}
public class FindTomasObjective : QuestObjective
{
public override int Message => 1055117;
public override void OnComplete()
{
System.AddConversation(new TomasToysConversation());
}
}
public enum CaptureResponse
{
Valid,
AlreadyDone,
Invalid
}
public class CaptureImagesObjective : QuestObjective
{
private bool[] m_Done;
private ImageType[] m_Images;
public CaptureImagesObjective(bool init)
{
if (init)
{
m_Images = ImageTypeInfo.RandomList(4);
m_Done = new bool[4];
}
}
public CaptureImagesObjective()
{
}
public override int Message => 1055120;
public override bool Completed
{
get
{
for (var i = 0; i < m_Done.Length; i++)
{
if (!m_Done[i])
{
return false;
}
}
return true;
}
}
public override bool IgnoreYoungProtection(Mobile from)
{
if (Completed)
{
return false;
}
var fromType = from.GetType();
for (var i = 0; i < m_Images.Length; i++)
{
var info = ImageTypeInfo.Get(m_Images[i]);
if (info.Type == fromType)
{
return true;
}
}
return false;
}
public CaptureResponse CaptureImage(Type type, out ImageType image)
{
for (var i = 0; i < m_Images.Length; i++)
{
var info = ImageTypeInfo.Get(m_Images[i]);
if (info.Type == type)
{
image = m_Images[i];
if (m_Done[i])
{
return CaptureResponse.AlreadyDone;
}
m_Done[i] = true;
CheckCompletionStatus();
return CaptureResponse.Valid;
}
}
image = 0;
return CaptureResponse.Invalid;
}
public override void RenderProgress(ref DynamicGumpBuilder builder)
{
if (!Completed)
{
for (var i = 0; i < m_Images.Length; i++)
{
var info = ImageTypeInfo.Get(m_Images[i]);
BaseQuestGump.AddHtmlObject(ref builder, 70, 260 + 20 * i, 200, 100, info.Name, BaseQuestGump.Blue, false, false);
builder.AddLabel(200, 260 + 20 * i, 0x64, " : ");
BaseQuestGump.AddHtmlObject(
ref builder,
220,
260 + 20 * i,
100,
100,
m_Done[i] ? 1055121 : 1055122,
BaseQuestGump.Blue,
false,
false
);
}
}
else
{
base.RenderProgress(ref builder);
}
}
public override void OnComplete()
{
System.AddObjective(new ReturnImagesObjective());
}
public override void ChildDeserialize(IGenericReader reader)
{
var version = reader.ReadEncodedInt();
var count = reader.ReadEncodedInt();
m_Images = new ImageType[count];
m_Done = new bool[count];
for (var i = 0; i < count; i++)
{
m_Images[i] = (ImageType)reader.ReadEncodedInt();
m_Done[i] = reader.ReadBool();
}
}
public override void ChildSerialize(IGenericWriter writer)
{
writer.WriteEncodedInt(0); // version
writer.WriteEncodedInt(m_Images.Length);
for (var i = 0; i < m_Images.Length; i++)
{
writer.WriteEncodedInt((int)m_Images[i]);
writer.Write(m_Done[i]);
}
}
}
public class ReturnImagesObjective : QuestObjective
{
public override int Message => 1055128;
public override void OnComplete()
{
System.AddConversation(new ReturnImagesConversation());
}
}
public class ReturnToysObjective : QuestObjective
{
public override int Message => 1055132;
}
public class MakeRoomObjective : QuestObjective
{
public override int Message => 1055136;
}
}