Skip to content

Commit

Permalink
Restore the support for Majsoul private rooms.
Browse files Browse the repository at this point in the history
Majsoul public lobby is still prohibited on purpose in order to prevent abuse.
  • Loading branch information
zhangjk95 committed Oct 5, 2021
1 parent f603c6c commit a93f097
Show file tree
Hide file tree
Showing 8 changed files with 166 additions and 45 deletions.
26 changes: 12 additions & 14 deletions MahjongAI/App.config.example
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,12 @@
<appSettings>

<!--
Platform. Only "Tenhou" is supported.
Platform. "Tenhou" or "Majsoul".
We no longer support Majsoul starting from 0.6.64.w due to abuse of
this project.
NOTE: If you choose "Majsoul", you must join a private room by setting
a value for "PrivateRoom" below.
We no longer support Majsoul public lobby starting from 0.6.64.w due to
abuse of this project.
-->
<add key="Platform" value="Tenhou" />

Expand All @@ -26,27 +28,23 @@

<!--
The version and the route you would like to use.
- "cn_domestic": Chinese version with domestic route
- "cn_international": Chinese version with international route
- "cn_international_1": Chinese international version (route #1)
- "cn_international_2": Chinese international version (route #2)
- "cn_international_3": Chinese international version (route #3)
- "cn_international_4": Chinese international version (route #4)
- "cn_international_5": Chinese international version (route #5)
Japanese and English versions are NOT supported. They will be supported
in the future.
Deprecated.
Japanese and English versions are NOT supported.
-->
<add key="MajsoulRegion" value="cn_international" />
<add key="MajsoulRegion" value="cn_international_2" />

<!--
Your Majsoul username. Third-party login is NOT supported.
Deprecated.
-->
<add key="MajsoulUsername" value="your-username" />

<!--
Your Majsoul password.
Deprecated.
-->
<add key="MajsoulPassword" value="your-password" />

Expand Down
7 changes: 5 additions & 2 deletions MahjongAI/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,11 @@ static class Constants
{
public static readonly IReadOnlyDictionary<MajsoulRegion, string> MAJSOUL_API_URL_PRIFIX = new Dictionary<MajsoulRegion, string>()
{
{ MajsoulRegion.CN_DOMESTIC, "https://lb-mainland.majsoul.com:2901/api/v0" },
{ MajsoulRegion.CN_INTERNATIONAL, "https://lb-hk.majsoul.com:7891/api/v0" },
{ MajsoulRegion.CN_INTERNATIONAL_1, "https://lb-hw.maj-soul.com/api/v0" },
{ MajsoulRegion.CN_INTERNATIONAL_2, "https://lb-v2.maj-soul.com:4443/api/v0" },
{ MajsoulRegion.CN_INTERNATIONAL_3, "https://lb-cdn.maj-soul.com/api/v0" },
{ MajsoulRegion.CN_INTERNATIONAL_4, "https://lb-hw.maj-soul.com/api/v0" },
{ MajsoulRegion.CN_INTERNATIONAL_5, "https://lb-sy.maj-soul.com/api/v0" },
};

public const string TENHOU_SERVER_HOST = "133.242.10.78";
Expand Down
4 changes: 1 addition & 3 deletions MahjongAI/MahjongAI.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,6 @@
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Data" />
<Reference Include="System.Xml" />
<Reference Include="websocket-sharp.clone, Version=1.0.2.39869, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\packages\websocket-sharp.clone.3.0.0\lib\net45\websocket-sharp.clone.dll</HintPath>
</Reference>
</ItemGroup>
<ItemGroup>
<Compile Include="AIController.cs" />
Expand Down Expand Up @@ -87,6 +84,7 @@
<Compile Include="TenhouClient.cs" />
<Compile Include="Test.cs" />
<Compile Include="Util\Syanten.cs" />
<Compile Include="Util\WebSocketUtils.cs" />
</ItemGroup>
<ItemGroup>
<None Include="App.config.example" />
Expand Down
77 changes: 55 additions & 22 deletions MahjongAI/MajsoulClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,12 @@
using System.Threading;
using System.Diagnostics;
using System.Net;
using System.Net.WebSockets;

using Newtonsoft.Json.Linq;
using WebSocketSharp;

using MahjongAI.Models;
using MahjongAI.Util;

namespace MahjongAI
{
Expand All @@ -22,12 +23,12 @@ class MajsoulClient : PlatformClient
private const string gameServerListUrlTemplate = "/recommend_list?service=ws-game-gateway&protocol=ws&ssl=true&location={0}";
private const string replaysFileName = "replays.txt";

private WebSocket ws;
private WebSocket wsGame;
private ClientWebSocket ws;
private ClientWebSocket wsGame;
private WebSocketUtils wsUtils = new WebSocketUtils();
private string username;
private string password;
private MajsoulHelper majsoulHelper = new MajsoulHelper();
private byte[] buffer = new byte[1048576];
private IEnumerable<JToken> operationList;
private bool nextReach = false;
private bool gameEnded = false;
Expand All @@ -47,8 +48,13 @@ class MajsoulClient : PlatformClient
public MajsoulClient(Config config) : base(config)
{
var host = getServerHost(serverListUrl);
ws = new WebSocket("wss://" + host, onMessage: OnMessage, onError: OnError);
ws.Connect().Wait();
var url = "wss://" + host;
if (url.EndsWith(":443"))
{
url += "/gateway";
}
ws = wsUtils.Connect(url);
wsUtils.StartRecv(ws, OnMessage, OnError);
username = config.MajsoulUsername;
password = config.MajsoulPassword;
}
Expand All @@ -67,8 +73,8 @@ public override void Close(bool unexpected = false)
InvokeOnClose();
try
{
ws.Close().Wait();
wsGame.Close().Wait();
wsUtils.Close(ws).Wait();
wsUtils.Close(wsGame).Wait();
} catch { }
}
}
Expand All @@ -78,14 +84,28 @@ public override void Login()
{
Send(ws, ".lq.Lobby.login", new
{
currency_platforms = new[] { 2 },
currency_platforms = new[] { 2, 6, 8, 10, 11 },
account = username,
password = EncodePassword(password),
reconnect = false,
device = new { device_type = "pc", browser = "safari" },
random_key = GetDeviceUUID(),
client_version = "0.4.149.w",
client_version = new
{
resource = "0.9.302.w",
},
client_version_string = "web-0.9.302",
gen_access_token = false,
type = 0,
device = new
{
hardware = "pc",
is_browser = true,
os = "windows",
os_version = "win10",
platform = "pc",
sale_platform = "web",
software = "Chrome",
},
}).Wait();
new Task(HeartBeat).Start();
connected = true;
Expand Down Expand Up @@ -135,7 +155,7 @@ public override void EnterPrivateRoom(int roomNumber)
{
if (roomNumber != 0)
{
Send(ws, ".lq.Lobby.joinRoom", new { room_id = roomNumber }).Wait();
Send(ws, ".lq.Lobby.joinRoom", new { room_id = roomNumber, client_version_string = "web-0.9.302" }).Wait();
inPrivateRoom = true;
}
else
Expand All @@ -151,7 +171,7 @@ public override void NextReady()

public override void Bye()
{
wsGame.Close();
wsUtils.Close(ws).Wait();
wsGame = null;
}

Expand Down Expand Up @@ -255,8 +275,14 @@ private void StartGame(JToken data, bool continued)
InvokeOnUnknownEvent("Game found. Connecting...");
while (!gameStarted)
{
wsGame = new WebSocket("wss://" + getServerHost(string.Format(gameServerListUrlTemplate, data["location"])), onMessage: OnMessage, onError: OnError);
wsGame.Connect().Wait();
var host = getServerHost(string.Format(gameServerListUrlTemplate, data["location"]));
var url = "wss://" + host;
if (url.EndsWith(":443"))
{
url += "/game-gateway";
}
wsGame = wsUtils.Connect(url);
wsUtils.StartRecv(wsGame, OnMessage, OnError);
Send(wsGame, ".lq.FastTest.authGame", new
{
account_id = accountId,
Expand Down Expand Up @@ -587,7 +613,14 @@ private void HandleInit(JToken data)
gameData.remainingTile = GameData.initialRemainingTile;

gameData.dora.Clear();
gameData.dora.Add(new Tile((string)data["dora"]));
if (data["doras"] != null)
{
var doras = data["doras"].Select(t => (string)t);
foreach (var dora in doras)
{
gameData.dora.Add(new Tile(dora));
}
}

for (int i = 0; i < 4; i++)
{
Expand Down Expand Up @@ -734,11 +767,10 @@ private void SaveReplayTag(string tag)
writer.Close();
}

private async Task OnMessage(MessageEventArgs args)
private void OnMessage(byte[] buffer, int length)
{
try
{
int length = await args.Data.ReadAsync(buffer, 0, buffer.Length);
MajsoulMessage message = majsoulHelper.decode(buffer, 0, length);
HandleMessage(message);
} catch (Exception ex)
Expand All @@ -748,9 +780,10 @@ private async Task OnMessage(MessageEventArgs args)
}
}

private Task OnError(WebSocketSharp.ErrorEventArgs args)
private void OnError(Exception ex)
{
return Task.Factory.StartNew(() => Close(true));
InvokeOnUnknownEvent(ex.Message);
Close(true);
}

private static string EncodePassword(string password)
Expand All @@ -761,7 +794,7 @@ private static string EncodePassword(string password)
}
}

public async Task Send(WebSocket ws, string methodName, object data)
private async Task Send(ClientWebSocket ws, string methodName, object data)
{
byte[] buffer = majsoulHelper.encode(new MajsoulMessage
{
Expand All @@ -771,7 +804,7 @@ public async Task Send(WebSocket ws, string methodName, object data)
});
try
{
await ws.Send(buffer);
await wsUtils.Send(ws, buffer);
}
catch (Exception ex)
{
Expand Down
7 changes: 5 additions & 2 deletions MahjongAI/Models/MajsoulRegion.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ namespace MahjongAI.Models
{
enum MajsoulRegion
{
CN_DOMESTIC,
CN_INTERNATIONAL,
CN_INTERNATIONAL_1,
CN_INTERNATIONAL_2,
CN_INTERNATIONAL_3,
CN_INTERNATIONAL_4,
CN_INTERNATIONAL_5,
}
}
87 changes: 87 additions & 0 deletions MahjongAI/Util/WebSocketUtils.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
using System;
using System.Threading.Tasks;
using System.Net.WebSockets;
using System.Threading;
using System.Reflection;
using System.Collections;
using System.Net;

namespace MahjongAI.Util
{
class WebSocketUtils
{
private CancellationTokenSource cts = new CancellationTokenSource();

public ClientWebSocket Connect(string uri)
{
var ws = new ClientWebSocket();
AllowSettingRequestHeader();
ws.Options.SetRequestHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.61 Safari/537.36");
ws.ConnectAsync(new Uri(uri), cts.Token).Wait();
return ws;
}

public async Task Send(ClientWebSocket ws, byte[] buffer)
{
await ws.SendAsync(new ArraySegment<byte>(buffer), WebSocketMessageType.Binary, /* endOfMessage */ true, cts.Token);
}

public void StartRecv(ClientWebSocket ws, Action<byte[], int> onMessage, Action<Exception> onError)
{
Task.Factory.StartNew(
async () =>
{
byte[] buffer = new byte[1048576];
while (true)
{
WebSocketReceiveResult result = await ws.ReceiveAsync(new ArraySegment<byte>(buffer), cts.Token);
if (result.CloseStatus.HasValue)
{
if (result.CloseStatus.Value != WebSocketCloseStatus.NormalClosure)
{
onError(new Exception(result.CloseStatus.Value.ToString() + ": " + result.CloseStatusDescription));
}
return;
}
onMessage(buffer, result.Count);
}
}, cts.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default);
}

public async Task Close(ClientWebSocket ws)
{
await ws.CloseAsync(WebSocketCloseStatus.NormalClosure, "", cts.Token);
}

/**
* A hack using reflection to allow setting request header.
* See https://stackoverflow.com/a/58585845.
*/
private static void AllowSettingRequestHeader()
{
Assembly a = typeof(HttpWebRequest).Assembly;
foreach (FieldInfo f in a.GetType("System.Net.HeaderInfoTable").GetFields(BindingFlags.NonPublic | BindingFlags.Static))
{
if (f.Name == "HeaderHashTable")
{
Hashtable hashTable = f.GetValue(null) as Hashtable;
foreach (string sKey in hashTable.Keys)
{
object headerInfo = hashTable[sKey];
foreach (FieldInfo g in a.GetType("System.Net.HeaderInfo").GetFields(BindingFlags.NonPublic | BindingFlags.Instance))
{
if (g.Name == "IsRequestRestricted")
{
bool b = (bool)g.GetValue(headerInfo);
if (b)
{
g.SetValue(headerInfo, false);
}
}
}
}
}
}
}
}
}
1 change: 0 additions & 1 deletion MahjongAI/packages.config
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Newtonsoft.Json" version="12.0.1" targetFramework="net45" />
<package id="websocket-sharp.clone" version="3.0.0" targetFramework="net45" />
</packages>
2 changes: 1 addition & 1 deletion MajsoulHelper/liqi.json

Large diffs are not rendered by default.

0 comments on commit a93f097

Please sign in to comment.