ModernUO/Projects/UOContent/Engines/ML Quests/Objectives/BaseObjective.cs
Kamron Batman 8d1283ba27
feat: convert all delta-time serialization to anchored time
Every actively-written delta-time value now stores an absolute anchored
timestamp, shifted by downtime at load: remaining time is preserved across
restarts and idle saves become byte-stable.

- Item v11: LastMoved (was whole-minute delta, rewritten every save) and
  DecayResetTime; Mobile v38: the three stat-gain stamps; BaseCreature v21:
  SummonEnd; plus VendorInventory, StealableArtifacts, and ML quest objective
  timers in their own version gates.
- 17 code-generated classes swap [DeltaDateTime] for [AnchoredDateTime] with a
  version bump and MigrateFrom each (generated from their current migration
  schemas, so old saves replay the delta reads).
- GenericPersistence payloads carry no anchor of their own, so they inherit
  the save-wide shift: entity indexes always load first, and every file in a
  save shares one SaveStartTime - World.LoadTimeShift is stamped from the idx
  v5 header and applied to generic persistence readers. This is what makes
  VirtueContext and StealableArtifacts convertible at all.
- Legacy read sites (old-version fallbacks and migration replays) keep
  ReadDeltaTime: they decode old bytes and must never change.

Acceptance tests: item serialization is byte-identical across saves hours
apart, and LastMoved/DecayResetTime round-trip exactly at sub-minute
precision (impossible under the old minutes encoding).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-22 19:19:42 -07:00

186 lines
5 KiB
C#

using System;
using Server.Gumps;
using Server.Mobiles;
namespace Server.Engines.MLQuests.Objectives
{
public abstract class BaseObjective
{
public virtual bool IsTimed => false;
public virtual TimeSpan Duration => TimeSpan.Zero;
public virtual bool CanOffer(IQuestGiver quester, PlayerMobile pm, bool message) => true;
public abstract void WriteToGump(ref DynamicGumpBuilder builder, ref int y);
public virtual BaseObjectiveInstance CreateInstance(MLQuestInstance instance) => null;
}
public abstract class BaseObjectiveInstance
{
public enum DataType : byte
{
None,
EscortObjective,
KillObjective,
DeliverObjective
}
public BaseObjectiveInstance(MLQuestInstance instance, BaseObjective obj)
{
Instance = instance;
if (obj.IsTimed)
{
EndTime = Core.Now + obj.Duration;
}
}
public MLQuestInstance Instance { get; }
public bool IsTimed => EndTime != DateTime.MinValue;
public DateTime EndTime { get; set; }
public bool Expired { get; set; }
public virtual DataType ExtraDataType => DataType.None;
public virtual void WriteToGump(ref DynamicGumpBuilder builder, ref int y)
{
if (IsTimed)
{
WriteTimeRemaining(ref builder, ref y, Utility.Max(EndTime - Core.Now, TimeSpan.Zero));
}
}
public static void WriteTimeRemaining(ref DynamicGumpBuilder builder, ref int y, TimeSpan timeRemaining)
{
builder.AddHtmlLocalized(103, y, 120, 16, 1062379, 0x5F90); // Est. time remaining:
builder.AddLabel(223, y, 0x481, $"{timeRemaining.TotalSeconds:F0}");
y += 16;
}
public virtual bool AllowsQuestItem(Item item, Type type) => false;
public virtual bool IsCompleted() => false;
public virtual void CheckComplete()
{
if (IsCompleted())
{
Instance.Player.PlaySound(0x5B6); // public sound
Instance.CheckComplete();
}
}
public virtual void OnQuestAccepted()
{
}
public virtual void OnQuestCancelled()
{
}
public virtual void OnQuestCompleted()
{
}
public virtual bool OnBeforeClaimReward() => true;
public virtual void OnClaimReward()
{
}
public virtual void OnAfterClaimReward()
{
}
public virtual void OnRewardClaimed()
{
}
public virtual void OnQuesterDeleted()
{
}
public virtual void OnPlayerDeath()
{
}
public virtual void OnExpire()
{
}
public virtual void Serialize(IGenericWriter writer)
{
// Version info is written in MLQuestPersistence.Serialize
if (IsTimed)
{
writer.Write(true);
writer.WriteAnchoredTime(EndTime);
}
else
{
writer.Write(false);
}
// For type checks on deserialization
// (This way quest objectives can be changed without breaking serialization)
writer.Write((byte)ExtraDataType);
}
public static void Deserialize(IGenericReader reader, int version, BaseObjectiveInstance objInstance)
{
if (reader.ReadBool())
{
var endTime = version >= 3 ? reader.ReadAnchoredTime() : reader.ReadDeltaTime();
if (objInstance != null)
{
objInstance.EndTime = endTime;
}
}
var extraDataType = (DataType)reader.ReadByte();
switch (extraDataType)
{
case DataType.EscortObjective:
{
var completed = reader.ReadBool();
if (objInstance is EscortObjectiveInstance instance)
{
instance.HasCompleted = completed;
}
break;
}
case DataType.KillObjective:
{
var slain = reader.ReadInt();
if (objInstance is KillObjectiveInstance instance)
{
instance.Slain = slain;
}
break;
}
case DataType.DeliverObjective:
{
var completed = reader.ReadBool();
if (objInstance is DeliverObjectiveInstance instance)
{
instance.HasCompleted = completed;
}
break;
}
}
}
}
}