[Retail] Calling Functions while Internal menu

User Tag List

Page 1 of 2 12 LastLast
Results 1 to 15 of 19
  1. #1
    CodeBytes's Avatar Member
    Reputation
    14
    Join Date
    Feb 2020
    Posts
    39
    Thanks G/R
    7/7
    Trade Feedback
    0 (0%)
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)

    [Retail] Calling Functions while Internal

    Hello,

    I'm currently exploring the benefits of being internal (D3D12 present hook). Thus far, I've been entirely external--only poking the client with ReadProcessMemory, and an occasional write. Being external has several limitations, one of which is not being able to access the client's API without reading custom addon memory.

    My first attempt at an internal function call went like this:

    Code:
    inline bool Spell_C_GetSpellCooldown(unsigned int spellId, int a2, __int64 a3, __int64 a4, __int64 a5)
    {
        return ((bool(__cdecl*)(unsigned int spellId, int a2, __int64 a3, __int64 a4, __int64 a5))0x7FF75537B610)(spellId, a2, a3, a4, a5); // Build 33941; function offset 0xB8B610
    }
    
    ...
    
    bool result = Spell_C_GetSpellCooldown(spellId, 0, 0, 0, 0);
    sGUI->ShowMessage(std::to_string(result));
    Unfortunately, I never get to see the GUI update because World of Warcraft crashes. The pseudocode in IDA looks like this:

    Code:
    // IDA adds information to the start of the DB, shifting offsets by 10000
    bool __fastcall sub_B9B610(unsigned int a1, int a2, __int64 a3, __int64 a4, __int64 a5)
    {
      return sub_BC1190((_UNKNOWN *)((char *)&unk_294A020 + (a2 != 0 ? 0xB8 : 0)), a1, 0, a3);
    }
    unk_294A020 (offset 293A020) is s_spellHistory.

    What am I doing wrong?

    [Retail] Calling Functions while Internal
  2. #2
    namreeb's Avatar Legendary

    Reputation
    658
    Join Date
    Sep 2008
    Posts
    1,023
    Thanks G/R
    7/215
    Trade Feedback
    0 (0%)
    Mentioned
    8 Post(s)
    Tagged
    0 Thread(s)
    Originally Posted by CodeBytes View Post
    Hello,

    I'm currently exploring the benefits of being internal (D3D12 present hook). Thus far, I've been entirely external--only poking the client with ReadProcessMemory, and an occasional write. Being external has several limitations, one of which is not being able to access the client's API without reading custom addon memory.

    My first attempt at an internal function call went like this:

    Code:
    inline bool Spell_C_GetSpellCooldown(unsigned int spellId, int a2, __int64 a3, __int64 a4, __int64 a5)
    {
        return ((bool(__cdecl*)(unsigned int spellId, int a2, __int64 a3, __int64 a4, __int64 a5))0x7FF75537B610)(spellId, a2, a3, a4, a5); // Build 33941; function offset 0xB8B610
    }
    
    ...
    
    bool result = Spell_C_GetSpellCooldown(spellId, 0, 0, 0, 0);
    sGUI->ShowMessage(std::to_string(result));
    Unfortunately, I never get to see the GUI update because World of Warcraft crashes. The pseudocode in IDA looks like this:

    Code:
    // IDA adds information to the start of the DB, shifting offsets by 10000
    bool __fastcall sub_B9B610(unsigned int a1, int a2, __int64 a3, __int64 a4, __int64 a5)
    {
      return sub_BC1190((_UNKNOWN *)((char *)&unk_294A020 + (a2 != 0 ? 0xB8 : 0)), a1, 0, a3);
    }
    unk_294A020 (offset 293A020) is s_spellHistory.

    What am I doing wrong?
    You may have other problems, but one that I see is you are calling it as __cdecl when IDA believes it is __fastcall.

  3. Thanks CodeBytes (1 members gave Thanks to namreeb for this useful post)
  4. #3
    CodeBytes's Avatar Member
    Reputation
    14
    Join Date
    Feb 2020
    Posts
    39
    Thanks G/R
    7/7
    Trade Feedback
    0 (0%)
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    Originally Posted by namreeb
    You may have other problems, but one that I see is you are calling it as __cdecl when IDA believes it is __fastcall.
    Well that's embarrassing. Thank you namreeb. Unfortunately, changing it to __fastcall still causes the client to crash. I'm calling the function inside the present hook:

    Code:
        long __fastcall hookPresentD3D12(IDXGISwapChain3* pSwapChain, UINT SyncInterval, UINT Flags)
        {
            // Testing
            if (GetAsyncKeyState(VK_F12) & 0x1)
            {
                unsigned int spellId = 100;
                bool result = Spell_C_GetSpellCooldown(spellId, 0, 0, 0, 0);
                sGUI->ShowMessage(std::to_string(result));
            }
    
            return oPresentD3D12(pSwapChain, SyncInterval, Flags);
        }
    I'll admit I'm new to the internal scene. Should I be running my logic in D3DPresent, or do I need to hook somewhere else?

  5. #4
    ejt's Avatar Contributor
    Reputation
    209
    Join Date
    Mar 2008
    Posts
    166
    Thanks G/R
    3/111
    Trade Feedback
    0 (0%)
    Mentioned
    4 Post(s)
    Tagged
    0 Thread(s)
    Code:
    bool __cdecl Spell_GetSpellCooldown(uint32_t spellId, bool isPet, uint64_t* duration, uint64_t* start, uint64_t* enabled, uint64_t unk_0, uint64_t unk_1, uint64_t* modrate)
    Usage:

    Code:
    uint32_t spell_id = 1234;
    bool is_pet = false;
    uint64_t duration = 0, startTime = 0, enabled = 0, modRate = 1;
    Spell_GetSpellCooldown(spell_id, is_pet, &duration, &startTime, &enabled, 0, 0, &modRate);

  6. #5
    CodeBytes's Avatar Member
    Reputation
    14
    Join Date
    Feb 2020
    Posts
    39
    Thanks G/R
    7/7
    Trade Feedback
    0 (0%)
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    Thanks ejt. This is where I am now:

    Code:
    inline bool Spell_C_GetSpellCooldown(uint32_t spellId, int32_t isPet, int64_t* duration, int64_t* startTime, int64_t* modRate)
    {
        return ((bool(__fastcall*)(uint32_t spellId, int32_t isPet, int64_t* duration, int64_t* startTime, int64_t* modRate))0x7FF75537B610)(spellId, isPet, duration, startTime, modRate);
    }
    
    ...
    
    uint32_t spellId = 100;
    int32_t isPet = 0;
    int64_t duration = 0, startTime = 0, modRate = 1;
    Spell_C_GetSpellCooldown(spellId, isPet, &duration, &startTime, &modRate);
    
    std::string sresult = "Duration: " + std::to_string(duration) + " Start Time: " + std::to_string(startTime) + " Mod Rate: " + std::to_string(modRate);
    sGUI->ShowMessage(std::to_string(result));
    I suppose those parameters were indeed references, even though the subroutine definition did not show them as such. I see the function being used accordingly:

    Code:
    signed __int64 __fastcall sub_11DF060(__int64 a1)
    {
      __int64 v1; // rbx@1
      signed __int64 result; // rax@2
      int v3; // [sp+40h] [bp-18h]@3
      int v4; // [sp+44h] [bp-14h]@3
      int v5; // [sp+48h] [bp-10h]@3
      unsigned __int8 v6; // [sp+68h] [bp+10h]@1
      unsigned int v7; // [sp+70h] [bp+18h]@1
      int v8; // [sp+78h] [bp+20h]@3
    
      v1 = a1;
      v7 = 0;
      v6 = 0;
      if ( (unsigned __int8)sub_11EFE10(&v7, &v6, a1) )
      {
        v5 = xmmword_20CD27C;
        v8 = 0;
        v3 = 0;
        v4 = 0;
        sub_B9B610(v7, v6, (__int64)&v3, (__int64)&v8, (__int64)&v4);     <<<< Here
        sub_1E0C30(v1);
        sub_1E0C30(v1);
        sub_1E0C30(v1);
        sub_1E0C30(v1);
        result = 4i64;
      }
      else
      {
        result = 0i64;
      }
      return result;
    }
    However, the client still crashes. If I change isPet to bool then the client just freezes and becomes non-responsive. v6 is declared as unsigned __int8; however, even making this adjustment (which was grasping at straws) causes a crash.

    Any ideas?

  7. #6
    ejt's Avatar Contributor
    Reputation
    209
    Join Date
    Mar 2008
    Posts
    166
    Thanks G/R
    3/111
    Trade Feedback
    0 (0%)
    Mentioned
    4 Post(s)
    Tagged
    0 Thread(s)
    0x7FF75537B610

    That's your error, should be easy for you to figure out why this is wrong.

  8. Thanks CodeBytes, Neer (2 members gave Thanks to ejt for this useful post)
  9. #7
    CodeBytes's Avatar Member
    Reputation
    14
    Join Date
    Feb 2020
    Posts
    39
    Thanks G/R
    7/7
    Trade Feedback
    0 (0%)
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    Yes, thanks ejt. I'm still in the external-app mindset. Fixing the address now causes an access violation crash, "The instruction at "0x00000000b8b610" referenced memory at "0x00000000b8b610". The memory could not be "executed"." There are a host of reasons for why this is happening, so I'll start the process of elimination this weekend and post back--unless of course someone already experienced this and came up with a solution they are willing to share?

  10. #8
    Icesythe7's Avatar Contributor
    Reputation
    230
    Join Date
    Feb 2017
    Posts
    168
    Thanks G/R
    10/111
    Trade Feedback
    0 (0%)
    Mentioned
    1 Post(s)
    Tagged
    0 Thread(s)
    Originally Posted by CodeBytes View Post
    Yes, thanks ejt. I'm still in the external-app mindset. Fixing the address now causes an access violation crash, "The instruction at "0x00000000b8b610" referenced memory at "0x00000000b8b610". The memory could not be "executed"." There are a host of reasons for why this is happening, so I'll start the process of elimination this weekend and post back--unless of course someone already experienced this and came up with a solution they are willing to share?
    Did u add the games base?

    Code:
    inline uintptr_t Base = reinterpret_cast<uintptr_t>(GetModuleHandle(nullptr));
    
    inline bool Spell_C_GetSpellCooldown(const uint32_t spellId, const bool isPet, int64_t* duration, int64_t* startTime, int64_t* modRate)
    {
    	return reinterpret_cast<bool(__fastcall*)(uint32_t, bool, int64_t*, int64_t*, int64_t*)>(Base + 0xB9B610)(spellId, isPet, duration, startTime, modRate);
    }
    
    uint32_t spellId = 100;
    bool isPet = false;
    int64_t duration = 0, startTime = 0, modRate = 1;
    Spell_C_GetSpellCooldown(spellId, isPet, &duration, &startTime, &modRate);
    Last edited by Icesythe7; 05-06-2020 at 09:01 AM.

  11. Thanks Corthezz (1 members gave Thanks to Icesythe7 for this useful post)
  12. #9
    CodeBytes's Avatar Member
    Reputation
    14
    Join Date
    Feb 2020
    Posts
    39
    Thanks G/R
    7/7
    Trade Feedback
    0 (0%)
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    Hi Icesythe7,

    Both with and without game's base. To get the client's module, I did this:

    Code:
    uintptr_t GetModuleBaseAddress(DWORD processId, const wchar_t* moduleName)
    {
        uintptr_t moduleBaseAddress = 0;
        HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE | TH32CS_SNAPMODULE32, processId);
        if (snapshot != INVALID_HANDLE_VALUE)
        {
            MODULEENTRY32 moduleEntry;
            moduleEntry.dwSize = sizeof(moduleEntry);
            if (Module32First(snapshot, &moduleEntry))
            {
                do
                {
                    if (!_wcsicmp(moduleEntry.szModule, moduleName))
                    {
                        moduleBaseAddress = (uintptr_t)moduleEntry.modBaseAddr;
                        break;
                    }
    
                } while (Module32Next(snapshot, &moduleEntry));
            }
        }
        CloseHandle(snapshot);
        return moduleBaseAddress;
    }
    I'm sure there is a better way to do it internally, and I'll get to that over the weekend.

    The function's address would then be m_moduleBaseAddress + 0xB8B610. Unfortunately, this causes the client to freeze and become unresponsive. If I don't include the module's base address, as was suggested elsewhere on the forum, I then get "The instruction at "0x00000000b8b610" referenced memory at "0x00000000b8b610". The memory could not be "executed"."

    I just need a little time to look it all over, my profession is leaving very little time to work on this hobby project; but I am committed to solving this!

  13. #10
    Icesythe7's Avatar Contributor
    Reputation
    230
    Join Date
    Feb 2017
    Posts
    168
    Thanks G/R
    10/111
    Trade Feedback
    0 (0%)
    Mentioned
    1 Post(s)
    Tagged
    0 Thread(s)
    Originally Posted by CodeBytes View Post
    Hi Icesythe7,

    Both with and without game's base. To get the client's module, I did this:

    Code:
    uintptr_t GetModuleBaseAddress(DWORD processId, const wchar_t* moduleName)
    {
        uintptr_t moduleBaseAddress = 0;
        HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE | TH32CS_SNAPMODULE32, processId);
        if (snapshot != INVALID_HANDLE_VALUE)
        {
            MODULEENTRY32 moduleEntry;
            moduleEntry.dwSize = sizeof(moduleEntry);
            if (Module32First(snapshot, &moduleEntry))
            {
                do
                {
                    if (!_wcsicmp(moduleEntry.szModule, moduleName))
                    {
                        moduleBaseAddress = (uintptr_t)moduleEntry.modBaseAddr;
                        break;
                    }
    
                } while (Module32Next(snapshot, &moduleEntry));
            }
        }
        CloseHandle(snapshot);
        return moduleBaseAddress;
    }
    I'm sure there is a better way to do it internally, and I'll get to that over the weekend.

    The function's address would then be m_moduleBaseAddress + 0xB8B610. Unfortunately, this causes the client to freeze and become unresponsive. If I don't include the module's base address, as was suggested elsewhere on the forum, I then get "The instruction at "0x00000000b8b610" referenced memory at "0x00000000b8b610". The memory could not be "executed"."

    I just need a little time to look it all over, my profession is leaving very little time to work on this hobby project; but I am committed to solving this!
    I edited my code above also yes there is a better way to get base which is posted above, also (atleast on classic) there is 9 parameters not 5 albeit retail may be different, you need to decompile (press f5) on the function so it can get the correct number of params, I can tell you didnt do this as the call under getspellcooldown shows only 1 param and it actually has 2 as it is lua_pushnumber(lua_state, value).

    according to ejt it should be more like this
    Code:
    inline uintptr_t Base = reinterpret_cast<uintptr_t>(GetModuleHandle(nullptr));
    
    inline bool Spell_C_GetSpellCooldown(const uint32_t spellId, const bool isPet, int64_t* duration, int64_t* startTime, bool* enabled, int64_t* modRate)
    {
    	return reinterpret_cast<bool(__fastcall*)(uint32_t, bool, bool, int64_t*, int64_t*, bool*, uint64_t, uint64_t, int64_t*)>(Base + 0xB9B610)(spellId, false,  isPet, duration, startTime, enabled, 0, 0, modRate);
    }
    
    uint32_t spellId = 100;
    bool isPet = false, enabled = false;
    int64_t duration = 0, startTime = 0,  modRate = 1;
    Spell_C_GetSpellCooldown(spellId, isPet, &duration, &startTime, &enabled, &modRate);
    it should look more like this
    Last edited by Icesythe7; 05-06-2020 at 06:04 PM.

  14. Thanks CodeBytes, Corthezz (2 members gave Thanks to Icesythe7 for this useful post)
  15. #11
    Jadd's Avatar 🐸 Premium Seller
    Reputation
    1511
    Join Date
    May 2008
    Posts
    2,432
    Thanks G/R
    81/333
    Trade Feedback
    1 (100%)
    Mentioned
    2 Post(s)
    Tagged
    0 Thread(s)
    I'm currently looking at classic. You're missing a parameter before isPet. As you can see in your screenshot it is invoked with 0.

    Code:
    [return: MarshalAs(UnmanagedType.I1)]
    public delegate bool Spell_C_GetSpellCooldown(int spellId, bool unk1, bool pet, ref int duration, ref int startTime, ref bool enabled, IntPtr unk2, IntPtr unk3, ref float modRate);

  16. Thanks Corthezz (1 members gave Thanks to Jadd for this useful post)
  17. #12
    Icesythe7's Avatar Contributor
    Reputation
    230
    Join Date
    Feb 2017
    Posts
    168
    Thanks G/R
    10/111
    Trade Feedback
    0 (0%)
    Mentioned
    1 Post(s)
    Tagged
    0 Thread(s)
    Originally Posted by Jadd View Post
    I'm currently looking at classic. You're missing a parameter before isPet. As you can see in your screenshot it is invoked with 0.

    Code:
    [return: MarshalAs(UnmanagedType.I1)]
    public delegate bool Spell_C_GetSpellCooldown(int spellId, bool unk1, bool pet, ref int duration, ref int startTime, ref bool enabled, IntPtr unk2, IntPtr unk3, ref float modRate);
    edited, was just doing ejt's example as i dont feel like dumping retail

  18. #13
    CodeBytes's Avatar Member
    Reputation
    14
    Join Date
    Feb 2020
    Posts
    39
    Thanks G/R
    7/7
    Trade Feedback
    0 (0%)
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    Hi Icesyhe7,

    Yes, I did press F5 to decompile the subroutine.

    F5.png

    Spell_C_GetSpellCooldown.png

    Well, that's odd. I'm not sure why lua_pushnumber has only one parameter in my idb. Perhaps some new client protection messing with current public dumping methods?

    The above issue with missing parameters was indeed the culprit. Adding the enabled parameter fixed the issue, and now everything works as expected. However, a new issue arises. Why is my IDB missing parameters? I'm using the OverwatchDumpFix method to dump WoW from memory. Is anyone able to confirm a recent change that would cause this behavior?

    Here is the full working code:

    Code:
        uintptr_t Base = reinterpret_cast<uintptr_t>(GetModuleHandle(nullptr));
    
        bool Spell_C_GetSpellCooldown(const uint32_t spellId, const bool isPet, int64_t* duration, int64_t* startTime, bool* enabled, int64_t* modRate)
        {
            return reinterpret_cast<bool(__fastcall*)(const uint32_t, const bool, int64_t*, int64_t*, bool*, int64_t*)>(Base + (uintptr_t)0xB8B610)(spellId, isPet, duration, startTime, enabled, modRate);
        }
    
        uint32_t spellId = 100;
        bool isPet = false, enabled = false;
        int64_t duration = 0, startTime = 0, modRate = 1;
        Spell_C_GetSpellCooldown(spellId, isPet, &duration, &startTime, &enabled, &modRate);
    
        std::string sresult = "Duration: " + std::to_string(duration) + " Start Time: " + std::to_string(startTime) + " Mod Rate: " + std::to_string(modRate);
        std::wstring wresult = std::wstring(sresult.begin(), sresult.end());
    
        MessageBox(sGUI->FindMainWindow(GetCurrentProcessId()), wresult.c_str(), L"Test", MB_OK);
    I added the MessageBox bit at the end for a universal way to verify it works for anyone interested in this issue.

    EDIT:

    Apparently it's common for IDA to mess up on the parameters, particularly when working with the __fastcall convention. I might try updating IDA, perhaps a newer version will perform better in this regard.
    Last edited by CodeBytes; 05-06-2020 at 09:06 PM.

  19. Thanks Corthezz (1 members gave Thanks to CodeBytes for this useful post)
  20. #14
    Icesythe7's Avatar Contributor
    Reputation
    230
    Join Date
    Feb 2017
    Posts
    168
    Thanks G/R
    10/111
    Trade Feedback
    0 (0%)
    Mentioned
    1 Post(s)
    Tagged
    0 Thread(s)
    Originally Posted by CodeBytes View Post
    Hi Icesyhe7,

    Yes, I did press F5 to decompile the subroutine.

    F5.png

    Spell_C_GetSpellCooldown.png

    Well, that's odd. I'm not sure why lua_pushnumber has only one parameter in my idb. Perhaps some new client protection messing with current public dumping methods?

    The above issue with missing parameters was indeed the culprit. Adding the enabled parameter fixed the issue, and now everything works as expected. However, a new issue arises. Why is my IDB missing parameters? I'm using the OverwatchDumpFix method to dump WoW from memory. Is anyone able to confirm a recent change that would cause this behavior?

    Here is the full working code:

    Code:
        uintptr_t Base = reinterpret_cast<uintptr_t>(GetModuleHandle(nullptr));
    
        bool Spell_C_GetSpellCooldown(const uint32_t spellId, const bool isPet, int64_t* duration, int64_t* startTime, bool* enabled, int64_t* modRate)
        {
            return reinterpret_cast<bool(__fastcall*)(const uint32_t, const bool, int64_t*, int64_t*, bool*, int64_t*)>(Base + (uintptr_t)0xB8B610)(spellId, isPet, duration, startTime, enabled, modRate);
        }
    
        uint32_t spellId = 100;
        bool isPet = false, enabled = false;
        int64_t duration = 0, startTime = 0, modRate = 1;
        Spell_C_GetSpellCooldown(spellId, isPet, &duration, &startTime, &enabled, &modRate);
    
        std::string sresult = "Duration: " + std::to_string(duration) + " Start Time: " + std::to_string(startTime) + " Mod Rate: " + std::to_string(modRate);
        std::wstring wresult = std::wstring(sresult.begin(), sresult.end());
    
        MessageBox(sGUI->FindMainWindow(GetCurrentProcessId()), wresult.c_str(), L"Test", MB_OK);
    I added the MessageBox bit at the end for a universal way to verify it works for anyone interested in this issue.

    EDIT:

    Apparently it's common for IDA to mess up on the parameters, particularly when working with the __fastcall convention. I might try updating IDA, perhaps a newer version will perform better in this regard.
    Happy you got it working man, glad we finally got another internal user(external sucks xd) In your spare time try implementing something like imgui so you have a nice user interface

  21. #15
    CodeBytes's Avatar Member
    Reputation
    14
    Join Date
    Feb 2020
    Posts
    39
    Thanks G/R
    7/7
    Trade Feedback
    0 (0%)
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    Already using ImGui--love it. I built a class around it, so I can do something like sGUI->ShowMessage("Useful information") and get a notification in-game. Just have to be careful because Blizzard takes screen captures for analysis.

    My app started as a multiboxing assistant, allowing me to sync all the party member's movements across multiple instances (like garrisons, dungeons etc). But once you start, you can't stop. It seems I only have a retail subscription now in order to keep working on the bot. I use a master program which is external and communicates with the Wow clients using named pipes. The reason why I tried this internal experiment is because I reversed s_spellHistory, found the cooldown information, and then tried to use it with the Warrior's spell "Charge" (Spell ID 100). Of course it didn't work right. Charge has a 1.5 second cooldown but a 20 second recharge rate. So my program failed to accurately measure Charge's cooldown (and I'm sure there are a lot of cases like this).

    I know there is a another class that handles charges, but it starts to get complicated now. With Spell_C_GetCooldown, I get the correct time left without running any other logic. Sounds like a good deal to me.

  22. Thanks Corthezz, Icesythe7 (2 members gave Thanks to CodeBytes for this useful post)
Page 1 of 2 12 LastLast

Similar Threads

  1. Calling functions?
    By wootpeng in forum Diablo 3 Memory Editing
    Replies: 2
    Last Post: 05-31-2013, 07:07 PM
  2. [C#]How to call function
    By RD49 in forum Diablo 3 Memory Editing
    Replies: 2
    Last Post: 10-25-2012, 04:40 AM
  3. [General] Assembler calling function
    By streppel in forum WoW Memory Editing
    Replies: 0
    Last Post: 02-13-2011, 03:45 AM
  4. [C#] Call functions without having to declare delegates
    By bigtimt in forum WoW Memory Editing
    Replies: 12
    Last Post: 05-26-2010, 01:44 AM
  5. [Out of Process] Calling functions in the VTable.
    By cenron in forum WoW Memory Editing
    Replies: 12
    Last Post: 01-31-2009, 08:39 PM
All times are GMT -5. The time now is 09:24 AM. Powered by vBulletin® Version 4.2.3
Copyright © 2024 vBulletin Solutions, Inc. All rights reserved. User Alert System provided by Advanced User Tagging (Pro) - vBulletin Mods & Addons Copyright © 2024 DragonByte Technologies Ltd.
Digital Point modules: Sphinx-based search