Skip to main content

samp_sdk/omp/
world.rs

1//! World entities: objects and pickups.
2//!
3//! Both follow the shape the [`vehicles`](super::vehicles) module established —
4//! query the component by UID, call `create`, and talk to the handle that comes
5//! back. Position comes from the shared `IEntity` subobject, so the accessors
6//! are the same ones a player or a vehicle uses.
7//!
8//! | Method | Itanium | MSVC |
9//! | ------ | :-----: | :--: |
10//! | `IObjectsComponent::create(int, Vector3, Vector3, float)` | 21 | 19 |
11//! | `IPickupsComponent::create(int, PickupType, Vector3, uint32, bool)` | 19 | 17 |
12//!
13//! Slots from `scripts/omp-vtable.py`; both components declare overloads (a
14//! player-scoped `create` among them), so the MSVC column is not simply one
15//! less.
16
17use super::players::{ENTITY_OFFSET, SLOT_ENTITY_GET_ID};
18use super::server::ServerComponent;
19use super::types::{Colour, StringView, UID, Vector2, Vector3};
20use super::vtable::call_vtable;
21
22/// UID of the Open Multiplayer `TextDraws` component.
23pub const TEXTDRAWS_COMPONENT_UID: UID = 0x9b5d_c2b1_d15c_992a;
24
25/// UID of the Open Multiplayer `GangZones` component.
26pub const GANGZONES_COMPONENT_UID: UID = 0xb335_1d11_ee8d_8056;
27
28/// UID of the Open Multiplayer `Actors` component.
29pub const ACTORS_COMPONENT_UID: UID = 0xc81c_a021_eae2_ad5c;
30
31/// UID of the Open Multiplayer `TextLabels` component.
32pub const TEXTLABELS_COMPONENT_UID: UID = 0xa0c5_7ea8_0a00_9742;
33
34/// UID of the Open Multiplayer `Menus` component.
35pub const MENUS_COMPONENT_UID: UID = 0x621e_219e_b97e_e0b2;
36
37/// UID of the Open Multiplayer `Classes` component.
38pub const CLASSES_COMPONENT_UID: UID = 0x8cfb_3183_976d_a208;
39
40/// UID of the Open Multiplayer `Objects` component.
41pub const OBJECTS_COMPONENT_UID: UID = 0x59f8_415f_72da_6160;
42
43/// UID of the Open Multiplayer `Pickups` component.
44pub const PICKUPS_COMPONENT_UID: UID = 0xcf30_4faa_363d_d971;
45
46#[cfg(not(target_env = "msvc"))]
47const SLOT_CREATE_OBJECT: usize = 21;
48#[cfg(target_env = "msvc")]
49const SLOT_CREATE_OBJECT: usize = 19;
50
51#[cfg(not(target_env = "msvc"))]
52const SLOT_CREATE_PICKUP: usize = 19;
53#[cfg(target_env = "msvc")]
54const SLOT_CREATE_PICKUP: usize = 17;
55
56/// `ITextDrawsComponent::create(Vector2, StringView)` — the text overload,
57/// which MSVC emits after the model one ([17] against [18]).
58#[cfg(not(target_env = "msvc"))]
59const SLOT_CREATE_TEXTDRAW: usize = 19;
60#[cfg(target_env = "msvc")]
61const SLOT_CREATE_TEXTDRAW: usize = 18;
62
63#[cfg(not(target_env = "msvc"))]
64const SLOT_CREATE_GANGZONE: usize = 19;
65#[cfg(target_env = "msvc")]
66const SLOT_CREATE_GANGZONE: usize = 17;
67
68#[cfg(not(target_env = "msvc"))]
69const SLOT_CREATE_ACTOR: usize = 19;
70#[cfg(target_env = "msvc")]
71const SLOT_CREATE_ACTOR: usize = 17;
72
73/// `ITextLabelsComponent::create` — the global overload. Three of them share
74/// the name (global, per player, per vehicle), so MSVC emits the set reversed:
75/// [18], [17], [16] against Itanium's [18], [19], [20].
76#[cfg(not(target_env = "msvc"))]
77const SLOT_CREATE_TEXTLABEL: usize = 18;
78#[cfg(target_env = "msvc")]
79const SLOT_CREATE_TEXTLABEL: usize = 18;
80
81#[cfg(not(target_env = "msvc"))]
82const SLOT_CREATE_MENU: usize = 19;
83#[cfg(target_env = "msvc")]
84const SLOT_CREATE_MENU: usize = 17;
85
86#[cfg(not(target_env = "msvc"))]
87const SLOT_CREATE_CLASS: usize = 19;
88#[cfg(target_env = "msvc")]
89const SLOT_CREATE_CLASS: usize = 17;
90
91/// How many weapon slots a spawn class carries (`MAX_WEAPON_SLOTS`).
92pub const MAX_WEAPON_SLOTS: usize = 13;
93
94/// One weapon slot of a spawn class (`WeaponSlotData`).
95#[repr(C)]
96#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
97pub struct WeaponSlot {
98    pub id: u8,
99    pub ammo: u32,
100}
101
102/// Opaque handle for `ITextLabelsComponent*`.
103#[repr(C)]
104pub struct ITextLabelsComponent {
105    _opaque: [u8; 0],
106}
107
108/// Opaque handle for `ITextLabel*`.
109#[repr(C)]
110pub struct ITextLabel {
111    _opaque: [u8; 0],
112}
113
114/// Opaque handle for `IMenusComponent*`.
115#[repr(C)]
116pub struct IMenusComponent {
117    _opaque: [u8; 0],
118}
119
120/// Opaque handle for `IMenu*`.
121#[repr(C)]
122pub struct IMenu {
123    _opaque: [u8; 0],
124}
125
126/// Opaque handle for `IClassesComponent*`.
127#[repr(C)]
128pub struct IClassesComponent {
129    _opaque: [u8; 0],
130}
131
132/// Opaque handle for `IClass*`.
133#[repr(C)]
134pub struct IClass {
135    _opaque: [u8; 0],
136}
137
138/// Opaque handle for `ITextDrawsComponent*`.
139#[repr(C)]
140pub struct ITextDrawsComponent {
141    _opaque: [u8; 0],
142}
143
144/// Opaque handle for `ITextDraw*`.
145#[repr(C)]
146pub struct ITextDraw {
147    _opaque: [u8; 0],
148}
149
150/// Opaque handle for `IGangZonesComponent*`.
151#[repr(C)]
152pub struct IGangZonesComponent {
153    _opaque: [u8; 0],
154}
155
156/// Opaque handle for `IGangZone*`.
157#[repr(C)]
158pub struct IGangZone {
159    _opaque: [u8; 0],
160}
161
162/// Opaque handle for `IActorsComponent*`.
163#[repr(C)]
164pub struct IActorsComponent {
165    _opaque: [u8; 0],
166}
167
168/// Opaque handle for `IActor*`.
169#[repr(C)]
170pub struct IActor {
171    _opaque: [u8; 0],
172}
173
174/// The rectangle a gang zone covers, as `GangZonePos` declares it.
175#[repr(C)]
176#[derive(Debug, Clone, Copy, PartialEq)]
177pub struct GangZonePos {
178    pub min: Vector2,
179    pub max: Vector2,
180}
181
182/// Opaque handle for `IObjectsComponent*`.
183#[repr(C)]
184pub struct IObjectsComponent {
185    _opaque: [u8; 0],
186}
187
188/// Opaque handle for `IPickupsComponent*`.
189#[repr(C)]
190pub struct IPickupsComponent {
191    _opaque: [u8; 0],
192}
193
194/// Opaque handle for `IPickup*`.
195#[repr(C)]
196pub struct IPickup {
197    _opaque: [u8; 0],
198}
199
200/// What a pickup does when a player walks into it (`PickupType` in
201/// `pickups.hpp`). The values the scripts use are passed straight through, so
202/// this is an `i32` rather than an enum that would have to track the server's.
203pub type PickupType = i32;
204
205/// Casts a component handle obtained by UID into the objects component.
206///
207/// # Safety
208/// `component` must be what `queryComponent(OBJECTS_COMPONENT_UID)` returned.
209#[must_use]
210pub unsafe fn as_objects_component(component: *mut ServerComponent) -> *mut IObjectsComponent {
211    component.cast::<IObjectsComponent>()
212}
213
214/// Casts a component handle obtained by UID into the pickups component.
215///
216/// # Safety
217/// `component` must be what `queryComponent(PICKUPS_COMPONENT_UID)` returned.
218#[must_use]
219pub unsafe fn as_pickups_component(component: *mut ServerComponent) -> *mut IPickupsComponent {
220    component.cast::<IPickupsComponent>()
221}
222
223/// `IObjectsComponent::create(modelID, position, rotation, drawDistance)`.
224///
225/// A `draw_distance` of `0.0` asks the server for its default. Returns null
226/// when the object pool is full.
227///
228/// # Safety
229/// `component` must be a live `IObjectsComponent`.
230#[must_use]
231pub unsafe fn create_object(
232    component: *mut IObjectsComponent,
233    model: i32,
234    position: Vector3,
235    rotation: Vector3,
236    draw_distance: f32,
237) -> *mut super::players::IObject {
238    call_vtable!(
239        component.cast::<u8>(),
240        0,
241        SLOT_CREATE_OBJECT,
242        (i32, Vector3, Vector3, f32) -> *mut super::players::IObject,
243        (model, position, rotation, draw_distance),
244        std::ptr::null_mut()
245    )
246}
247
248/// `IPickupsComponent::create(modelId, type, pos, virtualWorld, isStatic)`.
249///
250/// # Safety
251/// `component` must be a live `IPickupsComponent`.
252#[must_use]
253pub unsafe fn create_pickup(
254    component: *mut IPickupsComponent,
255    model: i32,
256    pickup_type: PickupType,
257    position: Vector3,
258    virtual_world: u32,
259    is_static: bool,
260) -> *mut IPickup {
261    call_vtable!(
262        component.cast::<u8>(),
263        0,
264        SLOT_CREATE_PICKUP,
265        (i32, PickupType, Vector3, u32, bool) -> *mut IPickup,
266        (model, pickup_type, position, virtual_world, is_static),
267        std::ptr::null_mut()
268    )
269}
270
271/// Casts a component handle obtained by UID into the text draws component.
272///
273/// # Safety
274/// `component` must be what `queryComponent(TEXTDRAWS_COMPONENT_UID)` returned.
275#[must_use]
276pub unsafe fn as_textdraws_component(component: *mut ServerComponent) -> *mut ITextDrawsComponent {
277    component.cast::<ITextDrawsComponent>()
278}
279
280/// Casts a component handle obtained by UID into the gang zones component.
281///
282/// # Safety
283/// `component` must be what `queryComponent(GANGZONES_COMPONENT_UID)` returned.
284#[must_use]
285pub unsafe fn as_gangzones_component(component: *mut ServerComponent) -> *mut IGangZonesComponent {
286    component.cast::<IGangZonesComponent>()
287}
288
289/// Casts a component handle obtained by UID into the actors component.
290///
291/// # Safety
292/// `component` must be what `queryComponent(ACTORS_COMPONENT_UID)` returned.
293#[must_use]
294pub unsafe fn as_actors_component(component: *mut ServerComponent) -> *mut IActorsComponent {
295    component.cast::<IActorsComponent>()
296}
297
298/// `ITextDrawsComponent::create(position, text)` — a global text draw.
299///
300/// The text is borrowed for the call; the server copies it.
301///
302/// # Safety
303/// `component` must be a live `ITextDrawsComponent`.
304#[must_use]
305pub unsafe fn create_textdraw(
306    component: *mut ITextDrawsComponent,
307    position: Vector2,
308    text: &str,
309) -> *mut ITextDraw {
310    let text = StringView {
311        data: text.as_ptr(),
312        len: text.len(),
313    };
314    call_vtable!(
315        component.cast::<u8>(),
316        0,
317        SLOT_CREATE_TEXTDRAW,
318        (Vector2, StringView) -> *mut ITextDraw,
319        (position, text),
320        std::ptr::null_mut()
321    )
322}
323
324/// `IGangZonesComponent::create(pos)`.
325///
326/// # Safety
327/// `component` must be a live `IGangZonesComponent`.
328#[must_use]
329pub unsafe fn create_gangzone(
330    component: *mut IGangZonesComponent,
331    area: GangZonePos,
332) -> *mut IGangZone {
333    call_vtable!(
334        component.cast::<u8>(),
335        0,
336        SLOT_CREATE_GANGZONE,
337        (GangZonePos) -> *mut IGangZone,
338        (area),
339        std::ptr::null_mut()
340    )
341}
342
343/// `IActorsComponent::create(skin, pos, angle)` — a static NPC-looking actor.
344///
345/// # Safety
346/// `component` must be a live `IActorsComponent`.
347#[must_use]
348pub unsafe fn create_actor(
349    component: *mut IActorsComponent,
350    skin: i32,
351    position: Vector3,
352    angle: f32,
353) -> *mut IActor {
354    call_vtable!(
355        component.cast::<u8>(),
356        0,
357        SLOT_CREATE_ACTOR,
358        (i32, Vector3, f32) -> *mut IActor,
359        (skin, position, angle),
360        std::ptr::null_mut()
361    )
362}
363
364/// Casts a component handle obtained by UID into the text labels component.
365///
366/// # Safety
367/// `component` must be what `queryComponent(TEXTLABELS_COMPONENT_UID)` returned.
368#[must_use]
369pub unsafe fn as_textlabels_component(
370    component: *mut ServerComponent,
371) -> *mut ITextLabelsComponent {
372    component.cast::<ITextLabelsComponent>()
373}
374
375/// Casts a component handle obtained by UID into the menus component.
376///
377/// # Safety
378/// `component` must be what `queryComponent(MENUS_COMPONENT_UID)` returned.
379#[must_use]
380pub unsafe fn as_menus_component(component: *mut ServerComponent) -> *mut IMenusComponent {
381    component.cast::<IMenusComponent>()
382}
383
384/// Casts a component handle obtained by UID into the classes component.
385///
386/// # Safety
387/// `component` must be what `queryComponent(CLASSES_COMPONENT_UID)` returned.
388#[must_use]
389pub unsafe fn as_classes_component(component: *mut ServerComponent) -> *mut IClassesComponent {
390    component.cast::<IClassesComponent>()
391}
392
393/// `ITextLabelsComponent::create(text, colour, pos, drawDist, vw, los)` — a
394/// label everyone sees.
395///
396/// `line_of_sight` decides whether the label shows through walls.
397///
398/// # Safety
399/// `component` must be a live `ITextLabelsComponent`.
400#[must_use]
401#[allow(clippy::too_many_arguments)]
402pub unsafe fn create_textlabel(
403    component: *mut ITextLabelsComponent,
404    text: &str,
405    colour: Colour,
406    position: Vector3,
407    draw_distance: f32,
408    virtual_world: i32,
409    line_of_sight: bool,
410) -> *mut ITextLabel {
411    let text = StringView {
412        data: text.as_ptr(),
413        len: text.len(),
414    };
415    call_vtable!(
416        component.cast::<u8>(),
417        0,
418        SLOT_CREATE_TEXTLABEL,
419        (StringView, Colour, Vector3, f32, i32, bool) -> *mut ITextLabel,
420        (text, colour, position, draw_distance, virtual_world, line_of_sight),
421        std::ptr::null_mut()
422    )
423}
424
425/// `IMenusComponent::create(title, position, columns, col1Width, col2Width)`.
426///
427/// # Safety
428/// `component` must be a live `IMenusComponent`.
429#[must_use]
430pub unsafe fn create_menu(
431    component: *mut IMenusComponent,
432    title: &str,
433    position: Vector2,
434    columns: u8,
435    column1_width: f32,
436    column2_width: f32,
437) -> *mut IMenu {
438    let title = StringView {
439        data: title.as_ptr(),
440        len: title.len(),
441    };
442    call_vtable!(
443        component.cast::<u8>(),
444        0,
445        SLOT_CREATE_MENU,
446        (StringView, Vector2, u8, f32, f32) -> *mut IMenu,
447        (title, position, columns, column1_width, column2_width),
448        std::ptr::null_mut()
449    )
450}
451
452/// `IClassesComponent::create(skin, team, spawn, angle, weapons)` — a spawn
453/// class, what `AddPlayerClass` creates on the script side.
454///
455/// The weapons array is passed by reference, so it only has to outlive the
456/// call.
457///
458/// # Safety
459/// `component` must be a live `IClassesComponent`.
460#[must_use]
461pub unsafe fn create_class(
462    component: *mut IClassesComponent,
463    skin: i32,
464    team: i32,
465    spawn: Vector3,
466    angle: f32,
467    weapons: &[WeaponSlot; MAX_WEAPON_SLOTS],
468) -> *mut IClass {
469    call_vtable!(
470        component.cast::<u8>(),
471        0,
472        SLOT_CREATE_CLASS,
473        (i32, i32, Vector3, f32, *const WeaponSlot) -> *mut IClass,
474        (skin, team, spawn, angle, weapons.as_ptr()),
475        std::ptr::null_mut()
476    )
477}
478
479/// `IEntity::getID()` for any entity that carries the subobject — an object, a
480/// pickup, a vehicle or a player.
481///
482/// # Safety
483/// `entity` must point at an `IExtensible`-derived interface that also inherits
484/// `IEntity`, which every entity in the SDK does.
485#[must_use]
486pub unsafe fn entity_id(entity: *mut u8) -> i32 {
487    call_vtable!(entity, ENTITY_OFFSET, SLOT_ENTITY_GET_ID, () -> i32, (), -1)
488}
489
490#[cfg(test)]
491mod tests {
492    use super::*;
493
494    #[test]
495    fn uids_match_the_headers() {
496        assert_eq!(OBJECTS_COMPONENT_UID, 0x59f8_415f_72da_6160);
497        assert_eq!(PICKUPS_COMPONENT_UID, 0xcf30_4faa_363d_d971);
498    }
499
500    #[test]
501    fn slots_match_what_clang_reports() {
502        #[cfg(not(target_env = "msvc"))]
503        assert_eq!([SLOT_CREATE_OBJECT, SLOT_CREATE_PICKUP], [21, 19]);
504        #[cfg(target_env = "msvc")]
505        assert_eq!([SLOT_CREATE_OBJECT, SLOT_CREATE_PICKUP], [19, 17]);
506    }
507
508    #[test]
509    fn the_other_uids_and_slots_match() {
510        assert_eq!(TEXTDRAWS_COMPONENT_UID, 0x9b5d_c2b1_d15c_992a);
511        assert_eq!(GANGZONES_COMPONENT_UID, 0xb335_1d11_ee8d_8056);
512        assert_eq!(ACTORS_COMPONENT_UID, 0xc81c_a021_eae2_ad5c);
513
514        // Text draws overload `create`, so MSVC reverses that pair; the other
515        // two do not, and lose only the destructor slot.
516        #[cfg(not(target_env = "msvc"))]
517        assert_eq!(
518            [
519                SLOT_CREATE_TEXTDRAW,
520                SLOT_CREATE_GANGZONE,
521                SLOT_CREATE_ACTOR
522            ],
523            [19, 19, 19]
524        );
525        #[cfg(target_env = "msvc")]
526        assert_eq!(
527            [
528                SLOT_CREATE_TEXTDRAW,
529                SLOT_CREATE_GANGZONE,
530                SLOT_CREATE_ACTOR
531            ],
532            [18, 17, 17]
533        );
534    }
535
536    #[test]
537    fn the_last_batch_of_uids_and_slots_match() {
538        assert_eq!(TEXTLABELS_COMPONENT_UID, 0xa0c5_7ea8_0a00_9742);
539        assert_eq!(MENUS_COMPONENT_UID, 0x621e_219e_b97e_e0b2);
540        assert_eq!(CLASSES_COMPONENT_UID, 0x8cfb_3183_976d_a208);
541
542        // Text labels overload `create` three ways; the global one happens to
543        // land on 18 in both ABIs, which the reversal makes a coincidence
544        // rather than a rule.
545        #[cfg(not(target_env = "msvc"))]
546        assert_eq!(
547            [SLOT_CREATE_TEXTLABEL, SLOT_CREATE_MENU, SLOT_CREATE_CLASS],
548            [18, 19, 19]
549        );
550        #[cfg(target_env = "msvc")]
551        assert_eq!(
552            [SLOT_CREATE_TEXTLABEL, SLOT_CREATE_MENU, SLOT_CREATE_CLASS],
553            [18, 17, 17]
554        );
555
556        // `WeaponSlotData` is a byte and a word, and the server reads an array
557        // of them.
558        assert_eq!(
559            std::mem::size_of::<WeaponSlot>(),
560            8,
561            "padded to 4-byte alignment"
562        );
563        assert_eq!(MAX_WEAPON_SLOTS, 13);
564    }
565
566    #[test]
567    fn null_components_create_nothing() {
568        let zero = Vector3 {
569            x: 0.0,
570            y: 0.0,
571            z: 0.0,
572        };
573        assert!(unsafe { create_object(std::ptr::null_mut(), 1337, zero, zero, 0.0) }.is_null());
574        assert!(unsafe { create_pickup(std::ptr::null_mut(), 1274, 1, zero, 0, true) }.is_null());
575    }
576}