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>
186 lines
5 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|