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