Fexty12573 / SharpPluginLoader

A C# plugin loader for Monster Hunter World
MIT License
32 stars 2 forks source link

[Feature Request/Help Wanted] Region Lock Fix + Some Matchmaking QoL #26

Closed GreenComfyTea closed 5 months ago

GreenComfyTea commented 6 months ago

Region Lock Fix

SPL hooks to a lot of in-game functions, which is really cool. I assume you have some good way or just good knowledge and experience of how to find those functions. Therefore, I assume there is chance you might find the function that will disable Region Lock.

In Rise it is done by literally 1 function call. By visually observing how World and Rise behave, I make a big assumption that the way it is handled in World is the same as in Rise, which would mean a function of same functionality have a high chance of existing. And perhaps there is a chance you may find it.

How it works in Rise

Both in Rise and probably World (I am 99% sure) Join Request (SOS) discovery is provided thru Steam Matchmaking API (Docs) and after that the games establish p2p connection. The most interesting part in the API is LobbyDistanceFilter (Docs). It literally defines how far from your Steam region the Matchmaking API will search for a match.

As per docs it can have 4 states:

Name Value Description
k_ELobbyDistanceFilterClose 0 Only lobbies in the same immediate region will be returned.
k_ELobbyDistanceFilterDefault 1 Only lobbies in the same region or nearby regions will be returned. DEFAULT.
k_ELobbyDistanceFilterFar 2 For games that don't have many latency requirements, will return lobbies about half-way around the globe.
k_ELobbyDistanceFilterWorldwide 3 No filtering, will match lobbies as far as India to NY (not recommended, expect multiple seconds of latency between the clients).

In Rise the devs kindly made a wrapper to this API: via.network.SessionSteam which is inherited from big via.Network.SessionBase base class. This is a sensible thing to do, right? Having a wrapper for the API you are using? I believe a similar wrapper should exist in World too.

In Rise: image

setLobbyDistanceFilter(UInt32 lobbyDistanceFilter) is the function that wraps AddRequestLobbyListDistanceFilter(ELobbyDistanceFilter eLobbyDistanceFilter) function in the Matchmaking API.

This function is actually never gets called while playing (maybe it gets called once on game initialization but before REFramework is initialized, so I can't detect this call). Therefore, Steam Matchmaking simply uses the default value = k_ELobbyDistanceFilterDefault, which limits to lobbies in the same region or nearby regions,

The way I did in Rise is by hooking to setIsInvisible(Boolean isInvisible) function. This function is being called before each search for Join Requests. The hook provides me with a reference to SessionSteam instance, so I can call setLobbyDistanceFilter() function in pre function of the hook.

Obviously, hooking onto setIsInvisible() function is not mandatory. SessionSteam instance exists somewhere in session managers as a field. Hooking is just simpler.

I cannot guarantee that in World setIsInvisible() exists, but I strongly believe that a similar SessionSteam wrapper and a similar setLobbyDistanceFilter() function exist!

Matchmaking QoL β„– 1 - Session Search Limit

By default the limit is 20. We actually already know the memory address for it. Cheat Engine table by Marcus101RR already includes a pointer to it. But running cheat engine for such thing is kinda dank. SPL/Plugin is more fit for this task. I am aware that SPL already allows direct memory access, I just haven't figure out how to use it yet (forgive my foolishness πŸ˜…). But, perhaps, this value should be exposed thru SPL API? Otherwise, can you help with translating the CE pointer chain into C# code?

The value itself can be increased to 32 safely. Going further makes the game throw an error.

Default Limit Screenshot - 20/20 ![image](https://github.com/Fexty12573/SharpPluginLoader/assets/30152047/55e8f6e7-d755-4da5-b043-77ae53e7ee82)
Cheat Engine Screenshot ![image](https://github.com/Fexty12573/SharpPluginLoader/assets/30152047/ef57170d-56ca-47f6-96f1-39bd5040b071)
Pointer Chain Screenshot ![image](https://github.com/Fexty12573/SharpPluginLoader/assets/30152047/3ccc0c82-de5a-4487-9508-17a943d8d521)
32 Limit Screenshot - 29/32 ![image](https://github.com/Fexty12573/SharpPluginLoader/assets/30152047/8aba1b32-7019-4e23-9e7b-4d6cf3ee744f)
Game Error Screenshot ![image](https://github.com/Fexty12573/SharpPluginLoader/assets/30152047/ddb79a86-f456-4391-b3ff-c3758560a63c)

Matchmaking QoL β„– 2 - Session Search Filter

A lot of found sessions are just 1/16 - people playing by themselves. When I am not playing with friends, I usually look for most populated session. An option to filter 1/16 sessions out would be nice.

If sessions are handled by Steam Matchmaking API, it probably uses some of those. And if not there still should be a packet going out, defining the search rules, that can be altered?

Matchmaking QoL β„– 3 - Quest Search Limit

By default the limit is 20 too. It is probably stored the same way as session search limit. Again, maybe it should be exposed thru SPL API if found?

Quest Search Screenshot - 20/20 ![image](https://github.com/Fexty12573/SharpPluginLoader/assets/30152047/b30f27e2-8677-4688-b5ab-df354758afc1)
Fexty12573 commented 6 months ago

Unfortunately, MHWs networking system is not as well organized as MHRs. There are no wrappers around Steam API calls.

Specifically for the search filters, the game doesn't even make a direct call to AddRequestLobbyListDistanceFilter, just like you mentioned in Rise. Search filters are all set up in one big function, MtNet::Steam::Context::startRequest.

image

The SteamInternal_ContextInit functions retrieve steam interfaces. There are a couple relevant calls here. The SearchKey* filters are game version and network branch data. Then there is a key SlotPublicOpen which requires the filters to return only lobbies that have at least 1 open slot.

Furthermore, the game is most likely programmed to display an error when the result count is > 32 because steam refuses to return more than 32 results for me. I removed the error and tried setting the value passed to the function to 100 always, but it never returns more than 32 results.

So to change these filters you'd have to hook this function and make Steam API calls yourself. I can provide a C# interface to do so, but I don't know if there really is much use-case. If anything, have an API that lets plugins set specific filter values. Even then, I don't know if that is really something SPL should have responsibility over. That feels more like something to be implemented by a plugin, as a sort of extension.

Fexty12573 commented 6 months ago

Regarding the Quest Search, the same rules go as for the session search itself. It is handled through the same function. Raising the limit up to 32 is possible, anything past that doesn't do anything. image

GreenComfyTea commented 6 months ago

I rewrote the code from the screenshot to better understand what's going on (for myself): https://pastebin.com/fiG9t1GG.

I have a few questions.

1) Do you have any idea why it calls Steam_Internal_ContextInit multiple times? Does it destroy the context after each function call? 2) I can't hook AddRequestLobbyListResultStringFilter, right?

If anything, have an API that lets plugins set specific filter values. Even then, I don't know if that is really something SPL should have responsibility over. That feels more like something to be implemented by a plugin, as a sort of extension.

Yes, of course. A plugin is what i meant. I simply ask for an api/interface because i saw some commits trying to make more update-resistance with less hardcoded stuff. So I assumed that handling it thru SPL can be more reliable than if I read memory/hooked functions directly with hardcoded values in the plugin, because I see these things as highly desired by the community.

Furthermore, the game is most likely programmed to display an error when the result count is > 32 because steam refuses to return more than 32 results for me. I removed the error and tried setting the value passed to the function to 100 always, but it never returns more than 32 results.

Pog, the value in the CE table didn't affect quest search. That's good news!

Fexty12573 commented 6 months ago

Do you have any idea why it calls Steam_Internal_ContextInit multiple times? Does it destroy the context after each function call?

Usually that function is not something you call yourself. Most likely it was wrapped inside some lightweight struct/class. Something like this:

struct SteamContext {
  ISteamMatchmaking* getSteamMatchmaking() { /* SteamInternal_ContextInit call here */ }
  // other stuff
};

And the devs didn't really thing much about what happens behind that get... function and just wrote code like this

context->getSteamMatchmaking()->AddRequestLobbyListResultStringFilter(...);
context->getSteamMatchmaking()->AddRequestLobbyListResultNumericalFilter(...);
...

I can't hook AddRequestLobbyListResultStringFilter, right?

You could, tho I wouldn't recommend it tbh. The cleaner way would be to hook that big function I was talking about, which is at 0x1421e2430. It gets a MtNetRequest object passed to it which contains a mPhase member at offset 0xE0.

The search request gets made when mPhase is equal to 1, but due to some compiler optimizations you'd likely have to check for mPhase == 0 instead.

I rewrote the code from the screenshot to better understand what's going on (for myself): https://pastebin.com/fiG9t1GG.

Don't worry about that too much πŸ˜…. That's decompiled code, the call to AddFavouriteGame isn't actually that, it calls ISteamUser::GetSteamID, I just couldn't be bothered to create a vtable for it and map it out in ghidra so it looks like that.

And.. the full function body is just under 350 lines long soo yeah.

Some things:

FUN_1421e5790(^local_ownerID, 0xf, s_SearchKey%d_142fc6b08, *arr2[-1]);

This is a call to snprintf with the format string "SearchKey%d"

// ???
if(*(char *) (param_1 + 0x30) != '\0') {
    *(int *) (param_1 + 0x34) -= 1;
    LeaveCriticalSection((LPCRITICAL_SECTION)(param_1 + 8));
}

This is purely for synchronization purposes, the weird != '\0' check is just a bool check for if that specific structure is "thread safe", i.e. does it have a critical section object. There is a corresponding EnterCriticalSection call up above.

GreenComfyTea commented 6 months ago

image

Interesting, thanks for detailed explanation.

Ghidra is taking long time to do analyzing... It looks confusing anyway. For now I've tried this, but it leads to a crash (I assume because there should be more parameters).

private delegate void StartRequestDelegate(nint mtNetRequestPointer);
private Hook<StartRequestDelegate> _startRequestDelegate;

...

public void OnLoad()
{
        _startRequestDelegate = Hook.Create<StartRequestDelegate>(0x1421e2430, (mtNetRequestPointer) =>
        {
            Log.Info($"{mtNetRequestPointer.ToString()}: startRequest");

            _startRequestDelegate.Original(mtNetRequestPointer);
        });
}
Fexty12573 commented 6 months ago

Yes, that function takes 2 parameters.

You could write it like so

private delegate int StartRequestDelegate(nint netCore, MtObject netRequest);
private MarshallingHook<StartRequestDelegate> _startRequestHook;

public void OnLoad()
{
    _startRequestHook = MarshallingHook.Create<StartRequestDelegate>(0x1421e2430, (netCore, netRequest) =>
    {
        Log.Info($"StartRequest: Phase={netRequest.Get<uint>(0xE0)}");
        return _startRequestHook.Original(netCore, netRequest);
    });
}

Also requires a using SharpPluginLoader.Core.Experimental; for the MarshallingHook class.

Btw, I saw in your original post you mentioned not knowing how to do direct memory access, there are a couple examples here.

GreenComfyTea commented 6 months ago

Yeah, I was still confused after reading it.

I assume that process's base address is already accounted because MemoryUtil is for reading game's memory.

For this: image image

The first step would be to get the pointer stored at base address + 51C2478, right? I tried: int value = MemoryUtil.Read<int>(0x51C2478); and nint value = MemoryUtil.Read<nint>(0x51C2478); and

unsafe
{
    int* pointer = MemoryUtil.ReadPointer<int>(0x51C2478);
    int value = *pointer;
}

and

unsafe
{
    nint* pointer = MemoryUtil.ReadPointer<nint>(0x51C2478);
    nint value = *pointer;
}

All led me to a crash. Image

PS. I thought you were joking about ghidra and 50 hours...

Fexty12573 commented 6 months ago

Regarding ghidra, I wouldn't recommend doing a full analysis from nothing lol. You can download my pre-analyzed binary here. I upload a new one every once in a while. Latest one I uploaded just a couple minutes ago (MonsterHunterWorld_15.20_Fexty_20240219.gzf).

You can just import the .gzf into an existing ghidra project and you'll have all the annotations I made. Additionally you should also download the MonsterHunterWorld.gdt from that folder and import that from the datatype manager window.

The base address is not accounted for with MemoryUtil (or anywhere in SPL, really). For MHW "base address + x" will basically always evaluate to 0x140000000 + x. The only time this isn't the case is if the person running it has ASLR enabled, which personally I've never come across.

If you really want to be ultra safe you can pull in GetModuleHandleA/W from kernel32.dll as a DllImport and call it with either null or "MonsterHunterWorld.exe". That will give you back the base address of the process.

Additionally, to change the max results you should do this instead:

// in startRequest hook
netRequest.GetRef<uint>(0x60) = 32;

Since that other pointer will get overwritten by the value stored in the net request.

GreenComfyTea commented 6 months ago

Regarding ghidra, I wouldn't recommend doing a full analysis from nothing lol. You can download my pre-analyzed binary here. I upload a new one every once in a while. Latest one I uploaded just a couple minutes ago (MonsterHunterWorld_15.20_Fexty_20240219.gzf).

I got MonsterHunterWorld_15.20_Fexty_20240108.gzf but went for full analysis because latest mhw is 15.21...

The base address is not accounted for with MemoryUtil (or anywhere in SPL, really). For MHW "base address + x" will basically always evaluate to 0x140000000 + x. The only time this isn't the case is if the person running it has ASLR enabled, which personally I've never come across.

I see, thanks. I will try it later. πŸ‘

Fexty12573 commented 6 months ago

The exe for 15.20 is identical to the one for 15.21.

GreenComfyTea commented 6 months ago

FYI, because of your help I was able to improve Region Lock Fix for Rise and it now supports Lobbies too! Thank you very much! ❀️

Screenshot ![image](https://github.com/Fexty12573/SharpPluginLoader/assets/30152047/cac95915-4bbb-4590-a50f-6f513aaaebd5)

[UPD.] Fixed Language Lock too. WICKED.

GreenComfyTea commented 6 months ago

With your code I am still crashing when I initiate session search. πŸ€”

private delegate int StartRequestDelegate(nint netCore, MtObject netRequest);
private MarshallingHook<StartRequestDelegate> _startRequestHook;

public void OnLoad()
{
    _startRequestHook = MarshallingHook.Create<StartRequestDelegate>(0x1421e2430, (netCore, netRequest) =>
    {
        Log.Info($"StartRequest: Phase={netRequest.Get<uint>(0xE0)}");
        return _startRequestHook.Original(netCore, netRequest);
    });
}
Fexty12573 commented 6 months ago

Uh, not sure what's going on then. I will try this out myself when I get back from work today.

Fexty12573 commented 6 months ago

Added a Steam Matchmaking API with 8b9f6192ab793c216ab343e93a579ad785a2e31e.

The way it works is you subscribe to the OnLobbySearch event. In that function you can modify the maximum number of results, and add arbitrary filters using functions exposed by the SharpPluginLoader.Core.Steam.Matchmaking class.

GreenComfyTea commented 6 months ago

God's work! Thank you very much! ❀️ ❀️ ❀️

I have issues with my plugin atm. I am confused with new event system. None of the events are firing and Plugin TeaOverlay does not have an entry point is printed in the console.

using SharpPluginLoader.Core;
using ImGuiNET;
using System.Diagnostics;
using SharpPluginLoader.Core.Memory;
using SharpPluginLoader.Core.Experimental;

namespace TeaOverlay;

public class TeaOverlayPlugin : IPlugin
{
    public string Name => $"Tea Overlay v1";

    public string Author => "GreenComfyTea";

    public PluginData Initialize()
    {
        Log.Info("Initialize");
        return new PluginData();
    }

    public void OnLoad()
    {
        Log.Info("OnLoad");
    }

    void IPlugin.OnImGuiRender()
    {
        Log.Info("OnImGuiRender");
    }

    public void OnImGuiFreeRender()
    {
        Log.Info("OnImGuiFreeRender");
    }

    public void OnLobbySearch(ref int maxResults)
    {
        Log.Info("OnLobbySearch");
    }
}

PS. Some examples do not compile because they were not updated for new event system and are missing mandatory Author property.

Fexty12573 commented 6 months ago

That sounds to me like the SPL Core binary was copied to your plugins output directory, and now your plugin depends on the types inside its local instance of SPL, instead of the main one.

Make sure, if you're referencing the assembly directly instead of the NuGet package, that you have <private>false</private> set for it so it doesn't get copied to your out dir.

GreenComfyTea commented 6 months ago

OH SHIT you are right. I am referencing the .dll directly, but it does copy a new core dll into my plugin. I swear it wasn't happening before (at least with newtonjson.dll, so i was copying it manually). *facepalm*

GreenComfyTea commented 6 months ago

Ok, back to OnLobbySearch. All other events work now, but not this one. I get no console output when searching for sessions or quests.

public void OnLobbySearch(ref int maxResults)
{
    Log.Info($"OnLobbySearch");
}

[UPD.] Meanwhile direct hook started working but calling AddRequestLobbyListDistanceFilter causes a crash.

private delegate int SearchLobbiesDelegate(nint netCore, nint netRequest);
private Hook<SearchLobbiesDelegate> SearchLobbiesHook;

SearchLobbiesHook = Hook.Create<SearchLobbiesDelegate>(0x1421e2430, (netCore, netRequest) =>
{
    var phase = MemoryUtil.Read<int>(netRequest + 0xE0);
    if (phase != 0) return SearchLobbiesHook!.Original(netCore, netRequest);

    Log.Info("Searching for Lobbies");
    ref int maxResults = ref MemoryUtil.GetRef<int>(netRequest + 0x60);
    Log.Info("Set Max Result to 32");
    maxResults = 32;

    Log.Info("Trying to set Distance to WorldWide -> Crash");
    Matchmaking.AddRequestLobbyListDistanceFilter(LobbyDistanceFilter.WorldWide);

    return SearchLobbiesHook!.Original(netCore, netRequest);
});
Fexty12573 commented 6 months ago

Alright, I made some mistakes in my initial commit (lack of testing lol). Should be fixed in 5640882cabe7eb3aef2e8afafd382bcbeefcc443.

GreenComfyTea commented 6 months ago

It works now πŸ‘

GreenComfyTea commented 5 months ago

Sup, I want to report about my current progress and ask for further assistance.

So far Region Lock fix, Lobby Count increase and Player Count filter are implemented.

image

I also plan to implement a bypass for Player Type filter, because Any search returns only lobbies with Any option selected, which is not exactly the behavior I would expect. This requires me to hook AddRequestLobbyListNumericalFilter, which I already do successfully. It's just that I basically duplicated your code to get the address.

private delegate void numericalFilter_Delegate(nint steamInterface, nint keyAddress, int value, int comparison);
private MarshallingHook<numericalFilter_Delegate> numericalFilter;

// 0x7FFE2A0B5700
var numericalFilterAddress = SteamApi.GetVirtualFunction(SteamApi.VirtualFunctionIndex.AddRequestLobbyListNumericalFilter);
numericalFilter = MarshallingHook.Create<numericalFilter_Delegate>(numericalFilterAddress, OnNumericalFilter);

SteamApi is the copy-paste class of your Matchmaking. Would be nice if you were to expose Matchmaking.VirtualFunctionIndex enum and Matchmaking.GetVirtualFunction (but with implicit steamInterface).

public static nint GetVirtualFunction(VirtualFunctionIndex functionIndex)
{
    var steamInterface = GetSteamMatchmakingInterface();
    return GetVirtualFunction(steamInterface, functionIndex);
}

Regarding the Quest Search, the same rules go as for the session search itself. It is handled through the same function. Raising the limit up to 32 is possible, anything past that doesn't do anything.

This is actually not true. The hardcap set by Steam Matchmaking API is 50. Setting maxResults to 50 does actually make Matchmaking API return all 50 results. They simply are not being displayed.

SteamAPICall_t RequestLobbyList(); returns a callback with LobbyMatchList_t that has to be iterated with GetLobbyByIndex. That's what the game does in phase 2.

Here is the loop to go over each lobby:

index = 0;
do {
    ppIVar11 = SteamInternal_ContextInit(&PTR_SteamAPI_GetISteamMatchmaking_143e98558);
    (*(code*)(*ppIVar11)->vft->GetLobbyByIndex)(*ppIVar11, &lobbyMatchList, index);
    if ((*(longlong*)(netCore + 0x136d0) == 0) ||
        (*(longlong*)(netCore + 0x358) != lobbyMatchList)) {
        lVar20 = (longlong)index2* 0x250 + netCore;
        cVar7 = FUN_1421dec80(lobbyMatchList, lVar20 + 0x138d8);
        if (cVar7 != '\0') {
            *puVar17 = 0;
            ppIVar11 = SteamInternal_ContextInit(&PTR_SteamAPI_GetISteamMatchmaking_143e98558);
            _Source = (char*)(*(code*)(*ppIVar11)->vft->GetLobbyData)
                (*ppIVar11, lobbyMatchList, s_Name_1434fd8f8);
            strncpy(puVar17 + 8, _Source, 0x20);
            puVar17[0x27] = 0;
            FUN_142197fb0(lVar20 + 0x13a90, 0xf, lVar20 + 0x13a58, 8);
            uVar8 = *(uint*)(netCore + 0x186e0);
            if (((uVar8 & 1) != 0) && (0 < *(int*)(puVar17 + -0x40))) {
                FUN_142204b00(netCore, index, lVar20 + 0x13928);
                uVar8 = *(uint*)(netCore + 0x186e0);
            }
            if ((uVar8 & 2) != 0) {
                puVar10[-1] = *(undefined8*)(puVar17 + -0x18);
                *puVar10 = *(undefined8*)(puVar17 + -0x10);
                puVar10[1] = 0xffff;
                *(undefined*)(puVar10 + 2) = 0;
                FUN_1422065b0(QosChannel::Instance);
            }
            index2 = index2 + 1;
            puVar17 = puVar17 + 0x250;
            puVar10 = puVar10 + 4;
            if ((*(int*)(netCore + 0x186d8) <= index2) || (0x20 < index2)) break;
        }
    }
    index = index + 1;
} while (index < *(uint*)(netCore + 0x1871c));

netCore + 0x1871c (printed as loopLimit) in the while condition corresponds with actual number of lobbies returned by Matchmaking API. Inside the loop there are 2 break conditions. First one is: if(index2 >= netCore + 0x186d8), where netCore + 0x186d8 corresponds with maxResults (printed as breakCondition). Second one is: if(index2 > 32).

With Min Players = 14 and maxResults = 49: image

With Min Players = 1 and maxResults = 49: image

Patching the second condition to if(index2 > 50) works: resultCountSanityCheckPatch2 = new Patch((nint)0x1421e2905, [0x83, 0xfd, 0x32], true);

The loop continues after 32 lobbies and gets lobby data for 33rd and 34rd lobby correctly. But on 34rd lobby after getting lobby data it calls FUN_142204b00 and the game crashes with an error: Fatal error. System.AccessViolationException: Attempted to read or write protected memory. This is often an indication that other memory is corrupt.

The function:

void FUN_142204b00(longlong *param_1,undefined4 param_2,undefined8 param_3)

{
  char cVar1;

  if (*(char *)(param_1 + 6) != '\0') {
    EnterCriticalSection((LPCRITICAL_SECTION)(param_1 + 1));
    *(int *)((longlong)param_1 + 0x34) = *(int *)((longlong)param_1 + 0x34) + 1;
  }
  if (param_1[0x37] != 0) {
    cVar1 = (**(code **)(*param_1 + 0x28))(param_1);
    if (cVar1 == '\0') {
      (**(code **)(*(longlong *)param_1[0x37] + 0x48))((longlong *)param_1[0x37],param_2,param_3);
    }
  }
  if (*(char *)(param_1 + 6) != '\0') {
    *(int *)((longlong)param_1 + 0x34) = *(int *)((longlong)param_1 + 0x34) + -1;
    LeaveCriticalSection((LPCRITICAL_SECTION)(param_1 + 1));
  }
  return;
}

Hooking this function and skipping original call, prevents the crash as expected. No lobbies are shown in the game, but the loop actually stops after 34rd lobby because netCore + 0x186d8 (aka breakCondition) is set to 0 somewhere in the 34rd iteration.

image image

My only assumption is that in netCore there are only so much memory reserved for lobbies, because in the loop it does: lVar20 = (longlong) index2 * 0x250 + netCore; that is later passed to different functions: cVar7 = FUN_1421dec80(local_res10, lVar20 + 0x138d8); FUN_142197fb0(lVar20 + 0x13a90, 0xf, lVar20 + 0x13a58, 8); FUN_142204b00(param_1, index, lVar20 + 0x13928);

Thou it assigns lVar20 data inside FUN_142197fb0 and FUN_142204b00 without a crash. I am clueless how to continue rn.

Fexty12573 commented 5 months ago

The object only holds space for 32 lobbies. By writing more than 32 you're overwriting other data in the object. And after 34 you're going outside of the memory allocated by it. The objects start at 0x13A90, and there's 32 * 0x250 = 0x4A00 bytes for the objects. So anything past 0x18490 is no longer lobby data. The total size of the object is 0x18850.

You can technically change the allocation size, but you'd still have to move the other objects around and adjust offsets which is honestly not a very realistic thing to do.

So realistically there is no sensible way to fix this without for example moving the lobby array to the very end of the object and increasing the allocation size to fit 50 entries. That would also require adjusting every place where the lobbies are accessed to make sure it's using the correct offset.

GreenComfyTea commented 5 months ago

Hmm, that's what I thought. Thanks for the confirmation! Guess I will settle with 32 lobbies. :/

Fexty12573 commented 5 months ago

Matchmaking.GetVirtualFunction is now exposed as of da0b496909d2920abf0dad7f074965045efb349a.

Fexty12573 commented 5 months ago

Officially implemented with Version 0.0.4

GreenComfyTea commented 5 months ago

Ok, one more thing xdd

Can you expose GetSteamMatchmakingInterface(), please? Turns out in online sessions when not host the game calls NumericalFilter a lot with a different interface and it causes a crash, so I need it to filter out irrelevant hook calls.

image image

Fexty12573 commented 5 months ago

Done as of 932ecb358472d5edc5b9b86ba049d513635aa6ed

GreenComfyTea commented 4 months ago

There is an issue with hooking that seems to be related to SPL (or even Reloaded.Hooks), rather than to my mod.

Hooking AddRequestLobbyListNumericalFilter prevents Friend Session Search from working. It seems to not perform the search at all and always say that no sessions found.

The issue seems to be cause by simple fact that the hook exists. If I do nothing inside the hook, just call the original function - the issue persists. Commenting out Hook.Create fixes it. The proper functional hook in the mod works fine in every other aspect.

private delegate void numericalFilter_Delegate(nint steamInterface, nint keyAddress, int value, int comparison);
private Hook<numericalFilter_Delegate> NumericalFilterHook { get; set; }
var numericalFilterAddress = Matchmaking.GetVirtualFunction(Matchmaking.VirtualFunctionIndex.AddRequestLobbyListNumericalFilter);

// Causes Friend Session Search to not work
NumericalFilterHook = Hook.Create<numericalFilter_Delegate>(numericalFilterAddress, (steamInterface, keyAddress, value, comparison) =>
{
    NumericalFilterHook!.Original(steamInterface, keyAddress, value, comparison);
});

Any thoughts?

Fexty12573 commented 4 months ago

No clue tbh. You could try hooking from C++ using something like MinHook or safetyhook and see if it still happens. If it does then it might be some steam "anticheat". Ways around it that I could think of is either try a vtable hook, or mid-function hooks in the places where that function is called. Depending on how many call sites there are that might be really annoying tho.

GreenComfyTea commented 3 months ago

Just a follow up. mid-hooks won't work because I need to skip the call sometimes, as I understand, it can't be done with mid-hooks. And I was not successful with making vtable hook work, because I am dumb, that's why xD.

image

All I know is that the steam interface is different either for 96 or 112 bytes than the one stored in your Steam API when I search for friend sessions, and the rest of the parameters is junk.