ModernUO/assets/js/43f827d1.080e73fe.js

1 line
No EOL
18 KiB
JavaScript

"use strict";(globalThis.webpackChunkwebsite=globalThis.webpackChunkwebsite||[]).push([[518],{7096(e,i,t){t.r(i),t.d(i,{assets:()=>d,contentTitle:()=>a,default:()=>h,frontMatter:()=>s,metadata:()=>n,toc:()=>c});const n=JSON.parse('{"id":"development/timers","title":"Timers","description":"ModernUO uses a hierarchical timer wheel for scheduling delayed and recurring actions. The system is single-threaded, lock-free, and processes timers during each game loop tick.","source":"@site/content/development/timers.mdx","sourceDirName":"development","slug":"/development/timers","permalink":"/docs/development/timers","draft":false,"unlisted":false,"editUrl":"https://github.com/modernuo/ModernUO/tree/main/website/content/development/timers.mdx","tags":[],"version":"current","sidebarPosition":3,"frontMatter":{"sidebar_position":3,"title":"Timers"},"sidebar":"docsSidebar","previous":{"title":"Serialization","permalink":"/docs/development/serialization"},"next":{"title":"Commands & Targeting","permalink":"/docs/development/commands-and-targeting"}}');var r=t(4848),l=t(8453);const s={sidebar_position:3,title:"Timers"},a="Timers",d={},c=[{value:"Overview",id:"overview",level:2},{value:"Timer.StartTimer (Preferred)",id:"timerstarttimer-preferred",level:2},{value:"Immediate Execution",id:"immediate-execution",level:3},{value:"Delayed Execution",id:"delayed-execution",level:3},{value:"Repeating",id:"repeating",level:3},{value:"Repeating with Count Limit",id:"repeating-with-count-limit",level:3},{value:"Delayed Start, Then Repeating",id:"delayed-start-then-repeating",level:3},{value:"Cancellation with TimerExecutionToken",id:"cancellation-with-timerexecutiontoken",level:2},{value:"Token Properties",id:"token-properties",level:3},{value:"Lifecycle Pattern",id:"lifecycle-pattern",level:3},{value:"Timer.DelayCall (Legacy)",id:"timerdelaycall-legacy",level:2},{value:"When to Use DelayCall",id:"when-to-use-delaycall",level:3},{value:"Timer Restoration After Deserialization",id:"timer-restoration-after-deserialization",level:2},{value:"Pattern: Save Expiration Time",id:"pattern-save-expiration-time",level:3},{value:"Pattern: Timer Field with DeserializeTimerField",id:"pattern-timer-field-with-deserializetimerfield",level:3},{value:"Common Mistakes",id:"common-mistakes",level:2},{value:"Avoiding Lambda Allocation",id:"avoiding-lambda-allocation",level:3},{value:"Quick Reference",id:"quick-reference",level:2},{value:"Fire-and-Forget",id:"fire-and-forget",level:3},{value:"Cancellable Repeating Timer",id:"cancellable-repeating-timer",level:3},{value:"Awaitable Pause",id:"awaitable-pause",level:3}];function o(e){const i={admonition:"admonition",code:"code",h1:"h1",h2:"h2",h3:"h3",header:"header",hr:"hr",li:"li",p:"p",pre:"pre",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,l.R)(),...e.components};return(0,r.jsxs)(r.Fragment,{children:[(0,r.jsx)(i.header,{children:(0,r.jsx)(i.h1,{id:"timers",children:"Timers"})}),"\n",(0,r.jsx)(i.p,{children:"ModernUO uses a hierarchical timer wheel for scheduling delayed and recurring actions. The system is single-threaded, lock-free, and processes timers during each game loop tick."}),"\n",(0,r.jsx)(i.hr,{}),"\n",(0,r.jsx)(i.h2,{id:"overview",children:"Overview"}),"\n",(0,r.jsx)(i.p,{children:"The timer wheel has 3 layers with 4,096 slots each:"}),"\n",(0,r.jsxs)(i.table,{children:[(0,r.jsx)(i.thead,{children:(0,r.jsxs)(i.tr,{children:[(0,r.jsx)(i.th,{style:{textAlign:"left"},children:"Layer"}),(0,r.jsx)(i.th,{style:{textAlign:"left"},children:"Resolution"}),(0,r.jsx)(i.th,{style:{textAlign:"left"},children:"Range"})]})}),(0,r.jsxs)(i.tbody,{children:[(0,r.jsxs)(i.tr,{children:[(0,r.jsx)(i.td,{style:{textAlign:"left"},children:"0"}),(0,r.jsx)(i.td,{style:{textAlign:"left"},children:"8ms"}),(0,r.jsx)(i.td,{style:{textAlign:"left"},children:"~32.8 seconds"})]}),(0,r.jsxs)(i.tr,{children:[(0,r.jsx)(i.td,{style:{textAlign:"left"},children:"1"}),(0,r.jsx)(i.td,{style:{textAlign:"left"},children:"~32.8s"}),(0,r.jsx)(i.td,{style:{textAlign:"left"},children:"~22 minutes"})]}),(0,r.jsxs)(i.tr,{children:[(0,r.jsx)(i.td,{style:{textAlign:"left"},children:"2"}),(0,r.jsx)(i.td,{style:{textAlign:"left"},children:"~22m"}),(0,r.jsx)(i.td,{style:{textAlign:"left"},children:"~16 days"})]})]})]}),"\n",(0,r.jsx)(i.p,{children:"Key characteristics:"}),"\n",(0,r.jsxs)(i.ul,{children:["\n",(0,r.jsxs)(i.li,{children:[(0,r.jsx)(i.strong,{children:"O(1) insert and remove"})," -- adding thousands of timers does not slow the server."]}),"\n",(0,r.jsxs)(i.li,{children:[(0,r.jsx)(i.strong,{children:"No locks"})," -- the entire system runs on the main game thread."]}),"\n",(0,r.jsxs)(i.li,{children:[(0,r.jsxs)(i.strong,{children:["No ",(0,r.jsx)(i.code,{children:"TimerPriority"})]})," -- this concept from RunUO is removed entirely."]}),"\n",(0,r.jsxs)(i.li,{children:[(0,r.jsx)(i.strong,{children:"8ms minimum precision"})," -- all delays round up to the nearest 8ms boundary."]}),"\n"]}),"\n",(0,r.jsx)(i.hr,{}),"\n",(0,r.jsx)(i.h2,{id:"timerstarttimer-preferred",children:"Timer.StartTimer (Preferred)"}),"\n",(0,r.jsx)(i.p,{children:"The primary API for creating timers. Timers are automatically pooled for reuse."}),"\n",(0,r.jsx)(i.h3,{id:"immediate-execution",children:"Immediate Execution"}),"\n",(0,r.jsx)(i.pre,{children:(0,r.jsx)(i.code,{className:"language-csharp",children:"Timer.StartTimer(callback);\n"})}),"\n",(0,r.jsx)(i.h3,{id:"delayed-execution",children:"Delayed Execution"}),"\n",(0,r.jsx)(i.pre,{children:(0,r.jsx)(i.code,{className:"language-csharp",children:"Timer.StartTimer(TimeSpan.FromSeconds(5), callback);\n"})}),"\n",(0,r.jsx)(i.h3,{id:"repeating",children:"Repeating"}),"\n",(0,r.jsx)(i.pre,{children:(0,r.jsx)(i.code,{className:"language-csharp",children:"// Repeat every second, starting after 1 second\nTimer.StartTimer(TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(1), callback);\n"})}),"\n",(0,r.jsx)(i.h3,{id:"repeating-with-count-limit",children:"Repeating with Count Limit"}),"\n",(0,r.jsx)(i.pre,{children:(0,r.jsx)(i.code,{className:"language-csharp",children:"// Execute 10 times, once per second, starting immediately\nTimer.StartTimer(TimeSpan.FromSeconds(0), TimeSpan.FromSeconds(1), 10, callback);\n"})}),"\n",(0,r.jsx)(i.h3,{id:"delayed-start-then-repeating",children:"Delayed Start, Then Repeating"}),"\n",(0,r.jsx)(i.pre,{children:(0,r.jsx)(i.code,{className:"language-csharp",children:"// Wait 5 seconds, then repeat every second\nTimer.StartTimer(TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(1), callback);\n"})}),"\n",(0,r.jsx)(i.hr,{}),"\n",(0,r.jsx)(i.h2,{id:"cancellation-with-timerexecutiontoken",children:"Cancellation with TimerExecutionToken"}),"\n",(0,r.jsxs)(i.p,{children:["When you need to cancel a timer later, pass an ",(0,r.jsx)(i.code,{children:"out"})," token:"]}),"\n",(0,r.jsx)(i.pre,{children:(0,r.jsx)(i.code,{className:"language-csharp",children:"private TimerExecutionToken _token;\n\n// Start a cancellable repeating timer\nTimer.StartTimer(\n TimeSpan.FromSeconds(5),\n TimeSpan.FromSeconds(5),\n DoWork,\n out _token\n);\n\n// Cancel the timer (safe to call multiple times)\n_token.Cancel();\n"})}),"\n",(0,r.jsx)(i.h3,{id:"token-properties",children:"Token Properties"}),"\n",(0,r.jsxs)(i.table,{children:[(0,r.jsx)(i.thead,{children:(0,r.jsxs)(i.tr,{children:[(0,r.jsx)(i.th,{style:{textAlign:"left"},children:"Property"}),(0,r.jsx)(i.th,{style:{textAlign:"left"},children:"Type"}),(0,r.jsx)(i.th,{style:{textAlign:"left"},children:"Description"})]})}),(0,r.jsxs)(i.tbody,{children:[(0,r.jsxs)(i.tr,{children:[(0,r.jsx)(i.td,{style:{textAlign:"left"},children:(0,r.jsx)(i.code,{children:"Running"})}),(0,r.jsx)(i.td,{style:{textAlign:"left"},children:(0,r.jsx)(i.code,{children:"bool"})}),(0,r.jsx)(i.td,{style:{textAlign:"left"},children:"Whether the timer is still active"})]}),(0,r.jsxs)(i.tr,{children:[(0,r.jsx)(i.td,{style:{textAlign:"left"},children:(0,r.jsx)(i.code,{children:"RemainingCount"})}),(0,r.jsx)(i.td,{style:{textAlign:"left"},children:(0,r.jsx)(i.code,{children:"int"})}),(0,r.jsxs)(i.td,{style:{textAlign:"left"},children:["Ticks remaining (",(0,r.jsx)(i.code,{children:"int.MaxValue"})," if infinite)"]})]}),(0,r.jsxs)(i.tr,{children:[(0,r.jsx)(i.td,{style:{textAlign:"left"},children:(0,r.jsx)(i.code,{children:"Next"})}),(0,r.jsx)(i.td,{style:{textAlign:"left"},children:(0,r.jsx)(i.code,{children:"DateTime"})}),(0,r.jsx)(i.td,{style:{textAlign:"left"},children:"When the next tick fires"})]}),(0,r.jsxs)(i.tr,{children:[(0,r.jsx)(i.td,{style:{textAlign:"left"},children:(0,r.jsx)(i.code,{children:"Index"})}),(0,r.jsx)(i.td,{style:{textAlign:"left"},children:(0,r.jsx)(i.code,{children:"int"})}),(0,r.jsxs)(i.td,{style:{textAlign:"left"},children:["How many times ",(0,r.jsx)(i.code,{children:"OnTick"})," has fired so far"]})]})]})]}),"\n",(0,r.jsx)(i.h3,{id:"lifecycle-pattern",children:"Lifecycle Pattern"}),"\n",(0,r.jsx)(i.p,{children:"Always cancel tokens when the owning entity is deleted:"}),"\n",(0,r.jsx)(i.pre,{children:(0,r.jsx)(i.code,{className:"language-csharp",children:"private TimerExecutionToken _checkTimer;\n\n[Constructible]\npublic MyItem() : base(0x1234)\n{\n Timer.StartTimer(TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(5), Check, out _checkTimer);\n}\n\npublic override void OnAfterDelete()\n{\n _checkTimer.Cancel();\n base.OnAfterDelete();\n}\n"})}),"\n",(0,r.jsx)(i.admonition,{type:"warning",children:(0,r.jsxs)(i.p,{children:[(0,r.jsx)(i.code,{children:"TimerExecutionToken"})," is ",(0,r.jsx)(i.strong,{children:"not serializable"}),". Never add ",(0,r.jsx)(i.code,{children:"[SerializableField]"})," to a token. Restore timers in ",(0,r.jsx)(i.code,{children:"[AfterDeserialization]"})," instead."]})}),"\n",(0,r.jsx)(i.hr,{}),"\n",(0,r.jsx)(i.h2,{id:"timerdelaycall-legacy",children:"Timer.DelayCall (Legacy)"}),"\n",(0,r.jsxs)(i.p,{children:["Returns a ",(0,r.jsx)(i.code,{children:"Timer"})," object directly. Useful when you need state parameters to avoid lambda allocation:"]}),"\n",(0,r.jsx)(i.pre,{children:(0,r.jsx)(i.code,{className:"language-csharp",children:"// Basic delay call\nvar timer = Timer.DelayCall(TimeSpan.FromSeconds(5), DoWork);\ntimer.Stop(); // Cancel\n\n// With state parameters (no closure allocation)\nTimer.DelayCall(TimeSpan.FromSeconds(2), ProcessTarget, mobile, item);\n\n// Supports up to 5 state parameters\nTimer.DelayCall(TimeSpan.FromSeconds(1), DoWork, arg1, arg2, arg3);\n"})}),"\n",(0,r.jsx)(i.h3,{id:"when-to-use-delaycall",children:"When to Use DelayCall"}),"\n",(0,r.jsxs)(i.p,{children:["Prefer ",(0,r.jsx)(i.code,{children:"Timer.StartTimer"})," for most cases. Use ",(0,r.jsx)(i.code,{children:"Timer.DelayCall"})," when:"]}),"\n",(0,r.jsxs)(i.ul,{children:["\n",(0,r.jsx)(i.li,{children:"You need to pass state parameters to avoid lambda/closure allocation on hot paths."}),"\n",(0,r.jsxs)(i.li,{children:["You need the ",(0,r.jsx)(i.code,{children:"Timer"})," object reference for advanced control."]}),"\n"]}),"\n",(0,r.jsx)(i.hr,{}),"\n",(0,r.jsx)(i.h2,{id:"timer-restoration-after-deserialization",children:"Timer Restoration After Deserialization"}),"\n",(0,r.jsx)(i.p,{children:"Timers do not survive server restarts. Save the relevant timing data as a serialized field, then restart the timer after the world loads."}),"\n",(0,r.jsx)(i.h3,{id:"pattern-save-expiration-time",children:"Pattern: Save Expiration Time"}),"\n",(0,r.jsx)(i.pre,{children:(0,r.jsx)(i.code,{className:"language-csharp",children:"[SerializationGenerator(0)]\npublic partial class TimedItem : Item\n{\n private TimerExecutionToken _timer; // NOT serialized\n\n [SerializableField(0)]\n [DeltaDateTime]\n private DateTime _expireTime;\n\n [Constructible]\n public TimedItem() : base(0x1234)\n {\n _expireTime = Core.Now + TimeSpan.FromHours(1);\n StartTimer();\n }\n\n private void StartTimer()\n {\n Timer.StartTimer(TimeSpan.FromMinutes(1), TimeSpan.FromMinutes(1), Check, out _timer);\n }\n\n [AfterDeserialization]\n private void AfterDeserialization() => StartTimer();\n\n public override void OnAfterDelete()\n {\n _timer.Cancel();\n base.OnAfterDelete();\n }\n\n private void Check()\n {\n if (Core.Now >= _expireTime)\n {\n Delete();\n }\n }\n}\n"})}),"\n",(0,r.jsx)(i.p,{children:"Key points:"}),"\n",(0,r.jsxs)(i.ul,{children:["\n",(0,r.jsxs)(i.li,{children:[(0,r.jsx)(i.strong,{children:(0,r.jsx)(i.code,{children:"[DeltaDateTime]"})})," stores the time as an offset from ",(0,r.jsx)(i.code,{children:"Core.Now"}),", so it adjusts correctly if the server is down for a while."]}),"\n",(0,r.jsxs)(i.li,{children:[(0,r.jsx)(i.strong,{children:(0,r.jsx)(i.code,{children:"[AfterDeserialization]"})})," runs after the entity's fields are loaded -- this is where you restart timers."]}),"\n",(0,r.jsxs)(i.li,{children:["The ",(0,r.jsx)(i.code,{children:"TimerExecutionToken"})," field has no serialization attribute."]}),"\n"]}),"\n",(0,r.jsx)(i.h3,{id:"pattern-timer-field-with-deserializetimerfield",children:"Pattern: Timer Field with DeserializeTimerField"}),"\n",(0,r.jsxs)(i.p,{children:["For ",(0,r.jsx)(i.code,{children:"Timer"})," objects (not tokens), use ",(0,r.jsx)(i.code,{children:"[DeserializeTimerField]"}),":"]}),"\n",(0,r.jsx)(i.pre,{children:(0,r.jsx)(i.code,{className:"language-csharp",children:'[SerializableField(0, setter: "private")]\nprivate Timer _decayTimer;\n\n[DeserializeTimerField(0)]\nprivate void DeserializeDecayTimer(TimeSpan delay)\n{\n _decayTimer = Timer.DelayCall(delay, Delete);\n _decayTimer.Start();\n}\n\npublic override void OnAfterDelete()\n{\n _decayTimer?.Stop();\n _decayTimer = null;\n base.OnAfterDelete();\n}\n'})}),"\n",(0,r.jsx)(i.p,{children:"The serialization system saves the remaining delay and passes it to your deserialize method."}),"\n",(0,r.jsx)(i.hr,{}),"\n",(0,r.jsx)(i.h2,{id:"common-mistakes",children:"Common Mistakes"}),"\n",(0,r.jsxs)(i.table,{children:[(0,r.jsx)(i.thead,{children:(0,r.jsxs)(i.tr,{children:[(0,r.jsx)(i.th,{style:{textAlign:"left"},children:"Mistake"}),(0,r.jsx)(i.th,{style:{textAlign:"left"},children:"Problem"}),(0,r.jsx)(i.th,{style:{textAlign:"left"},children:"Fix"})]})}),(0,r.jsxs)(i.tbody,{children:[(0,r.jsxs)(i.tr,{children:[(0,r.jsxs)(i.td,{style:{textAlign:"left"},children:["Adding ",(0,r.jsx)(i.code,{children:"[SerializableField]"})," to ",(0,r.jsx)(i.code,{children:"TimerExecutionToken"})]}),(0,r.jsx)(i.td,{style:{textAlign:"left"},children:"Build error or data corruption"}),(0,r.jsxs)(i.td,{style:{textAlign:"left"},children:["Leave unserialized; use ",(0,r.jsx)(i.code,{children:"[AfterDeserialization]"})," to restart"]})]}),(0,r.jsxs)(i.tr,{children:[(0,r.jsx)(i.td,{style:{textAlign:"left"},children:"Not cancelling timer on delete"}),(0,r.jsx)(i.td,{style:{textAlign:"left"},children:"Timer fires on a deleted entity, causing errors"}),(0,r.jsxs)(i.td,{style:{textAlign:"left"},children:["Cancel in ",(0,r.jsx)(i.code,{children:"OnAfterDelete()"})]})]}),(0,r.jsxs)(i.tr,{children:[(0,r.jsxs)(i.td,{style:{textAlign:"left"},children:["Using ",(0,r.jsx)(i.code,{children:"Thread.Sleep"})]}),(0,r.jsx)(i.td,{style:{textAlign:"left"},children:"Blocks the entire game loop"}),(0,r.jsxs)(i.td,{style:{textAlign:"left"},children:["Use ",(0,r.jsx)(i.code,{children:"await Timer.Pause()"})]})]}),(0,r.jsxs)(i.tr,{children:[(0,r.jsx)(i.td,{style:{textAlign:"left"},children:"Creating timer inside deserialization"}),(0,r.jsx)(i.td,{style:{textAlign:"left"},children:"Timer starts before the world is fully loaded"}),(0,r.jsxs)(i.td,{style:{textAlign:"left"},children:["Use ",(0,r.jsx)(i.code,{children:"[AfterDeserialization]"})]})]}),(0,r.jsxs)(i.tr,{children:[(0,r.jsx)(i.td,{style:{textAlign:"left"},children:"Lambda capturing state in hot-path timer"}),(0,r.jsx)(i.td,{style:{textAlign:"left"},children:"Allocates a closure object every invocation"}),(0,r.jsxs)(i.td,{style:{textAlign:"left"},children:["Use ",(0,r.jsx)(i.code,{children:"Timer.DelayCall"})," with state parameters"]})]})]})]}),"\n",(0,r.jsx)(i.h3,{id:"avoiding-lambda-allocation",children:"Avoiding Lambda Allocation"}),"\n",(0,r.jsx)(i.pre,{children:(0,r.jsx)(i.code,{className:"language-csharp",children:"// BAD on hot paths -- allocates a closure each time\nTimer.StartTimer(TimeSpan.FromSeconds(2), () => ProcessTarget(from, target));\n\n// GOOD -- state parameters, no allocation\nTimer.DelayCall(TimeSpan.FromSeconds(2), ProcessTarget, from, target);\n\nprivate static void ProcessTarget(Mobile from, Mobile target)\n{\n // Process...\n}\n"})}),"\n",(0,r.jsx)(i.hr,{}),"\n",(0,r.jsx)(i.h2,{id:"quick-reference",children:"Quick Reference"}),"\n",(0,r.jsx)(i.h3,{id:"fire-and-forget",children:"Fire-and-Forget"}),"\n",(0,r.jsx)(i.pre,{children:(0,r.jsx)(i.code,{className:"language-csharp",children:"// One-shot after 10 seconds\nTimer.StartTimer(TimeSpan.FromSeconds(10), Delete);\n"})}),"\n",(0,r.jsx)(i.h3,{id:"cancellable-repeating-timer",children:"Cancellable Repeating Timer"}),"\n",(0,r.jsx)(i.pre,{children:(0,r.jsx)(i.code,{className:"language-csharp",children:"private TimerExecutionToken _token;\n\nTimer.StartTimer(TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(5), Tick, out _token);\n\n// Later:\n_token.Cancel();\n"})}),"\n",(0,r.jsx)(i.h3,{id:"awaitable-pause",children:"Awaitable Pause"}),"\n",(0,r.jsx)(i.pre,{children:(0,r.jsx)(i.code,{className:"language-csharp",children:"await Timer.Pause(TimeSpan.FromMilliseconds(100));\nawait Timer.Pause(500); // Milliseconds overload\n"})}),"\n",(0,r.jsxs)(i.p,{children:["This is safe because ",(0,r.jsx)(i.code,{children:"EventLoopContext"})," routes continuations back to the main thread."]})]})}function h(e={}){const{wrapper:i}={...(0,l.R)(),...e.components};return i?(0,r.jsx)(i,{...e,children:(0,r.jsx)(o,{...e})}):o(e)}}}]);