API talk:MumbleLink/Example implementation (Python)

From Guild Wars 2 Wiki
Jump to navigationJump to search

I have been testing this code and it doesn't trigger GW2 to start sending MumbleLink data because with the example implementation, the MumbleLink shared file isn't created if it doesn't exist. GW2 doesn't create it neither and instead expects the consumer to create it. So, in order to start receiving data, the following line must be changed:

        memfile = mmap.mmap(0, size_link + size_context, "MumbleLink", mmap.ACCESS_READ)

to

        memfile = mmap.mmap(0, size_link + size_context, "MumbleLink")

I spent a few hours trying to find out why I wasn't receiving any data at all. I think this change is useful to avoid more people into falling in the same problem. However, I can't edit this page. Could someone with proper permissions do it? Alvaro (talk) 10:25, 9 January 2021 (UTC)

Hey, you're right - turns out i always had another app open that would also access (and create) the shared file, not realizing that the game doesn't create it. However, I'm now trying to reproduce the change and it still errors out (Python 3.8, Win7/64).
Edit: further reading the python docs reveals that ACCESS_READ was in fact not wrong - it should *create* a memory mapped file that is readonly, while omitting the access parameter would create a write through file.
Edit2: I'm unable to reproduce the above change. None of the access modes (or omitting them) would let me properly read the mumble data.

--Smiley™ de: user | talk 10:36, 9 January 2021 (UTC)


Hello Smiley, sorry for the delay.
I've been debugging the example implementation, as I have a working version to compare. I've found that there are 3 issues that are preventing GW2 to start sending data if you don't already have a properly set up shared file:
  • The first one is related to the ACCESS_READ parameter. Using that parameter, GW2 won't start sending data unless another process had already created the shared file. Yes, the drawback is that we would be creating a read/write file, but as we won't be writting to it, there shouldn't be any problem. About the mmap documentation, I don't see where it says that a shared file is created when using the ACCESS_READ mode.
  • The second issue is related to the length of the shared file. I've found by trial and error that if the file isn't big enough to hold the whole MumbleLink structure, GW2 won't start sending data until another process creates a big enough shared file.
  • The last issue is that even if everything is properly set, GW2 may not notice the shared file if it's closed very fast. The current example implementation only fetches the data once and immediately closes the file. If it's the first time the shared file has been created after launching the game, the data read will be all empty (as expected for the first time), but there is a high chance that subsequent executions will result in the same behavior as GW2 may not have noticed the shared file yet. In order to fix that, I propose reading a few times before exiting so that users don't get confused about why they aren't getting anything for the first time, and also to give time to GW2 to detect the shared file and start sending data.
Here is my proposed version of the example implementation with all the mentioned issues solved and tested working (Python 3.9):
import ctypes
import mmap
import time


class Link(ctypes.Structure):
    _fields_ = [
        ("uiVersion", ctypes.c_uint32),           # 4 bytes
        ("uiTick", ctypes.c_ulong),               # 4 bytes
        ("fAvatarPosition", ctypes.c_float * 3),  # 3*4 bytes
        ("fAvatarFront", ctypes.c_float * 3),     # 3*4 bytes
        ("fAvatarTop", ctypes.c_float * 3),       # 3*4 bytes
        ("name", ctypes.c_wchar * 256),           # 512 bytes
        ("fCameraPosition", ctypes.c_float * 3),  # 3*4 bytes
        ("fCameraFront", ctypes.c_float * 3),     # 3*4 bytes
        ("fCameraTop", ctypes.c_float * 3),       # 3*4 bytes
        ("identity", ctypes.c_wchar * 256),       # 512 bytes
        ("context_len", ctypes.c_uint32),         # 4 bytes
        # ("context", ctypes.c_ubyte * 256),      # 256 bytes, see below
        # ("description", ctypes.c_wchar * 2048), # 4096 bytes, always empty
    ]


class Context(ctypes.Structure):
    _fields_ = [
        ("serverAddress", ctypes.c_ubyte * 28),   # 28 bytes
        ("mapId", ctypes.c_uint32),               # 4 bytes
        ("mapType", ctypes.c_uint32),             # 4 bytes
        ("shardId", ctypes.c_uint32),             # 4 bytes
        ("instance", ctypes.c_uint32),            # 4 bytes
        ("buildId", ctypes.c_uint32),             # 4 bytes
        ("uiState", ctypes.c_uint32),             # 4 bytes
        ("compassWidth", ctypes.c_uint16),        # 2 bytes
        ("compassHeight", ctypes.c_uint16),       # 2 bytes
        ("compassRotation", ctypes.c_float),      # 4 bytes
        ("playerX", ctypes.c_float),              # 4 bytes
        ("playerY", ctypes.c_float),              # 4 bytes
        ("mapCenterX", ctypes.c_float),           # 4 bytes
        ("mapCenterY", ctypes.c_float),           # 4 bytes
        ("mapScale", ctypes.c_float),             # 4 bytes
        ("processId", ctypes.c_uint32),           # 4 bytes
        ("mountIndex", ctypes.c_uint8),           # 1 byte
    ]


class MumbleLink:
    data: Link
    context: Context

    def __init__(self):
        self.size_link = ctypes.sizeof(Link)
        self.size_context = ctypes.sizeof(Context)
        size_discarded = 256 - self.size_context + 4096  # empty areas of context and description

        # GW2 won't start sending data if memfile isn't big enough so we have to add discarded bits too
        memfile_length = self.size_link + self.size_context + size_discarded

        self.memfile = mmap.mmap(fileno=-1, length=memfile_length, tagname="MumbleLink")

    def read(self):
        self.memfile.seek(0)

        self.data = self.unpack(Link, self.memfile.read(self.size_link))
        self.context = self.unpack(Context, self.memfile.read(self.size_context))

    def close(self):
        self.memfile.close()

    @staticmethod
    def unpack(ctype, buf):
        cstring = ctypes.create_string_buffer(buf)
        ctype_instance = ctypes.cast(ctypes.pointer(cstring), ctypes.POINTER(ctype)).contents
        return ctype_instance


def main():
    ml = MumbleLink()

    for _ in range(5):
        ml.read()

        # do stuff ...
        
        print("identity:", ml.data.identity)
        print("position:", [round(ml.context.playerX), round(ml.context.playerY)])
        print()

        time.sleep(1)

    ml.close()


if __name__ == "__main__":
    main()
I've also updated some of the Link struct types to properly reflect the C type as described in the API:MumbleLink page, and also added processId and mountIndex to Context as they were missing.
Please tell me if you can confirm it's working for you too, and feel free to make any changes before updating.
Alvaro (talk) 15:59, 19 February 2021 (UTC)
Since Smiley doesn't appear to be around at the time i'll go ahead and update the example with a slightly modified version of your version for the time being. Modified so that it works with 3.5 (as that's the version i happen to have around and can thus easily test with) and so that it loops until it could read something.
You may find it useful to add a request to Guild Wars 2 Wiki:Requests for API editorship. That way you could rather likely update the API pages yourself then. Nightsky (talk) 06:04, 19 March 2021 (UTC)
Sorry, i didn't find the time to test and forgot about it: The update looks good - thank you both! --Smiley™ de: user | talk 11:56, 19 March 2021 (UTC)