Skip to main content

samp_sdk/omp/
timers.rs

1//! Bindings for the Open Multiplayer `ITimersComponent` interface.
2//!
3//! Open Multiplayer has no native `ProcessTick`-equivalent callback for
4//! components. To deliver the SDK's unified [`SampPlugin::on_tick`] on this
5//! server, the `samp` crate installs a repeating timer through this
6//! interface in `on_ready` and routes its timeout into the plugin.
7//!
8//! The interval is whatever the plugin requested via
9//! `samp::plugin::enable_tick_with(TickConfig::new().omp_interval(...))`,
10//! or 5 ms by default (from `enable_tick()`).
11//!
12//! [`SampPlugin::on_tick`]: ../../../samp/plugin/trait.SampPlugin.html#method.on_tick
13//!
14//! ## Primary `ITimersComponent` vtable (19 slots — confirmed via disasm of `Timers.dll`)
15//!
16//! Slots `[0..15]` are inherited from `IComponent`. New slots:
17//! - **[16]** `create(handler*, Milliseconds interval, bool repeating)` -> `ITimer*`
18//! - **[17]** `create(handler*, Milliseconds initial, Milliseconds interval, unsigned count)` -> `ITimer*`
19//! - **[18]** `count() const` -> `size_t`
20//!
21//! ## `ITimer` vtable (slots starting from `IExtensible`)
22//!
23//! - **[0..3]** `IExtensible` (`getExtension`, `addExtension`, `removeExtension`x2)
24//! - **[4]** destructor (1 slot MSVC / 2 slots Itanium)
25//! - **[5]** `running()` const
26//! - **[6]** `remaining()` const -> Milliseconds (8 bytes, hidden ptr)
27//! - **[7]** `calls()` const
28//! - **[8]** `interval()` const -> Milliseconds (8 bytes, hidden ptr)
29//! - **[9]** `trigger()`
30//! - **[10]** `kill()`
31//! - **[11]** `handler() const`
32//!
33//! ## `TimerTimeOutHandler` vtable (interface provided by the plugin)
34//!
35//! No virtual destructor in the header -> 2 slots only:
36//! - **[0]** `timeout(ITimer&)`
37//! - **[1]** `free(ITimer&)`
38
39use super::component_api::OmpComponentHandle;
40use super::server::{ServerComponent, query_component};
41use super::types::UID;
42use std::ptr::NonNull;
43
44/// UID of the Open Multiplayer `Timers` component.
45pub const TIMERS_COMPONENT_UID: UID = 0x2ad8_124c_5ea2_57a3;
46
47/// Slot of `create(handler, interval, repeating)` in the `ITimersComponent` vtable.
48const SLOT_CREATE_INTERVAL: usize = 16;
49
50/// Slot of `kill()` in the `ITimer` vtable.
51const SLOT_TIMER_KILL: usize = 10;
52
53/// Opaque pointer to the server's `ITimersComponent`.
54#[repr(C)]
55pub struct ITimersComponent {
56    _opaque: [u8; 0],
57}
58
59/// Opaque pointer to the server's `ITimer` — returned by `create_timer`.
60#[repr(C)]
61pub struct ITimer {
62    _opaque: [u8; 0],
63}
64
65/// `TimerTimeOutHandler` vtable — Itanium ABI.
66#[cfg(not(target_env = "msvc"))]
67#[repr(C)]
68pub struct TimerHandlerVTable {
69    pub timeout: unsafe extern "C" fn(*mut TimerTimeOutHandler, *mut ITimer),
70    pub free: unsafe extern "C" fn(*mut TimerTimeOutHandler, *mut ITimer),
71}
72
73/// `TimerTimeOutHandler` vtable — MSVC ABI (`this` in ECX).
74#[cfg(target_env = "msvc")]
75#[repr(C)]
76pub struct TimerHandlerVTable {
77    pub timeout: unsafe extern "thiscall" fn(*mut TimerTimeOutHandler, *mut ITimer),
78    pub free: unsafe extern "thiscall" fn(*mut TimerTimeOutHandler, *mut ITimer),
79}
80
81/// Object the server will invoke on each timer timeout.
82///
83/// `#[repr(C)]` layout: vtable pointer at offset 0 + the handler's own data.
84/// The server treats it as an opaque `TimerTimeOutHandler*` and only interacts
85/// via the vtable.
86#[repr(C)]
87pub struct TimerTimeOutHandler {
88    pub vtable: *const TimerHandlerVTable,
89}
90
91unsafe impl Send for TimerTimeOutHandler {}
92unsafe impl Sync for TimerTimeOutHandler {}
93
94/// Signature of `ITimersComponent::create(handler, interval, repeating)`.
95///
96/// `Milliseconds` is `std::chrono::milliseconds` in C++, a wrapper over `int64_t`.
97/// At the ABI it is passed as 8 bytes on the stack (or hidden in registers,
98/// depending on the compiler).
99#[cfg(not(target_env = "msvc"))]
100type CreateFn = unsafe extern "C" fn(
101    this: *mut ITimersComponent,
102    handler: *mut TimerTimeOutHandler,
103    interval_ms: i64,
104    repeating: bool,
105) -> *mut ITimer;
106
107#[cfg(target_env = "msvc")]
108type CreateFn = unsafe extern "thiscall" fn(
109    this: *mut ITimersComponent,
110    handler: *mut TimerTimeOutHandler,
111    interval_ms: i64,
112    repeating: bool,
113) -> *mut ITimer;
114
115#[cfg(not(target_env = "msvc"))]
116type KillFn = unsafe extern "C" fn(this: *mut ITimer);
117
118#[cfg(target_env = "msvc")]
119type KillFn = unsafe extern "thiscall" fn(this: *mut ITimer);
120
121/// Queries `ITimersComponent` in the server's component list.
122///
123/// # Safety
124/// `core` must point to a valid `ICore`. Internally uses `query_component`,
125/// which casts the `ServerComponent` from the list — follows its contract.
126pub unsafe fn query_timers_component(
127    components: *mut super::server::ServerComponentList,
128) -> *mut ITimersComponent {
129    if components.is_null() {
130        return std::ptr::null_mut();
131    }
132    let raw = unsafe { query_component(components, TIMERS_COMPONENT_UID) };
133    raw.cast::<ITimersComponent>()
134}
135
136/// Creates a repeating timer on the Open Multiplayer server.
137///
138/// Returns the server's `ITimer*` (non-owning — the server owns it). Use
139/// [`kill_timer`] at shutdown to stop it and free the server's resources.
140///
141/// # Safety
142/// - `timers` must be a valid `ITimersComponent` pointer (from `query_timers_component`)
143/// - `handler` must remain alive while the timer is active (allocate on the heap via `Box::into_raw`)
144pub unsafe fn create_repeating_timer(
145    timers: *mut ITimersComponent,
146    handler: *mut TimerTimeOutHandler,
147    interval_ms: i64,
148) -> *mut ITimer {
149    if handler.is_null() {
150        return std::ptr::null_mut();
151    }
152    let Some((_, slot)) = (unsafe {
153        super::vtable::secondary_call_target(timers.cast::<u8>(), 0, SLOT_CREATE_INTERVAL)
154    }) else {
155        return std::ptr::null_mut();
156    };
157    let create: CreateFn = unsafe { std::mem::transmute(slot) };
158    unsafe { create(timers, handler, interval_ms, true) }
159}
160
161// ---------------------------------------------------------------------------
162// TimersComponent — high-level typed wrapper
163// ---------------------------------------------------------------------------
164
165/// Typed wrapper for the Open Multiplayer server's `ITimersComponent`.
166///
167/// Obtained via `samp::plugin::omp_query::<TimersComponent>()`. Exposes
168/// `create_repeating` for timer creation and the generic `IComponent` methods
169/// (`name`, `version`).
170#[derive(Debug, Clone, Copy)]
171pub struct TimersComponent {
172    ptr: NonNull<ServerComponent>,
173}
174
175impl OmpComponentHandle for TimersComponent {
176    const UID: UID = TIMERS_COMPONENT_UID;
177
178    unsafe fn from_raw(ptr: NonNull<ServerComponent>) -> Self {
179        Self { ptr }
180    }
181
182    fn as_raw(&self) -> NonNull<ServerComponent> {
183        self.ptr
184    }
185}
186
187impl TimersComponent {
188    /// Returns the component name.
189    #[must_use]
190    pub fn name(&self) -> Option<String> {
191        super::component_api::component_name(self)
192    }
193
194    /// Returns the component version.
195    #[must_use]
196    pub fn version(&self) -> Option<super::types::SemanticVersion> {
197        super::component_api::component_version(self)
198    }
199
200    /// Creates a repeating timer on the server.
201    ///
202    /// `handler` must be heap-allocated (e.g. `Box::into_raw`) and must be
203    /// dropped inside the `TimerHandlerVTable::free` callback.
204    ///
205    /// # Safety
206    /// `handler` must point to a live [`TimerTimeOutHandler`] while the timer
207    /// is active.
208    pub unsafe fn create_repeating(
209        &self,
210        handler: *mut TimerTimeOutHandler,
211        interval_ms: i64,
212    ) -> *mut ITimer {
213        unsafe {
214            create_repeating_timer(
215                self.ptr.as_ptr().cast::<ITimersComponent>(),
216                handler,
217                interval_ms,
218            )
219        }
220    }
221}
222
223/// Kills an active timer, stopping future fires.
224///
225/// After `kill`, the server calls `TimerTimeOutHandler::free(timer)` allowing
226/// the heap-allocated handler to be released. Without it, the handler leaks.
227///
228/// # Safety
229/// `timer` must be a valid pointer returned by `create_repeating_timer`.
230pub unsafe fn kill_timer(timer: *mut ITimer) {
231    let Some((_, slot)) =
232        (unsafe { super::vtable::secondary_call_target(timer.cast::<u8>(), 0, SLOT_TIMER_KILL) })
233    else {
234        return;
235    };
236    let kill: KillFn = unsafe { std::mem::transmute(slot) };
237    unsafe { kill(timer) };
238}
239
240#[cfg(test)]
241mod tests {
242    //! Tests for the `timers` module.
243    //!
244    //! Cover: UID constant, `TimerTimeOutHandler` layout, defensive behavior
245    //! of `create_repeating_timer` and `kill_timer` against null or invalid
246    //! inputs.
247
248    use super::*;
249
250    #[test]
251    fn timers_component_uid_is_known_value() {
252        // Value declared in `timers.hpp:44` of the Open Multiplayer SDK.
253        assert_eq!(TIMERS_COMPONENT_UID, 0x2ad8_124c_5ea2_57a3);
254    }
255
256    #[test]
257    fn timers_component_uid_via_trait() {
258        assert_eq!(
259            <TimersComponent as OmpComponentHandle>::UID,
260            TIMERS_COMPONENT_UID
261        );
262    }
263
264    #[test]
265    fn timer_handler_has_vtable_at_offset_zero() {
266        // The server reads the vtable at offset 0 of the handler — confirm layout.
267        assert_eq!(std::mem::offset_of!(TimerTimeOutHandler, vtable), 0);
268    }
269
270    #[test]
271    fn timer_handler_size_is_one_pointer() {
272        // No own data: only the vtable pointer.
273        assert_eq!(
274            std::mem::size_of::<TimerTimeOutHandler>(),
275            std::mem::size_of::<*const ()>()
276        );
277    }
278
279    #[test]
280    fn timer_handler_vtable_has_two_slots() {
281        // IUIDProvider does not declare a destructor -> 2 slots (timeout, free).
282        assert_eq!(
283            std::mem::size_of::<TimerHandlerVTable>(),
284            2 * std::mem::size_of::<*const ()>()
285        );
286    }
287
288    #[test]
289    fn create_repeating_timer_returns_null_when_handler_is_null() {
290        let timers = std::ptr::null_mut::<ITimersComponent>();
291        let ret = unsafe { create_repeating_timer(timers, std::ptr::null_mut(), 5) };
292        assert!(ret.is_null());
293    }
294
295    #[test]
296    fn create_repeating_timer_returns_null_when_component_is_null() {
297        // Dummy handler: since timers is null, it should not even try to deref the handler.
298        let fake_handler = std::ptr::dangling_mut::<TimerTimeOutHandler>();
299        let ret = unsafe { create_repeating_timer(std::ptr::null_mut(), fake_handler, 5) };
300        assert!(ret.is_null());
301    }
302
303    #[test]
304    fn kill_timer_is_noop_for_null_pointer() {
305        // Must not panic or segfault.
306        unsafe { kill_timer(std::ptr::null_mut()) };
307    }
308
309    #[test]
310    fn query_timers_component_returns_null_for_null_list() {
311        let ret = unsafe { query_timers_component(std::ptr::null_mut()) };
312        assert!(ret.is_null());
313    }
314}