Files
Astral/gamehook/Main.cs
T
Txiaochen 1636a79141 feat: WaHappy v0 —— 舞萌DX 纯网络游戏助手(占位版)
当前仅 upsert.py 可用:扫码登录 -> 上传一局成绩 -> 落库回查 -> 登出。

- api/ 下 40+ 个接口模块已实现,236 项零网络自检全绿
- 上传成绩链路已真机跑通,报文与真机抓包逐字节一致
- 开发文档见 docs 分支;source/ 与 logs/ 不入库
2026-10-06 14:42:06 +08:00

377 lines
16 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using HarmonyLib;
using MelonLoader;
// ★ MelonLoader 靠这个特性识别 mod 并调用 OnInitializeMelon。
// 缺了它只会 "Melon Assembly loaded" 但不会实例化(表现为 "N Mods loaded" 不计入)。
[assembly: MelonInfo(typeof(MaimaiNetSpy.Main), "MaimaiNetSpy", "1.0.0", "wahappy")]
[assembly: MelonGame(null, null)]
[assembly: MelonColor(255, 120, 200, 255)]
namespace MaimaiNetSpy
{
/// <summary>
/// MaimaiNetSpy —— hook 舞萌DX 游戏网络层,做实时拦截与落盘。
///
/// 拦截点(全部依据 Assembly-CSharp 的实际签名):
/// Net.NetHttpClient.AddHeader(name, value) 所有请求头
/// Net.NetHttpClient.AddCookie(userId) Cookie
/// Net.NetHttpClient.Request(bytes, ua, method) ★ 加密前的明文 JSON
/// Net.NetHttpClient.GetResponse() 解密后的响应
/// Net.NetPacketUtil.GetUserAgent(query) 逻辑 API 名
/// Manager.GameManager.CalcSpecialNum() playSpecial 的硬件标志位
///
/// 输出:
/// &lt;游戏根目录&gt;/MaimaiNetSpy/YYYY-MM-DD_HHmmss.log 人类可读时间线
/// &lt;游戏根目录&gt;/MaimaiNetSpy/raw/*.json 每个接口的请求/响应原文
/// </summary>
public class Main : MelonMod
{
internal static MelonLogger.Instance Log;
//: 当前请求的上下文(按线程隔离,游戏是异步回调模型)
private static readonly object _lock = new object();
private static string _outDir;
private static StreamWriter _writer;
//: 本次请求累积的头部(AddHeader 逐条调用,Request 时才真正发出)
[ThreadStatic] private static List<KeyValuePair<string, string>> _headers;
[ThreadStatic] private static string _cookie;
[ThreadStatic] private static string _currentApi;
[ThreadStatic] private static string _currentUrl;
//: 统计
private static int _reqCount;
private static int _respCount;
public override void OnInitializeMelon()
{
Log = LoggerInstance;
string root = MelonLoader.Utils.MelonEnvironment.GameRootDirectory;
_outDir = Path.Combine(root, "MaimaiNetSpy");
Directory.CreateDirectory(_outDir);
Directory.CreateDirectory(Path.Combine(_outDir, "raw"));
string file = Path.Combine(_outDir,
DateTime.Now.ToString("yyyy-MM-dd_HHmmss") + ".log");
_writer = new StreamWriter(file, false, new UTF8Encoding(false)) { AutoFlush = true };
Line("=".PadRight(78, '='));
Line(" MaimaiNetSpy 已加载 —— 舞萌DX 网络层实时拦截");
Line(" 日志: " + file);
Line("=".PadRight(78, '='));
try
{
var h = new HarmonyLib.Harmony("MaimaiNetSpy");
int n = 0;
// ★ Request 有两个重载:Request() 与 Request(byte[], string, string)
// 必须按参数类型精确匹配,否则会命中无参版本导致
// "Parameter bytes not found in method Request()"
n += PatchBySig(h, "Net.NetHttpClient", "Create",
new[] { typeof(string) },
nameof(PreCreate), null);
n += PatchBySig(h, "Net.NetHttpClient", "AddHeader",
new[] { typeof(string), typeof(string) },
nameof(PreAddHeader), null);
n += PatchBySig(h, "Net.NetHttpClient", "AddCookie",
new[] { typeof(ulong) },
nameof(PreAddCookie), null);
n += PatchBySig(h, "Net.NetHttpClient", "Request",
new[] { typeof(byte[]), typeof(string), typeof(string) },
nameof(PreRequest), null);
n += PatchBySig(h, "Net.NetHttpClient", "GetResponse",
Type.EmptyTypes,
null, nameof(PostGetResponse));
n += PatchBySig(h, "Net.NetPacketUtil", "GetUserAgent",
new[] { AccessTools.TypeByName("Net.INetQuery") },
null, nameof(PostGetUserAgent));
n += PatchBySig(h, "Manager.GameManager", "CalcSpecialNum",
Type.EmptyTypes,
null, nameof(PostCalcSpecialNum));
n += PatchBySig(h, "Net.NetHttpClient", "SetError",
new[] { typeof(System.Net.WebExceptionStatus), typeof(string),
typeof(int), typeof(System.Net.HttpWebResponse) },
nameof(PreSetError), null);
Line(" 已挂载 " + n + " 个 hook");
}
catch (Exception e)
{
Line(" !! hook 挂载失败: " + e);
}
Line("");
}
public override void OnDeinitializeMelon()
{
try { _writer?.Flush(); _writer?.Dispose(); } catch { }
}
// ------------------------------------------------------------------
// hook 挂载工具
// ------------------------------------------------------------------
/// <summary>
/// 按**参数类型**精确挂载 hook。
///
/// ★ 为什么必须精确匹配:NetHttpClient.Request 有两个重载
/// Boolean Request() —— 只发请求(无体)
/// Boolean Request(Byte[] bytes, String ua, String m) —— 带请求体
/// 用 AccessTools.Method(t, "Request") 会命中前者,
/// 导致 prefix 里的 bytes 参数对不上而抛
/// "Parameter bytes not found in method Request()"。
/// </summary>
private static int PatchBySig(HarmonyLib.Harmony h, string typeName, string methodName,
Type[] paramTypes, string pre, string post)
{
var t = AccessTools.TypeByName(typeName);
if (t == null) { Line(" [skip] 类型未找到: " + typeName); return 0; }
if (paramTypes == null || paramTypes.Any(x => x == null))
{ Line(" [skip] 参数类型解析失败: " + typeName + "." + methodName); return 0; }
var m = AccessTools.Method(t, methodName, paramTypes);
if (m == null)
{
Line(" [skip] 方法未找到: " + typeName + "." + methodName
+ "(" + string.Join(", ", Array.ConvertAll(paramTypes, x => x.Name)) + ")");
return 0;
}
HarmonyMethod preM = pre == null ? null
: new HarmonyMethod(typeof(Main).GetMethod(pre, BindingFlags.Static | BindingFlags.NonPublic));
HarmonyMethod postM = post == null ? null
: new HarmonyMethod(typeof(Main).GetMethod(post, BindingFlags.Static | BindingFlags.NonPublic));
try
{
h.Patch(m, preM, postM);
Line(" [ok] " + typeName + "." + methodName
+ "(" + string.Join(", ", Array.ConvertAll(paramTypes, x => x.Name)) + ")");
return 1;
}
catch (Exception e)
{
Line(" [fail] " + typeName + "." + methodName + " -> " + e.Message);
return 0;
}
}
// ------------------------------------------------------------------
// Create —— 捕获完整 URL(NetHttpClient.Create(url))
// ------------------------------------------------------------------
private static void PreCreate(string url)
{
_currentUrl = url;
}
// ------------------------------------------------------------------
// AddHeader —— 收集请求头
// ------------------------------------------------------------------
private static void PreAddHeader(string name, string value)
{
if (_headers == null) _headers = new List<KeyValuePair<string, string>>();
_headers.Add(new KeyValuePair<string, string>(name, value));
}
private static void PostAddHeader()
{
// 不在这里输出:AddHeader 会被多次调用,等 Request 时统一打印
}
// ------------------------------------------------------------------
// AddCookie —— 记录 Cookie
// ------------------------------------------------------------------
private static void PreAddCookie(ulong userId)
{
_cookie = "userId=" + userId;
}
// ------------------------------------------------------------------
// Request —— ★ 核心:加密前的明文 JSON 在这里
// ------------------------------------------------------------------
private static void PreRequest(byte[] bytes, string userAgent, string method)
{
_reqCount++;
string plain = null;
try
{
if (bytes != null && bytes.Length > 0)
plain = Encoding.UTF8.GetString(bytes);
}
catch { }
var sb = new StringBuilder();
sb.AppendLine();
sb.AppendLine("+" + new string('-', 76));
sb.AppendLine("| [" + _reqCount + "] >>> 请求 "
+ DateTime.Now.ToString("HH:mm:ss.fff"));
sb.AppendLine("+" + new string('-', 76));
sb.AppendLine("| API : " + (_currentApi ?? "(unknown)"));
if (!string.IsNullOrEmpty(_currentUrl))
sb.AppendLine("| URL : " + _currentUrl);
sb.AppendLine("| User-Agent : " + userAgent);
sb.AppendLine("| Method : " + method);
sb.AppendLine("| Cookie : " + (_cookie ?? "(none)"));
sb.AppendLine("| 明文长度 : " + (bytes == null ? 0 : bytes.Length) + " 字节");
if (_headers != null && _headers.Count > 0)
{
sb.AppendLine("| ---- 请求头(AddHeader 收集)----");
foreach (var kv in _headers)
sb.AppendLine("| " + kv.Key + ": " + kv.Value);
}
else
{
sb.AppendLine("| ---- 请求头: (无 AddHeader 调用) ----");
}
if (plain != null)
{
sb.AppendLine("| ---- 请求体(加密前明文)----");
foreach (var ln in Wrap(plain, 150))
sb.AppendLine("| " + ln);
}
Line(sb.ToString());
// 落盘原始明文
try
{
string safe = Sanitize(_currentApi ?? ("req" + _reqCount));
File.WriteAllText(
Path.Combine(_outDir, "raw", _reqCount.ToString("D3") + "_" + safe + "_request.json"),
plain ?? "", new UTF8Encoding(false));
}
catch { }
// 清空累积状态(本次请求已消费)
_headers = null;
}
private static void PostRequest()
{
// 状态清理交给下一次 PreRequest
}
// ------------------------------------------------------------------
// GetResponse —— 解密后的响应
// ------------------------------------------------------------------
private static void PostGetResponse(ref MemoryStream __result)
{
_respCount++;
try
{
if (__result == null) return;
long pos = __result.Position;
__result.Position = 0;
byte[] buf = new byte[__result.Length];
int read = __result.Read(buf, 0, buf.Length);
__result.Position = pos;
string txt = Encoding.UTF8.GetString(buf, 0, read);
var sb = new StringBuilder();
sb.AppendLine("| [" + _respCount + "] <<< 响应 "
+ DateTime.Now.ToString("HH:mm:ss.fff")
+ " (" + read + " 字节)");
foreach (var ln in Wrap(txt, 150))
sb.AppendLine("| " + ln);
Line(sb.ToString());
string safe = Sanitize(_currentApi ?? ("resp" + _respCount));
File.WriteAllText(
Path.Combine(_outDir, "raw", _respCount.ToString("D3") + "_" + safe + "_response.json"),
txt, new UTF8Encoding(false));
}
catch (Exception e)
{
Line("| [响应解析失败] " + e.Message);
}
}
// ------------------------------------------------------------------
// GetUserAgent —— 从 UA 反推 API 名;同时记录 URL
// ------------------------------------------------------------------
private static void PostGetUserAgent(Net.INetQuery query, ref string __result)
{
try
{
_currentApi = query?.Api;
// UA 形如 "<md5>#<userId>"
Line(" [API] " + _currentApi + " UA=" + __result);
}
catch { }
}
// ------------------------------------------------------------------
// CalcSpecialNum —— playSpecial 的硬件标志位
// ------------------------------------------------------------------
private static void PostCalcSpecialNum(ref int __result)
{
Line(" [CalcSpecialNum] = " + __result
+ " (SpecialAP=" + SafeBool(() => Manager.GameManager.SpecialAP)
+ " IsFreedomMode=" + SafeBool(() => Manager.GameManager.IsFreedomMode)
+ ")");
}
private static string SafeBool(Func<bool> f)
{
try { return f().ToString(); } catch { return "?"; }
}
// ------------------------------------------------------------------
// SetError —— 记录网络层错误(含 HTTP 状态码)
// ------------------------------------------------------------------
private static void PreSetError(System.Net.WebExceptionStatus status, string error,
int state, System.Net.HttpWebResponse response)
{
Line(" [NetError] status=" + status + " state=" + state
+ " error=" + error
+ (response != null ? (" http=" + (int)response.StatusCode) : ""));
}
// ------------------------------------------------------------------
// 工具
// ------------------------------------------------------------------
private static string Sanitize(string s)
{
var sb = new StringBuilder();
foreach (char c in s)
sb.Append(char.IsLetterOrDigit(c) || c == '_' || c == '-' ? c : '_');
return sb.ToString();
}
private static IEnumerable<string> Wrap(string s, int width)
{
if (s == null) yield break;
for (int i = 0; i < s.Length; i += width)
yield return s.Substring(i, Math.Min(width, s.Length - i));
}
internal static void Line(string s)
{
lock (_lock)
{
try { _writer?.WriteLine(s); } catch { }
}
}
}
}