samp_sdk/omp/server.rs
1//! Vtables for objects managed by the Open Multiplayer server.
2//!
3//! The vtable indices and signatures were derived from the public
4//! specification of the Open Multiplayer SDK (<https://github.com/openmultiplayer/open.mp-sdk>).
5//! No SDK code was copied.
6//!
7//! Unlike `component.rs` (where WE implement the vtable), here we define
8//! vtables for objects created by the SERVER so we can call methods on
9//! them from Rust.
10//!
11//! ## Indices per ABI
12//!
13//! **Itanium ABI** — two destructor slots (D1 + D0) interleaved after the
14//! virtuals of each base class, in declaration order.
15//!
16//! **MSVC ABI** — a single destructor (scalar deleting) at the end of the
17//! virtuals of the class that declared it (e.g. `~IExtensible` at slot [4],
18//! after `removeExtension`).
19//!
20//! The practical difference is that on MSVC the vtable is 1 slot smaller per
21//! destructor (no separate D0 slot), which shifts all subsequent methods.
22
23use crate::raw::types::AMX;
24
25use super::events::PawnEventHandler;
26use super::types::UID;
27
28/// Number of AMX functions exported by `IPawnComponent` (`NUM_AMX_FUNCS` in the SDK).
29pub const NUM_AMX_FUNCS: usize = 52;
30
31/// UID of the Open Multiplayer Pawn component (`PawnComponent_UID` in the SDK).
32pub const PAWN_COMPONENT_UID: UID = 0x7890_6cd9_f19c_36a6;
33
34// ---------------------------------------------------------------------------
35// IComponentList — list of loaded components (server-owned object)
36// ---------------------------------------------------------------------------
37//
38// Inheritance: IComponentList : public IExtensible
39//
40// Primary vtable (Itanium ABI):
41// [0-3] IExtensible (get/add/remove/remove)
42// [4] ~destructor D1
43// [5] ~destructor D0
44// [6] IComponentList::queryComponent(UID) -> IComponent*
45//
46// Primary vtable (MSVC ABI):
47// [0-3] IExtensible (get/add/remove/remove)
48// [4] ~destructor (single scalar deleting)
49// [5] IComponentList::queryComponent(UID) -> IComponent*
50
51/// Opaque handle for the server's `IComponentList*`.
52#[repr(C)]
53pub struct ServerComponentList {
54 vtable: *const ServerComponentListVTable,
55}
56
57// IComponentList vtable layout — only the count of opaque destructor slots
58// differs (Itanium: D1+D0 = 2 slots; MSVC: single scalar deleting = 1 slot).
59// The calling convention also differs (Itanium "C" vs MSVC "thiscall").
60//
61// Opaque slots (in order):
62// [0] IExtensible::getExtension
63// [1] IExtensible::addExtension
64// [2] IExtensible::removeExtension(ptr)
65// [3] IExtensible::removeExtension(uid)
66// [4] ~destructor (D1 on Itanium, scalar deleting on MSVC)
67// [5] ~destructor D0 (Itanium-only — does not exist on MSVC)
68//
69// Useful slot:
70// [6 Itanium / 5 MSVC] queryComponent
71#[cfg(not(target_env = "msvc"))]
72type QueryComponentFn = unsafe extern "C" fn(*mut ServerComponentList, UID) -> *mut ServerComponent;
73#[cfg(target_env = "msvc")]
74type QueryComponentFn =
75 unsafe extern "thiscall" fn(*mut ServerComponentList, UID) -> *mut ServerComponent;
76
77#[cfg(not(target_env = "msvc"))]
78const COMPONENT_LIST_PREFIX_SLOTS: usize = 6;
79#[cfg(target_env = "msvc")]
80const COMPONENT_LIST_PREFIX_SLOTS: usize = 5;
81
82#[repr(C)]
83struct ServerComponentListVTable {
84 _prefix: [*const (); COMPONENT_LIST_PREFIX_SLOTS],
85 query_component: QueryComponentFn,
86}
87
88/// Opaque handle for the `IComponent*` returned by queryComponent.
89#[repr(C)]
90pub struct ServerComponent {
91 vtable: *const (),
92}
93
94/// Queries a component by UID in the list provided by the server.
95///
96/// # Safety
97/// `list` must be a valid pointer to an Open Multiplayer server `IComponentList`.
98pub unsafe fn query_component(list: *mut ServerComponentList, uid: UID) -> *mut ServerComponent {
99 unsafe { ((*(*list).vtable).query_component)(list, uid) }
100}
101
102// ---------------------------------------------------------------------------
103// IPawnComponent — access to the PAWN/AMX subsystem (server-owned object)
104// ---------------------------------------------------------------------------
105//
106// Inheritance: IPawnComponent : public IComponent : public IExtensible, IUIDProvider
107//
108// Primary vtable (Itanium ABI) — confirmed by runtime dump (Open Multiplayer 1.5.8):
109// [0-4] IExtensible + server-internal slots
110// [5] ~PawnComponent D1
111// [6] ~PawnComponent D0 (deleting)
112// [7] componentName
113// [8] (unknown)
114// [9] componentVersion
115// [10] onLoad
116// [11] (unknown)
117// [12] onReady
118// [13] onFree
119// [14] (unknown)
120// [15] free
121// [16] reset
122// [17] (unknown)
123// [18] IPawnComponent::getEventDispatcher <- confirmed at runtime
124// [19] IPawnComponent::getAmxFunctions <- confirmed at runtime
125//
126// Primary vtable (MSVC ABI):
127// [0-3] IExtensible (get/add/remove/remove)
128// [4] ~destructor (single scalar deleting)
129// [5-15] IComponent (supportedVersion..reset)
130// [16] IPawnComponent::getEventDispatcher
131// [17] IPawnComponent::getAmxFunctions
132
133/// Opaque handle for the server's `IPawnComponent*`.
134#[repr(C)]
135pub struct ServerPawnComponent {
136 vtable: *const ServerPawnComponentVTable,
137 // IUIDProvider secondary vtable — we do not access it directly
138 _uid_vtable: *const (),
139}
140
141// IPawnComponent vtable layout — useful slots ([18-19] Itanium / [16-17] MSVC)
142// and shared trailing opaques. The prefix difference comes from how each ABI
143// emits destructors (Itanium D1+D0 + unknown slots confirmed in the dump).
144//
145// Opaque prefix slots (Itanium ABI, 18 slots — confirmed by runtime dump):
146// [0-4] IExtensible (4 methods + 1 unknown slot)
147// [5] ~PawnComponent D1
148// [6] ~PawnComponent D0 (deleting)
149// [7] componentName
150// [8] (unknown)
151// [9] componentVersion
152// [10] onLoad
153// [11] (unknown)
154// [12] onReady
155// [13] onFree
156// [14] (unknown)
157// [15] free
158// [16] reset
159// [17] (unknown)
160//
161// Opaque prefix slots (MSVC ABI, 16 slots):
162// [0-3] IExtensible (get/add/removeExt(ptr)/removeExt(uid))
163// [4] ~destructor (single scalar deleting)
164// [5-15] IComponent (supportedVersion, componentName, componentType,
165// componentVersion, onLoad, onInit, onReady, onFree,
166// provideConfiguration, free, reset)
167//
168// Useful slots (in both):
169// [18-19 Itanium / 16-17 MSVC] getEventDispatcher, getAmxFunctions
170//
171// Trailing opaques (4 slots, identical in both ABIs):
172// getScript(const), getScript(mut), mainScript, sideScripts
173#[cfg(not(target_env = "msvc"))]
174type GetEventDispatcherFn =
175 unsafe extern "C" fn(*mut ServerPawnComponent) -> *mut IEventDispatcherPawn;
176#[cfg(target_env = "msvc")]
177type GetEventDispatcherFn =
178 unsafe extern "thiscall" fn(*mut ServerPawnComponent) -> *mut IEventDispatcherPawn;
179
180#[cfg(not(target_env = "msvc"))]
181type GetAmxFunctionsFn =
182 unsafe extern "C" fn(*const ServerPawnComponent) -> *const AmxFunctionTable;
183#[cfg(target_env = "msvc")]
184type GetAmxFunctionsFn =
185 unsafe extern "thiscall" fn(*const ServerPawnComponent) -> *const AmxFunctionTable;
186
187#[cfg(not(target_env = "msvc"))]
188const PAWN_COMPONENT_PREFIX_SLOTS: usize = 18;
189#[cfg(target_env = "msvc")]
190const PAWN_COMPONENT_PREFIX_SLOTS: usize = 16;
191
192#[repr(C)]
193struct ServerPawnComponentVTable {
194 _prefix: [*const (); PAWN_COMPONENT_PREFIX_SLOTS],
195 get_event_dispatcher: GetEventDispatcherFn,
196 get_amx_functions: GetAmxFunctionsFn,
197 // Trailing opaques common to both ABIs.
198 _get_script_const: *const (),
199 _get_script_mut: *const (),
200 _main_script: *const (),
201 _side_scripts: *const (),
202}
203
204/// Table of 52 AMX function pointers (`StaticArray<void*, NUM_AMX_FUNCS>`).
205pub type AmxFunctionTable = [*mut (); NUM_AMX_FUNCS];
206
207// ---------------------------------------------------------------------------
208// IPawnScript — opaque handle; we only use GetAMX() at index [57]
209// ---------------------------------------------------------------------------
210//
211// IPawnScript does not declare a virtual destructor.
212// Methods [0..56] are opaque; [57] is GetAMX().
213// Index 57 is identical on Itanium and MSVC (no virtual destructor = no shift).
214
215/// Opaque handle for the server's `IPawnScript*`.
216#[repr(C)]
217pub struct IPawnScript {
218 vtable: *const IPawnScriptVTable,
219}
220
221// IPawnScript does not declare a virtual destructor — identical layout on
222// Itanium and MSVC. Slot [57] is GetAMX(); only the calling convention differs.
223#[cfg(not(target_env = "msvc"))]
224type GetAmxFn = unsafe extern "C" fn(*mut IPawnScript) -> *mut AMX;
225#[cfg(target_env = "msvc")]
226type GetAmxFn = unsafe extern "thiscall" fn(*mut IPawnScript) -> *mut AMX;
227
228#[repr(C)]
229struct IPawnScriptVTable {
230 _prefix: [*const (); 57],
231 get_amx: GetAmxFn,
232}
233
234/// Extracts the `AMX*` pointer from an `IPawnScript*`.
235///
236/// # Safety
237/// `script` must be a valid pointer to an Open Multiplayer server `IPawnScript`.
238pub unsafe fn get_amx_from_script(script: *mut IPawnScript) -> *mut AMX {
239 unsafe { ((*(*script).vtable).get_amx)(script) }
240}
241
242// ---------------------------------------------------------------------------
243// IEventDispatcher<PawnEventHandler> — server-side vtable
244// ---------------------------------------------------------------------------
245//
246// IEventDispatcher<T> does not declare a virtual destructor.
247// Vtable:
248// [0] addEventHandler(handler*, priority: i8) -> bool
249// [1] removeEventHandler(handler*) -> bool
250// [2] hasEventHandler (unused)
251// [3] count (unused)
252//
253// No virtual destructor = no shift between Itanium and MSVC.
254// Only the calling convention differs.
255
256/// Opaque handle for the server's `IEventDispatcher<PawnEventHandler>*`.
257#[repr(C)]
258pub struct IEventDispatcherPawn {
259 vtable: *const IEventDispatcherPawnVTable,
260}
261
262// IEventDispatcher<T> does not declare a virtual destructor — identical layout
263// on both ABIs. Slots [0..3]: addEventHandler, removeEventHandler,
264// hasEventHandler, count. Only the first two are used; only the calling
265// convention differs.
266#[cfg(not(target_env = "msvc"))]
267type AddEventHandlerFn =
268 unsafe extern "C" fn(*mut IEventDispatcherPawn, *mut PawnEventHandler, i8) -> bool;
269#[cfg(target_env = "msvc")]
270type AddEventHandlerFn =
271 unsafe extern "thiscall" fn(*mut IEventDispatcherPawn, *mut PawnEventHandler, i8) -> bool;
272
273#[cfg(not(target_env = "msvc"))]
274type RemoveEventHandlerFn =
275 unsafe extern "C" fn(*mut IEventDispatcherPawn, *mut PawnEventHandler) -> bool;
276#[cfg(target_env = "msvc")]
277type RemoveEventHandlerFn =
278 unsafe extern "thiscall" fn(*mut IEventDispatcherPawn, *mut PawnEventHandler) -> bool;
279
280#[repr(C)]
281struct IEventDispatcherPawnVTable {
282 add_event_handler: AddEventHandlerFn,
283 remove_event_handler: RemoveEventHandlerFn,
284 _has_event_handler: *const (),
285 _count: *const (),
286}
287
288/// Registers a Pawn event handler in the dispatcher.
289///
290/// # Safety
291/// Both pointers must be valid. `handler` must outlive the dispatcher.
292pub unsafe fn add_pawn_event_handler(
293 dispatcher: *mut IEventDispatcherPawn,
294 handler: *mut PawnEventHandler,
295) {
296 unsafe { ((*(*dispatcher).vtable).add_event_handler)(dispatcher, handler, 0) };
297}
298
299/// Removes a Pawn event handler from the dispatcher.
300///
301/// # Safety
302/// Both pointers must be valid.
303pub unsafe fn remove_pawn_event_handler(
304 dispatcher: *mut IEventDispatcherPawn,
305 handler: *mut PawnEventHandler,
306) {
307 unsafe { ((*(*dispatcher).vtable).remove_event_handler)(dispatcher, handler) };
308}
309
310/// Gets the Pawn event dispatcher from the `IPawnComponent`.
311///
312/// # Safety
313/// `pawn` must be a valid pointer to an Open Multiplayer server `IPawnComponent`.
314pub unsafe fn get_pawn_event_dispatcher(pawn: *mut ServerComponent) -> *mut IEventDispatcherPawn {
315 let pawn = pawn.cast::<ServerPawnComponent>();
316 unsafe { ((*(*pawn).vtable).get_event_dispatcher)(pawn) }
317}
318
319/// Gets the pointer to the AMX function table from the `IPawnComponent`.
320///
321/// # Safety
322/// `pawn` must be a valid pointer to an Open Multiplayer server `IPawnComponent`.
323pub unsafe fn get_amx_functions(pawn: *mut ServerComponent) -> usize {
324 let pawn = pawn as *const ServerPawnComponent;
325 let table_ptr = unsafe { ((*(*pawn).vtable).get_amx_functions)(pawn) };
326 table_ptr as usize
327}
328
329// ---------------------------------------------------------------------------
330// PawnComponent — high-level typed wrapper
331// ---------------------------------------------------------------------------
332
333use super::component_api::OmpComponentHandle;
334use std::ptr::NonNull;
335
336/// Typed wrapper for the Open Multiplayer server's `IPawnComponent`.
337///
338/// Obtained via `samp::plugin::omp_query::<PawnComponent>()`. Exposes the
339/// Pawn-specific methods (event dispatcher, AMX functions) in addition to the
340/// generic `IComponent` ones (`name()`, `version()` via `component_api`).
341#[derive(Debug, Clone, Copy)]
342pub struct PawnComponent {
343 ptr: NonNull<ServerComponent>,
344}
345
346impl OmpComponentHandle for PawnComponent {
347 const UID: UID = PAWN_COMPONENT_UID;
348
349 unsafe fn from_raw(ptr: NonNull<ServerComponent>) -> Self {
350 Self { ptr }
351 }
352
353 fn as_raw(&self) -> NonNull<ServerComponent> {
354 self.ptr
355 }
356}
357
358impl PawnComponent {
359 /// Returns the component name — equivalent to `component_name(&self)`.
360 #[must_use]
361 pub fn name(&self) -> Option<String> {
362 super::component_api::component_name(self)
363 }
364
365 /// Returns the component version — equivalent to `component_version(&self)`.
366 #[must_use]
367 pub fn version(&self) -> Option<super::types::SemanticVersion> {
368 super::component_api::component_version(self)
369 }
370
371 /// Returns the component's `IEventDispatcher<PawnEventHandler>`.
372 ///
373 /// Use it to register AMX event handlers (load/unload).
374 #[must_use]
375 pub fn event_dispatcher(&self) -> *mut IEventDispatcherPawn {
376 unsafe { get_pawn_event_dispatcher(self.ptr.as_ptr()) }
377 }
378
379 /// Returns the AMX function table as `usize` (raw pointer).
380 ///
381 /// Available only after `on_omp_ready` — before that callback,
382 /// `getAmxFunctions()` returns 0 (server behavior).
383 #[must_use]
384 pub fn amx_functions(&self) -> usize {
385 unsafe { get_amx_functions(self.ptr.as_ptr()) }
386 }
387}
388
389#[cfg(test)]
390mod tests {
391 use super::*;
392
393 #[test]
394 fn pawn_component_uid_is_nonzero() {
395 assert_ne!(PAWN_COMPONENT_UID, 0);
396 }
397
398 #[test]
399 fn pawn_component_uid_matches_known_value() {
400 // Value derived from the Open Multiplayer SDK (PawnComponent_UID).
401 // If it changes, the vtable indices and the entire Open Multiplayer integration break.
402 assert_eq!(PAWN_COMPONENT_UID, 0x7890_6cd9_f19c_36a6);
403 }
404
405 #[test]
406 fn num_amx_funcs_is_52() {
407 assert_eq!(NUM_AMX_FUNCS, 52);
408 }
409
410 #[test]
411 fn pawn_component_uid_via_trait_matches_constant() {
412 assert_eq!(
413 <PawnComponent as OmpComponentHandle>::UID,
414 PAWN_COMPONENT_UID
415 );
416 }
417
418 #[test]
419 fn pawn_component_is_copy() {
420 // Sanity: the wrapper should be Copy so it can be used freely in closures.
421 fn assert_copy<T: Copy>() {}
422 assert_copy::<PawnComponent>();
423 }
424
425 #[test]
426 fn pawn_component_size_is_one_pointer() {
427 // Only stores a pointer — no overhead vs `*mut ServerComponent`.
428 assert_eq!(
429 std::mem::size_of::<PawnComponent>(),
430 std::mem::size_of::<*const ()>()
431 );
432 }
433}