-
Notifications
You must be signed in to change notification settings - Fork 7
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Feature Request/Help Wanted] Region Lock Fix + Some Matchmaking QoL #26
Comments
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 The 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. |
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.
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.
Pog, the value in the CE table didn't affect quest search. That's good news! |
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 context->getSteamMatchmaking()->AddRequestLobbyListResultStringFilter(...);
context->getSteamMatchmaking()->AddRequestLobbyListResultNumericalFilter(...);
...
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 The search request gets made when
Don't worry about that too much 😅. That's decompiled code, the call to 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 // ???
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 |
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);
});
} |
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 Btw, I saw in your original post you mentioned not knowing how to do direct memory access, there are a couple examples here. |
Yeah, I was still confused after reading it. I assume that process's base address is already accounted because The first step would be to get the pointer stored at
and
PS. I thought you were joking about ghidra and 50 hours... |
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 ( 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 The base address is not accounted for with If you really want to be ultra safe you can pull in 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. |
I got
I see, thanks. I will try it later. 👍 |
The exe for 15.20 is identical to the one for 15.21. |
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);
});
} |
Uh, not sure what's going on then. I will try this out myself when I get back from work today. |
Added a Steam Matchmaking API with 8b9f619. The way it works is you subscribe to the |
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 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 |
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 |
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). |
Ok, back to public void OnLobbySearch(ref int maxResults)
{
Log.Info($"OnLobbySearch");
} [UPD.] Meanwhile direct hook started working but calling 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);
}); |
Alright, I made some mistakes in my initial commit (lack of testing lol). Should be fixed in 5640882. |
It works now 👍 |
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. |
Hmm, that's what I thought. Thanks for the confirmation! Guess I will settle with 32 lobbies. :/ |
|
Officially implemented with Version 0.0.4 |
Done as of 932ecb3 |
There is an issue with hooking that seems to be related to SPL (or even Reloaded.Hooks), rather than to my mod. Hooking 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 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? |
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". |
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. 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. |
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:
In Rise the devs kindly made a wrapper to this API:
via.network.SessionSteam
which is inherited from bigvia.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:
setLobbyDistanceFilter(UInt32 lobbyDistanceFilter)
is the function that wrapsAddRequestLobbyListDistanceFilter(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 tolobbies 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 toSessionSteam
instance, so I can callsetLobbyDistanceFilter()
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 similarSessionSteam
wrapper and a similarsetLobbyDistanceFilter()
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
Cheat Engine Screenshot
Pointer Chain Screenshot
32 Limit Screenshot - 29/32
Game Error Screenshot
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
The text was updated successfully, but these errors were encountered: