1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
//! **mumble-link** provides an API for using the [Mumble Link][link] plugin
//! for position-aware VoIP communications.
//!
//! [link]: https://wiki.mumble.info/wiki/Link
//!
//! Connect to Mumble link with `MumbleLink::new()`, set the context or player
//! identity as needed, and call `update()` every frame with the position data.

use std::{io, ptr, mem};
use libc::{c_float, wchar_t};

#[cfg(any(test, windows))]
macro_rules! wide {
    ($($ch:ident)*) => {
        [$(stringify!($ch).as_bytes()[0] as ::libc::wchar_t,)* 0]
    }
}

#[cfg_attr(windows, path="windows.rs")]
#[cfg_attr(not(windows), path="unix.rs")]
mod imp;

/// A position in three-dimensional space.
///
/// The vectors are in a left-handed coordinate system: X positive towards
/// "right", Y positive towards "up", and Z positive towards "front". One unit
/// is treated as one meter by the sound engine.
///
/// `front` and `top` should be unit vectors and perpendicular to each other.
#[derive(Copy, Clone)]
#[repr(C)]
pub struct Position {
    /// The character's position in space.
    pub position: [f32; 3],
    /// A unit vector pointing out of the character's eyes.
    pub front: [f32; 3],
    /// A unit vector pointing out of the top of the character's head.
    pub top: [f32; 3],
}

// `f32` is used above for tidyness; assert that it matches c_float.
const _ASSERT_CFLOAT_IS_F32: c_float = 0f32;

impl Default for Position {
    fn default() -> Self {
        Position {
            position: [0., 0., 0.],
            front: [0., 0., 1.],
            top: [0., 1., 0.],
        }
    }
}

#[derive(Copy)]
#[repr(C)]
struct LinkedMem {
    ui_version: u32,
    ui_tick: u32,
    avatar: Position,
    name: [wchar_t; 256],
    camera: Position,
    identity: [wchar_t; 256],
    context_len: u32,
    context: [u8; 256],
    description: [wchar_t; 2048],
}

impl Clone for LinkedMem {
    fn clone(&self) -> Self { *self }
}

impl LinkedMem {
    fn new(name: &str, description: &str) -> LinkedMem {
        let mut result = LinkedMem {
            ui_version: 2,
            ui_tick: 0,
            avatar: Position::default(),
            name: [0; 256],
            camera: Position::default(),
            identity: [0; 256],
            context_len: 0,
            context: [0; 256],
            description: [0; 2048],
        };
        imp::copy(&mut result.name, name);
        imp::copy(&mut result.description, description);
        result
    }

    fn set_context(&mut self, context: &[u8]) {
        let len = std::cmp::min(context.len(), self.context.len());
        self.context[..len].copy_from_slice(&context[..len]);
        self.context_len = len as u32;
    }

    #[inline]
    fn set_identity(&mut self, identity: &str) {
        imp::copy(&mut self.identity, identity);
    }

    fn update(&mut self, avatar: Position, camera: Position) {
        self.ui_tick = self.ui_tick.wrapping_add(1);
        self.avatar = avatar;
        self.camera = camera;
    }
}

macro_rules! docs {
    ($(#[$attr:meta])* pub fn set_context(&mut $s:ident, $c:ident: &[u8]) $b:block) => {
        /// Update the context string, used to determine which users on a Mumble
        /// server should hear each other positionally.
        ///
        /// If context between two Mumble users does not match, the positional audio
        /// data is stripped server-side and voice will be received as
        /// non-positional. Accordingly, the context should only match for players
        /// on the same game, server, and map, depending on the game itself. When
        /// in doubt, err on the side of including less; this allows for more
        /// flexibility in the future.
        ///
        /// The context should be changed infrequently, at most a few times per
        /// second.
        ///
        /// The context has a maximum length of 256 bytes.
        $(#[$attr])*
        pub fn set_context(&mut $s, $c: &[u8]) $b
    };
    ($(#[$attr:meta])* pub fn set_identity(&mut $s:ident, $i:ident: &str) $b:block) => {
        /// Update the identity, uniquely identifying the player in the given
        /// context. This is usually the in-game name or ID.
        ///
        /// The identity may also contain any additional information about the
        /// player which might be useful for the Mumble server, for example to move
        /// teammates to the same channel or give squad leaders additional powers.
        /// It is recommended that a parseable format like JSON or CSV is used for
        /// this.
        ///
        /// The identity should be changed infrequently, at most a few times per
        /// second.
        ///
        /// The identity has a maximum length of 255 UTF-16 code units.
        $(#[$attr])*
        pub fn set_identity(&mut $s, $i: &str) $b
    };
    ($(#[$attr:meta])* pub fn update(&mut $s:ident, $a:ident: Position, $c:ident: Position) $b:block) => {
        /// Update the link with the latest position information. Should be called
        /// once per frame.
        ///
        /// `avatar` should be the position of the player. If it is all zero,
        /// positional audio will be disabled. `camera` should be the position of
        /// the camera, which may be the same as `avatar`.
        $(#[$attr])*
        pub fn update(&mut $s, $a: Position, $c: Position) $b
    };
}

/// An active Mumble link connection.
pub struct MumbleLink {
    map: imp::Map,
    local: LinkedMem,
}

impl MumbleLink {
    /// Attempt to open the Mumble link, providing the specified application
    /// name and description.
    ///
    /// Opening the link will fail if Mumble is not running. If another
    /// application is also using Mumble link, its data may be overwritten or
    /// conflict with this link. To avoid this, use `SharedLink`.
    pub fn new(name: &str, description: &str) -> io::Result<MumbleLink> {
        Ok(MumbleLink {
            map: (imp::Map::new(std::mem::size_of::<LinkedMem>()))?,
            local: LinkedMem::new(name, description),
        })
    }

    docs! {
        #[inline]
        pub fn set_context(&mut self, context: &[u8]) {
            self.local.set_context(context)
        }
    }
    docs! {
        #[inline]
        pub fn set_identity(&mut self, identity: &str) {
            self.local.set_identity(identity)
        }
    }
    docs! {
        #[inline]
        pub fn update(&mut self, avatar: Position, camera: Position) {
            self.local.update(avatar, camera);
            unsafe {
                ptr::write_volatile(self.map.ptr as *mut LinkedMem, self.local);
            }
        }
    }
}

unsafe impl Send for MumbleLink {}

impl Drop for MumbleLink {
    fn drop(&mut self) {
        unsafe {
            // zero the linked memory
            ptr::write_volatile(self.map.ptr as *mut LinkedMem, mem::zeroed());
        }
    }
}

/// A weak Mumble link connection.
///
/// Constructing a `SharedLink` always succeeds, even if Mumble is not running
/// or another application is writing to the link. If this happens, `update()`
/// will retry opening the link on a regular basis, succeeding if Mumble is
/// started or the other application stops using the link.
pub struct SharedLink {
    inner: Inner,
    local: LinkedMem,
}

impl SharedLink {
    /// Open the Mumble link, providing the specified application name and
    /// description.
    pub fn new(name: &str, description: &str) -> SharedLink {
        SharedLink {
            inner: Inner::open(),
            local: LinkedMem::new(name, description),
        }
    }

    docs! {
        #[inline]
        pub fn set_context(&mut self, context: &[u8]) {
            self.local.set_context(context)
        }
    }

    docs! {
        #[inline]
        pub fn set_identity(&mut self, identity: &str) {
            self.local.set_identity(identity)
        }
    }

    docs! {
        pub fn update(&mut self, avatar: Position, camera: Position) {
            self.local.update(avatar, camera);

            // If it's been a hundred ticks, try to reopen the link
            if self.local.ui_tick % 100 == 0 {
                self.inner = match mem::replace(&mut self.inner, Inner::Unset) {
                    Inner::Closed(_) => Inner::open(),
                    Inner::InUse(map, last_tick) => {
                        let previous = unsafe { ptr::read_volatile(map.ptr as *mut LinkedMem) };
                        if previous.ui_version == 0 || last_tick == previous.ui_tick {
                            Inner::Active(map)
                        } else {
                            Inner::InUse(map, previous.ui_tick)
                        }
                    }
                    Inner::Active(map) => Inner::Active(map),
                    Inner::Unset => unreachable!(),
                };
            }

            // If the link is active, write to it
            if let Inner::Active(ref mut map) = self.inner {
                unsafe {
                    ptr::write_volatile(map.ptr as *mut LinkedMem, self.local);
                }
            }
        }
    }

    /// Get the status of the shared link. See `Status` for details.
    pub fn status(&self) -> Status {
        match self.inner {
            Inner::Closed(ref err) => Status::Closed(err),
            Inner::InUse(ref map, _) => {
                let previous = unsafe { ptr::read_volatile(map.ptr as *mut LinkedMem) };
                Status::InUse {
                    name: imp::read(&previous.name),
                    description: imp::read(&previous.description)
                }
            },
            Inner::Active(_) => Status::Active,
            Inner::Unset => unreachable!(),
        }
    }

    /// Deactivate the shared link.
    ///
    /// Should be called when `update()` will not be called again for a while,
    /// such as if the player is no longer in-game.
    pub fn deactivate(&mut self) {
        if let Inner::Active(ref mut map) = self.inner {
            unsafe {
                ptr::write_volatile(map.ptr as *mut LinkedMem, mem::zeroed());
            }
        }
        self.inner = Inner::Closed(io::Error::new(io::ErrorKind::Other, "Manually closed"));
    }
}

unsafe impl Send for SharedLink {}

impl Drop for SharedLink {
    fn drop(&mut self) {
        self.deactivate();
    }
}

enum Inner {
    Unset,
    Closed(io::Error),
    InUse(imp::Map, u32),
    Active(imp::Map),
}

impl Inner {
    fn open() -> Inner {
        match imp::Map::new(std::mem::size_of::<LinkedMem>()) {
            Err(err) => Inner::Closed(err),
            Ok(map) => {
                let previous = unsafe { ptr::read_volatile(map.ptr as *mut LinkedMem) };
                if previous.ui_version != 0 {
                    Inner::InUse(map, previous.ui_tick)
                } else {
                    Inner::Active(map)
                }
            }
        }
    }
}

/// The status of a `SharedLink`.
#[derive(Debug)]
pub enum Status<'a> {
    /// The link is closed. This is usually because Mumble is not running or
    /// the link was closed manually with `deactivate()`.
    Closed(&'a io::Error),
    /// The link is in use by another application.
    InUse {
        /// The name of the other application.
        name: String,
        /// The description of the other application.
        description: String,
    },
    /// The link is active.
    Active,
}

#[test]
fn test_wide() {
    let wide = wide!(M u m b l e L i n k);
    for (i, b) in "MumbleLink".bytes().enumerate() {
        assert_eq!(b as wchar_t, wide[i]);
    }
    assert_eq!(0, wide[wide.len() - 1]);

    let mut wide = [1; 32];
    imp::copy(&mut wide, "FooBar");
    assert_eq!(&wide[..7], wide!(F o o B a r));
    assert_eq!("FooBar", imp::read(&wide));

    let mut wide = [1; 3];
    imp::copy(&mut wide, "ABC");
    assert_eq!(&wide[..], wide!(A B));
    assert_eq!("AB", imp::read(&wide));

    assert_eq!("BarFoo", imp::read(&wide!(B a r F o o)));
}