🎮 maimai DX Net层代码差异分析
版本 1.52 → 1.53 网络层完整代码对比
🔐
AES密钥更换
🔑
Token认证机制
🍪
Cookie会话管理
🗑️
PlaylogList删除
📋 变更摘要
1. AES加密密钥更换
Key:
IV:
Mai-Encoding:
a>32bVP7v<63BVLkY[xM>daZ1s9MBP<R → o2U8F6<adcYl25f_qwx_n]5_qxRcbLN>IV:
d6xHIKq]1J]Dt^ue → AL<G:k:X6Vu7@_U]Mai-Encoding:
1.50 → 1.53
2. Token认证机制
登录响应返回 token,后续请求需携带此token
涉及API: UserLogin, UserPreview, UpsertUserAll
涉及API: UserLogin, UserPreview, UpsertUserAll
3. 时间字段变更
dateTime → loginDateTime
登录时记录实际登录时间,登出时使用登录时的时间戳
登录时记录实际登录时间,登出时使用登录时的时间戳
4. Cookie会话管理
NetHttpClient新增CookieContainer支持
OperationManager新增Cookie和Token的存储管理
OperationManager新增Cookie和Token的存储管理
5. Playlog上传方式变更
删除 PacketUploadUserPlaylogList 和 UserPlaylogListRequestVO
Playlog改为在 UpsertUserAll 中通过 userPlaylogList 字段上传
Playlog改为在 UpsertUserAll 中通过 userPlaylogList 字段上传
🔄 认证流程对比
1.52 旧流程:
Login
→
API请求
→
Logout(dateTime)
1.53 新流程:
Login → 获取token
→
API请求(携带token+cookie)
→
Logout(loginDateTime)
📝 详细代码对比
🔐 CipherAES - 加密层
密钥更换
Net/CipherAES.cs
1.52
internal class CipherAES : Singleton<CipherAES>
{
private static readonly int BLOCK_SIZE = 128;
private static readonly int KEY_SIZE = 256;
private static readonly string AesKey = "a>32bVP7v<63BVLkY[xM>daZ1s9MBP<R";
private static readonly string AesIV = "d6xHIKq]1J]Dt^ue";
public static byte[] Encrypt(byte[] data)
{
using (AesManaged aesManaged = new AesManaged())
{
aesManaged.KeySize = KEY_SIZE;
aesManaged.BlockSize = BLOCK_SIZE;
aesManaged.Mode = CipherMode.CBC;
aesManaged.Key = Encoding.UTF8.GetBytes(AesKey);
aesManaged.IV = Encoding.UTF8.GetBytes(AesIV);
aesManaged.Padding = PaddingMode.PKCS7;
return aesManaged.CreateEncryptor()
.TransformFinalBlock(data, 0, data.Length);
}
}
public static byte[] Decrypt(byte[] encryptData)
{
// 同样使用AesKey和AesIV
}
}
1.53
internal class CipherAES : Singleton<CipherAES>
{
private static readonly int BLOCK_SIZE = 128;
private static readonly int KEY_SIZE = 256;
private static readonly string AesKey = "o2U8F6<adcYl25f_qwx_n]5_qxRcbLN>";
private static readonly string AesIV = "AL<G:k:X6Vu7@_U]";
public static byte[] Encrypt(byte[] data)
{
using (AesManaged aesManaged = new AesManaged())
{
aesManaged.KeySize = KEY_SIZE;
aesManaged.BlockSize = BLOCK_SIZE;
aesManaged.Mode = CipherMode.CBC;
aesManaged.Key = Encoding.UTF8.GetBytes(AesKey);
aesManaged.IV = Encoding.UTF8.GetBytes(AesIV);
aesManaged.Padding = PaddingMode.PKCS7;
return aesManaged.CreateEncryptor()
.TransformFinalBlock(data, 0, data.Length);
}
}
public static byte[] Decrypt(byte[] encryptData)
{
// 同样使用AesKey和AesIV
}
}
🌐 NetHttpClient - HTTP客户端
Cookie支持
Net/NetHttpClient.cs - 字段变更
1.52
public class NetHttpClient
{
protected HttpWebRequest _request;
protected HttpWebResponse _response;
protected WaitHandle _waitHandle;
protected RegisteredWaitHandle _timeoutWaitHandle;
protected readonly RequestCachePolicy _cachePolicy;
protected Stream _responseStream;
protected readonly byte[] _buffer = new byte[1024];
protected readonly MemoryStream _temporaryStream;
protected readonly MemoryStream _memoryStream;
protected int _state;
protected byte[] _bytes;
private readonly string DefaultHttpHeaderEncryption = "1.50";
}
1.53
public class NetHttpClient
{
protected HttpWebRequest _request;
protected HttpWebResponse _response;
protected WaitHandle _waitHandle;
protected RegisteredWaitHandle _timeoutWaitHandle;
protected readonly RequestCachePolicy _cachePolicy;
protected Stream _responseStream;
protected readonly byte[] _buffer = new byte[1024];
protected readonly MemoryStream _temporaryStream;
protected readonly MemoryStream _memoryStream;
protected int _state;
protected byte[] _bytes;
protected CookieCollection _cookie;
private readonly string DefaultHttpHeaderEncryption = "1.53";
}
Net/NetHttpClient.cs - Create方法
1.52
public static NetHttpClient Create(string url)
{
NetHttpClient netHttpClient = new NetHttpClient();
try
{
// ... hostBytes初始化 ...
netHttpClient._request =
(WebRequest.Create(url) as HttpWebRequest);
netHttpClient._request.CachePolicy =
netHttpClient._cachePolicy;
netHttpClient.State = 1;
// ... SSL设置 ...
return netHttpClient;
}
catch (Exception)
{
return null;
}
}
1.53
public static NetHttpClient Create(string url)
{
NetHttpClient netHttpClient = new NetHttpClient();
try
{
// ... hostBytes初始化 ...
netHttpClient._request =
(WebRequest.Create(url) as HttpWebRequest);
netHttpClient._request.CookieContainer =
new CookieContainer();
netHttpClient._request.CachePolicy =
netHttpClient._cachePolicy;
netHttpClient.State = 1;
// ... SSL设置 ...
return netHttpClient;
}
catch (Exception)
{
return null;
}
}
Net/NetHttpClient.cs - 新增方法
1.52
public MemoryStream GetResponse()
{
return this._memoryStream;
}
private void SetSuccess(int state)
{
this.WebException = WebExceptionStatus.Success;
this.Error = string.Empty;
this.DestroyReponse();
this.State = state;
}
1.53
public MemoryStream GetResponse()
{
return this._memoryStream;
}
public CookieCollection GetCookie()
{
return this._cookie;
}
public void AddCookie(ulong userId)
{
CookieContainer cookie =
Singleton<OperationManager>.Instance.GetCookie(userId);
if (cookie != null)
{
this._request.CookieContainer = cookie;
}
}
private void SetSuccess(int state)
{
this.WebException = WebExceptionStatus.Success;
this.Error = string.Empty;
this._cookie = this._response.Cookies;
this.DestroyReponse();
this.State = state;
}
🔑 UserLoginApi - 用户登录
Token认证
Net/VO/Mai2/UserLoginRequestVO.cs
1.52
[Serializable]
public class UserLoginRequestVO : VOSerializer
{
public ulong userId;
public string accessCode;
public int regionId;
public int placeId;
public string clientId;
public long dateTime;
public bool isContinue;
public int genericFlag;
}
1.53
[Serializable]
public class UserLoginRequestVO : VOSerializer
{
public ulong userId;
public string accessCode;
public int regionId;
public int placeId;
public string clientId;
public long dateTime;
public long loginDateTime;
public bool isContinue;
public int genericFlag;
public string token;
}
Net/VO/Mai2/UserLoginResponseVO.cs
1.52
[Serializable]
public class UserLoginResponseVO : VOSerializer
{
public int returnCode;
public string lastLoginDate;
public int loginCount;
public int consecutiveLoginCount;
public ulong loginId;
}
1.53
[Serializable]
public class UserLoginResponseVO : VOSerializer
{
public int returnCode;
public string lastLoginDate;
public int loginCount;
public int consecutiveLoginCount;
public ulong loginId;
public long loginDateTime;
public string token;
}
Net/Packet/Mai2/PacketUserLogin.cs - 构造函数
1.52
public PacketUserLogin(
ulong userId,
string acsessCode,
bool isContinue,
int genericFlag,
Action<UserLoginResponseVO> onDone,
Action<PacketStatus> onError = null)
{
this._onDone = onDone;
this._onError = onError;
base.Create(new NetQuery<
UserLoginRequestVO,
UserLoginResponseVO>("UserLoginApi", userId)
{
Request = { userId = userId },
Request = { accessCode = acsessCode },
Request = { regionId = Auth.RegionCode },
Request = { placeId = (int)Auth.LocationId },
Request = { clientId = System.KeychipId.ShortValue },
Request = { dateTime = new DateTimeOffset(
Auth.AuthTime).ToUnixTimeSeconds() },
Request = { isContinue = isContinue },
Request = { genericFlag = genericFlag }
}, NetConfig.TimeOutInMSecLong, -1, -1);
}
1.53
public PacketUserLogin(
ulong userId,
string acsessCode,
bool isContinue,
int genericFlag,
Action<UserLoginResponseVO> onDone,
Action<PacketStatus> onError = null)
{
this._onDone = onDone;
this._onError = onError;
base.Create(new NetQuery<
UserLoginRequestVO,
UserLoginResponseVO>("UserLoginApi", userId)
{
Request = { userId = userId },
Request = { accessCode = acsessCode },
Request = { regionId = Auth.RegionCode },
Request = { placeId = (int)Auth.LocationId },
Request = { clientId = System.KeychipId.ShortValue },
Request = { dateTime = new DateTimeOffset(
Auth.AuthTime).ToUnixTimeSeconds() },
Request = { loginDateTime = new DateTimeOffset(
MAI2.DateTime.Now).ToUnixTimeSeconds() },
Request = { isContinue = isContinue },
Request = { genericFlag = genericFlag },
Request = { token = Singleton<OperationManager>
.Instance.GetToken(userId) }
}, NetConfig.TimeOutInMSecLong, -1, -1);
}
Net/Packet/Mai2/PacketUserLogin.cs - Proc方法
1.52
public override PacketState Proc()
{
PacketState packetState = base.ProcImpl();
if (packetState != PacketState.Done)
{
if (packetState == PacketState.Error)
{
this._onError?.Invoke(base.Status);
// ... error handling ...
}
}
else
{
NetQuery<UserLoginRequestVO, UserLoginResponseVO>
netQuery = base.Query as NetQuery<
UserLoginRequestVO, UserLoginResponseVO>;
this._onDone(netQuery.Response);
}
return base.State;
}
1.53
public override PacketState Proc()
{
PacketState packetState = base.ProcImpl();
if (packetState != PacketState.Done)
{
if (packetState == PacketState.Error)
{
this._onError?.Invoke(base.Status);
// ... error handling ...
}
}
else
{
NetQuery<UserLoginRequestVO, UserLoginResponseVO>
netQuery = base.Query as NetQuery<
UserLoginRequestVO, UserLoginResponseVO>;
netQuery.Response.loginDateTime =
netQuery.Request.loginDateTime;
this._onDone(netQuery.Response);
}
return base.State;
}
📤 UserLogoutApi - 用户登出
时间字段变更
Net/VO/Mai2/UserLogoutRequestVO.cs
1.52
[Serializable]
public class UserLogoutRequestVO : VOSerializer
{
public ulong userId;
public string accessCode;
public int regionId;
public int placeId;
public string clientId;
public long dateTime;
public int type;
}
1.53
[Serializable]
public class UserLogoutRequestVO : VOSerializer
{
public ulong userId;
public string accessCode;
public int regionId;
public int placeId;
public string clientId;
public long loginDateTime;
public int type;
}
Net/Packet/Mai2/PacketUserLogout.cs - 构造函数签名
1.52
public PacketUserLogout(
ulong userId,
LogoutType logoutType,
string acsessCode,
Action onDone,
Action<PacketStatus> onError = null)
{
// ...
netQuery.Request.dateTime = new DateTimeOffset(
Auth.AuthTime).ToUnixTimeSeconds();
// ...
}
1.53
public PacketUserLogout(
int index,
ulong userId,
LogoutType logoutType,
string acsessCode,
Action onDone,
Action<PacketStatus> onError = null)
{
// ...
netQuery.Request.loginDateTime =
Singleton<NetDataManager>.Instance
.GetLoginVO(index).loginDateTime;
// ...
}
👤 GetUserPreviewApi - 用户预览
新增认证字段
Net/VO/Mai2/UserPreviewRequestVO.cs
1.52
[Serializable]
public class UserPreviewRequestVO : VOSerializer
{
public ulong userId;
public string segaIdAuthKey;
}
1.53
[Serializable]
public class UserPreviewRequestVO : VOSerializer
{
public ulong userId;
public string segaIdAuthKey;
public string token;
public string clientId;
}
Net/VO/Mai2/UserPreviewResponseVO.cs
1.52
[Serializable]
public class UserPreviewResponseVO : VOSerializer
{
public ulong userId;
public string userName;
public bool isLogin;
public string lastGameId;
public string lastDataVersion;
public string lastRomVersion;
public string lastLoginDate;
public string lastPlayDate;
public int playerRating;
public int nameplateId;
public int iconId;
public int trophyId;
public int partnerId;
public int frameId;
public int dispRate;
public int totalAwake;
public int isNetMember;
public string dailyBonusDate;
public int headPhoneVolume;
public bool isInherit;
public int banState;
}
1.53
[Serializable]
public class UserPreviewResponseVO : VOSerializer
{
public ulong userId;
public string userName;
public bool isLogin;
public string lastGameId;
public string lastDataVersion;
public string lastRomVersion;
public string lastLoginDate;
public string lastPlayDate;
public int playerRating;
public int nameplateId;
public int iconId;
public int trophyId;
public int partnerId;
public int frameId;
public int dispRate;
public int totalAwake;
public int isNetMember;
public string dailyBonusDate;
public int headPhoneVolume;
public bool isInherit;
public int banState;
public int errorId;
}
Net/Packet/Mai2/PacketGetUserPreview.cs - 构造函数
1.52
public PacketGetUserPreview(
ulong userId,
string authKey,
Action<ulong, UserPreviewResponseVO> onDone,
Action<PacketStatus> onError = null)
{
this._onDone = onDone;
this._onError = onError;
this._userId = userId;
base.Create(new NetQuery<
UserPreviewRequestVO,
UserPreviewResponseVO>("GetUserPreviewApi", userId)
{
Request = { userId = userId },
Request = { segaIdAuthKey = authKey }
}, -1, -1, -1);
}
1.53
public PacketGetUserPreview(
ulong userId,
string authKey,
string token,
Action<ulong, UserPreviewResponseVO> onDone,
Action<PacketStatus> onError = null)
{
this._onDone = onDone;
this._onError = onError;
this._userId = userId;
base.Create(new NetQuery<
UserPreviewRequestVO,
UserPreviewResponseVO>("GetUserPreviewApi", userId)
{
Request = { userId = userId },
Request = { segaIdAuthKey = authKey },
Request = { token = token },
Request = { clientId = System.KeychipId.ShortValue }
}, -1, -1, -1);
}
💾 UpsertUserAllApi - 用户数据上传
Playlog整合
Net/VO/Mai2/UserAllRequestVO.cs
1.52
[Serializable]
public class UserAllRequestVO : VOSerializer
{
public ulong userId;
public ulong playlogId;
public bool isEventMode;
public bool isFreePlay;
public UserAll upsertUserAll;
}
1.53
[Serializable]
public class UserAllRequestVO : VOSerializer
{
public ulong userId;
public ulong playlogId;
public bool isEventMode;
public bool isFreePlay;
public long loginDateTime;
public UserPlaylog[] userPlaylogList;
public UserAll upsertUserAll;
}
Net/Packet/Mai2/PacketUpsertUserAll.cs - 构造函数
1.52
public PacketUpsertUserAll(
int index,
UserData src,
Action<int> onDone,
Action<PacketStatus> onError = null)
: this(index,
UserID.IsGuest(src.Detail.UserID)
? UserID.GuestID(Auth.LocationId)
: src.Detail.UserID,
ToUserAll(src),
onDone,
onError)
{ }
public PacketUpsertUserAll(
int index,
ulong userId,
UserAll src,
Action<int> onDone,
Action<PacketStatus> onError = null)
{
// ...
netQuery.Request.userId = userId;
netQuery.Request.playlogId = loginVO?.loginId ?? 0UL;
netQuery.Request.isEventMode = GameManager.IsEventMode;
netQuery.Request.isFreePlay =
AmManager.Instance.Credit.IsFreePlay();
netQuery.Request.upsertUserAll = src;
// ...
}
1.53
public PacketUpsertUserAll(
int index,
UserData src,
int maxTrackNo,
Action<int> onDone,
Action<PacketStatus> onError = null)
: this(index,
UserID.IsGuest(src.Detail.UserID)
? UserID.GuestID(Auth.LocationId)
: src.Detail.UserID,
ToUserAll(index, src),
Convert(index, src, maxTrackNo),
onDone,
onError)
{ }
public PacketUpsertUserAll(
int index,
ulong userId,
UserAll src,
List<UserPlaylog> userPlaylogList,
Action<int> onDone,
Action<PacketStatus> onError = null)
{
// ...
netQuery.Request.userId = userId;
netQuery.Request.playlogId = loginVO?.loginId
?? NetDataManager.Instance.GetGuestLogId(index);
netQuery.Request.isEventMode = GameManager.IsEventMode;
netQuery.Request.isFreePlay =
AmManager.Instance.Credit.IsFreePlay();
netQuery.Request.loginDateTime =
loginVO?.loginDateTime ?? userPlaylogList[0].loginDate;
netQuery.Request.userPlaylogList = userPlaylogList.ToArray();
netQuery.Request.upsertUserAll = src;
// ...
}
Net/Packet/Mai2/PacketUpsertUserAll.cs - 新增方法
1.52
private static UserAll ToUserAll(UserData src)
{
UserAll result = new UserAll();
src.ExportUserAll(ref result);
return result;
}
1.53
private static UserAll ToUserAll(int index, UserData src)
{
UserAll result = new UserAll();
src.ExportUserAll(index, ref result);
return result;
}
private static List<UserPlaylog> Convert(
int index, UserData src, int maxTrackNo)
{
List<UserPlaylog> list = new List<UserPlaylog>();
for (int i = 0; i < maxTrackNo; i++)
{
list.Add(src.ExportUserPlaylog(index, i));
}
return list;
}
⚙️ OperationManager - 运营管理器
Token/Cookie管理
Manager/OperationManager.cs - 新增using
1.52
using System;
using AMDaemon;
using AMDaemon.Allnet;
using ChimeLib.NET;
using IO;
using MAI2.Util;
using Manager.Operation;
using Net;
using Net.Packet;
using Net.VO.Mai2;
using UnityEngine;
1.53
using System;
using System.Collections.Generic;
using System.Net;
using AMDaemon;
using AMDaemon.Allnet;
using ChimeLib.NET;
using IO;
using MAI2.Util;
using Manager.Operation;
using Net;
using Net.Packet;
using Net.VO.Mai2;
using UnityEngine;
Manager/OperationManager.cs - 字段
1.52
public class OperationManager : Singleton<OperationManager>
{
private readonly Mode<OperationManager, State> _mode;
private readonly OperationData _downloadData;
private readonly OperationData _operationData;
private readonly DataUploader _dataUploader;
private readonly DataDownloaderMai2 _dataDownloader;
private readonly MaintenanceTimer _maintenanceTimer;
private readonly SegaBootTimer _segaBootTimer;
private readonly ClosingTimer _closingTimer;
// ...
}
1.53
public class OperationManager : Singleton<OperationManager>
{
private readonly Mode<OperationManager, State> _mode;
private readonly OperationData _downloadData;
private readonly OperationData _operationData;
private readonly DataUploader _dataUploader;
private readonly DataDownloaderMai2 _dataDownloader;
private readonly Dictionary<ulong, CookieContainer> _cookie;
private readonly Dictionary<ulong, string> _token;
private readonly MaintenanceTimer _maintenanceTimer;
private readonly SegaBootTimer _segaBootTimer;
private readonly ClosingTimer _closingTimer;
// ...
}
Manager/OperationManager.cs - 构造函数
1.52
public OperationManager()
{
this.IsAuthGood = false;
this._mode = new Mode<OperationManager, State>(this);
this._downloadData = new OperationData();
this._operationData = new OperationData();
this._dataUploader = new DataUploader();
this._dataDownloader = new DataDownloaderMai2();
this.ShopData = new ShopInfomation();
this._maintenanceTimer = new MaintenanceTimer();
this._closingTimer = new ClosingTimer();
this._segaBootTimer = new SegaBootTimer();
}
1.53
public OperationManager()
{
this.IsAuthGood = false;
this._mode = new Mode<OperationManager, State>(this);
this._downloadData = new OperationData();
this._operationData = new OperationData();
this._dataUploader = new DataUploader();
this._dataDownloader = new DataDownloaderMai2();
this.ShopData = new ShopInfomation();
this._cookie = new Dictionary<ulong, CookieContainer>();
this._token = new Dictionary<ulong, string>();
this._maintenanceTimer = new MaintenanceTimer();
this._closingTimer = new ClosingTimer();
this._segaBootTimer = new SegaBootTimer();
}
Manager/OperationManager.cs - 新增方法
1.52
// 无Cookie和Token管理方法
1.53
public void ResetCookie()
{
this._cookie.Clear();
}
public void SetCookie(ulong userId, CookieContainer container)
{
if (this._cookie.ContainsKey(userId))
{
this._cookie.Remove(userId);
}
this._cookie.Add(userId, container);
}
public CookieContainer GetCookie(ulong userId)
{
CookieContainer result;
if (this._cookie.TryGetValue(userId, out result))
{
return result;
}
return null;
}
public void ResetToken()
{
this._token.Clear();
}
public void SetToken(ulong userId, string token)
{
if (this._token.ContainsKey(userId))
{
this._token.Remove(userId);
}
this._token.Add(userId, token);
}
public string GetToken(ulong userId)
{
string result;
if (this._token.TryGetValue(userId, out result))
{
return result;
}
return "";
}
🗑️ 已删除的文件
功能移除
Net/Packet/Mai2/PacketUploadUserPlaylogList.cs - 已删除
1.52 (已删除)
public class PacketUploadUserPlaylogList : Packet
{
public PacketUploadUserPlaylogList(
int index,
UserData src,
int maxTrackNo,
Action<int> onDone,
Action<PacketStatus> onError = null)
public PacketUploadUserPlaylogList(
ulong userId,
List<UserPlaylog> userPlaylogList,
Action<int> onDone,
Action<PacketStatus> onError = null)
{
base.Create(new NetQuery<
UserPlaylogListRequestVO,
UpsertResponseVO>("UploadUserPlaylogListApi", userId)
{
Request = { userPlaylogList = userPlaylogList.ToArray() },
Request = { userId = userId }
}, -1, -1, -1);
}
}
1.53
文件已删除
Playlog改为在UpsertUserAll中通过
userPlaylogList字段上传
API端点 "UploadUserPlaylogListApi" 已废弃
Net/VO/Mai2/UserPlaylogListRequestVO.cs - 已删除
1.52 (已删除)
[Serializable]
public class UserPlaylogListRequestVO : VOSerializer
{
public ulong userId;
public UserPlaylog[] userPlaylogList;
}
1.53
文件已删除
userPlaylogList字段已移至UserAllRequestVO中
📊 技术总结
🔐 安全增强
• AES加密密钥和IV已更换 (32字节Key + 16字节IV)
• 新增Token认证机制 - 登录后服务器返回token
• 新增Cookie会话管理 - 使用CookieContainer
• UserPreview请求需要clientId验证机柜身份
• 新增Token认证机制 - 登录后服务器返回token
• 新增Cookie会话管理 - 使用CookieContainer
• UserPreview请求需要clientId验证机柜身份
🔄 API变更
• dateTime字段改为loginDateTime
• 登录响应新增token字段用于后续认证
• 登出方法需要传入index参数
• HTTP头Mai-Encoding从1.50更新为1.53
• UserPreview响应新增errorId字段
• 登录响应新增token字段用于后续认证
• 登出方法需要传入index参数
• HTTP头Mai-Encoding从1.50更新为1.53
• UserPreview响应新增errorId字段
🗑️ 移除功能
• 移除独立的Playlog上传API
• 删除PacketUploadUserPlaylogList类
• 删除UserPlaylogListRequestVO类
• Playlog数据改为在UpsertUserAll中上传
• 删除PacketUploadUserPlaylogList类
• 删除UserPlaylogListRequestVO类
• Playlog数据改为在UpsertUserAll中上传