修改为playCount不变
UpsertUserAll 默认不再把 userData.playCount / currentPlayCount 加 1。 - 新增 config.PLAY_USER_PLAY_COUNT_GAIN = 0(config.py.example 同步) - PlayData 新增 user_play_count_gain 字段,默认取该常量 - 落库回查改判据:增量 0 时看 lastPlayDate 是否更新,而不是 playCount 是否变大 - 想复刻真机 +1 就把常量或 user_play_count_gain 设成 1 - selftest 相应更新,并新增显式 gain=1 的回归(238 项全绿)
This commit is contained in:
+46
-13
@@ -57,6 +57,13 @@ source/2026-10-05.log:123 逐节点核对:
|
||||
写入后「看起来什么都没发生」,这就是「returnCode=1 但没落库」的根因。
|
||||
差分节点若为空,服务端也看不出本局有任何变化。
|
||||
|
||||
⚠ 本工程的默认值与上表(真机)**有意不同**:
|
||||
config.PLAY_USER_PLAY_COUNT_GAIN = 0(默认)
|
||||
-> userData.playCount / currentPlayCount **保持不变**,不复刻真机的 +1。
|
||||
想复刻真机就把它改成 1。
|
||||
其它增量同理:PLAY_MUSIC_PLAY_COUNT_GAIN / PLAY_MAP_DISTANCE_GAIN /
|
||||
PLAY_CHARACTER_EXP_GAIN 默认都是 0。
|
||||
|
||||
所以本模块的流程是:
|
||||
|
||||
1. 从 snapshot(登录后抓的 Get 响应)建立**基线**
|
||||
@@ -687,7 +694,11 @@ class PlayData:
|
||||
local_play_count: int = field(default_factory=lambda: config.PLAY_LOCAL_PLAY_COUNT)
|
||||
|
||||
# 演算增量
|
||||
#: 曲目历史游玩次数增量(0 = 不回传 userMusicDetailList)
|
||||
#: 用户总游玩次数增量(0 = userData.playCount / currentPlayCount 都不变)
|
||||
#: ★ 默认 0:不再自动 +1。想复刻真机就设成 1。
|
||||
user_play_count_gain: int = field(
|
||||
default_factory=lambda: getattr(config, "PLAY_USER_PLAY_COUNT_GAIN", 0))
|
||||
#: 曲目历史游玩次数增量(0 = 条目照传但次数不变)
|
||||
music_play_count_gain: int = field(
|
||||
default_factory=lambda: config.PLAY_MUSIC_PLAY_COUNT_GAIN)
|
||||
map_distance_gain: int = field(default_factory=lambda: config.PLAY_MAP_DISTANCE_GAIN)
|
||||
@@ -1504,8 +1515,9 @@ def apply_play_effects(
|
||||
|
||||
★ 这是本模块最关键的一步。真机对照(source/2026-10-05.log):
|
||||
|
||||
userData.playCount 1257 -> 1258 (+1)
|
||||
userData.currentPlayCount 185 -> 186 (+1)
|
||||
userData.playCount 1257 -> 1258 (+1) ← 真机
|
||||
userData.currentPlayCount 185 -> 186 (+1) ← 真机
|
||||
★ 本工程默认 PLAY_USER_PLAY_COUNT_GAIN = 0,这两个**不变**。
|
||||
userData.lastPlayDate 旧 -> 本局时刻
|
||||
userData.lastPlayMode 2 -> 0
|
||||
userData.lastLoginDate 旧 -> 本次登录时刻
|
||||
@@ -1550,8 +1562,11 @@ def apply_play_effects(
|
||||
ud["lastRegionId"] = region_id
|
||||
ud["lastClientId"] = client_id
|
||||
ud["lastCountryCode"] = ud.get("lastCountryCode") or "CHN"
|
||||
ud["playCount"] = int(ud.get("playCount", 0) or 0) + 1
|
||||
ud["currentPlayCount"] = int(ud.get("currentPlayCount", 0) or 0) + 1
|
||||
# ★ 默认增量 0:playCount / currentPlayCount 保持不变
|
||||
# (config.PLAY_USER_PLAY_COUNT_GAIN,想复刻真机 +1 就改成 1)
|
||||
_pc_gain = int(getattr(play, "user_play_count_gain", 0) or 0)
|
||||
ud["playCount"] = int(ud.get("playCount", 0) or 0) + _pc_gain
|
||||
ud["currentPlayCount"] = int(ud.get("currentPlayCount", 0) or 0) + _pc_gain
|
||||
ud["lastPlayDate"] = wall # 本局上报时刻
|
||||
ud["lastPlayMode"] = play.play_mode
|
||||
ud["lastLoginDate"] = login_wall # 本次登录时刻
|
||||
@@ -1585,9 +1600,10 @@ def apply_play_effects(
|
||||
# ---------------- userMusicDetailList(差分:本局那条曲目)----------------
|
||||
# ★★ 这个节点**必须回传本局那条**,不能留空 —— 实测证明:
|
||||
# 同一份代码、同一账号,唯一差别是这里留空 vs 回传 1 条:
|
||||
# 回传 1 条 -> returnCode=1 且 userData.playCount 真的 +1(落库)
|
||||
# 留空 -> returnCode=1 但 playCount 纹丝不动(**没落库**)
|
||||
# 回传 1 条 -> returnCode=1 且服务端确实写入(当时靠 playCount +1 观察到)
|
||||
# 留空 -> returnCode=1 但什么都没变(**没落库**)
|
||||
# 即「本局有 playlog 却没有任何曲目明细」会被服务端当成无效包静默丢弃。
|
||||
# ★ 现在 playCount 默认不再 +1,落库判据改看 lastPlayDate(见 verify_upsert_landing)。
|
||||
#
|
||||
# ★ 但 playCount 的**增量**可以自由控制:
|
||||
# config.PLAY_MUSIC_PLAY_COUNT_GAIN = 0(默认)-> 条目照传,次数不变
|
||||
@@ -1794,8 +1810,8 @@ def build_user_all(
|
||||
# PLAY_MUSIC_PLAY_COUNT_GAIN = 0(本局不改次数)且成绩也没刷新时,
|
||||
# 本局那条会被剔光 -> 节点变空。
|
||||
# ★ 实测(同一份代码、同一账号,唯一差别就是这个节点):
|
||||
# 回传 1 条 -> returnCode=1 且 userData.playCount 真的 +1(落库)
|
||||
# 留空 -> returnCode=1 但 playCount 纹丝不动(**整包没生效**)
|
||||
# 回传 1 条 -> returnCode=1 且服务端确实写入
|
||||
# 留空 -> returnCode=1 但什么都没变(**整包没生效**)
|
||||
# 所以这里兜底:只要本局确实打了一首歌,就一定回传它那一条。
|
||||
# 位串给 "0" = 服务器侧已有该 (musicId, level),属于更新而非新增。
|
||||
if not (user_all.get("userMusicDetailList") or []):
|
||||
@@ -2239,12 +2255,29 @@ def verify_upsert_landing(
|
||||
except Exception as exc:
|
||||
log_warning(" 曲目明细回查失败:" + str(exc))
|
||||
|
||||
ok = (int(after_ud.get("playCount") or 0) > int(before_ud.get("playCount") or 0))
|
||||
if ok:
|
||||
log_info("【落库成功】userData.playCount 已 +1")
|
||||
# ★ 判定依据随增量走:
|
||||
# 增量 > 0 -> playCount 应当变大(原来的判据)
|
||||
# 增量 = 0 -> playCount 本来就不该变,改用 lastPlayDate 是否更新来判断
|
||||
_pc_gain = int(getattr(p, "user_play_count_gain", 0) or 0)
|
||||
if _pc_gain:
|
||||
ok = (int(after_ud.get("playCount") or 0)
|
||||
> int(before_ud.get("playCount") or 0))
|
||||
detail = "userData.playCount 已 +" + str(_pc_gain)
|
||||
else:
|
||||
log_error("【疑似未落库】userData.playCount 没有变化,"
|
||||
_b_lpd = str(before_ud.get("lastPlayDate") or "")
|
||||
_a_lpd = str(after_ud.get("lastPlayDate") or "")
|
||||
ok = bool(_a_lpd) and _a_lpd != _b_lpd
|
||||
detail = "userData.lastPlayDate " + _b_lpd + " -> " + _a_lpd
|
||||
|
||||
if ok:
|
||||
log_info("【落库成功】" + detail)
|
||||
else:
|
||||
log_error("【疑似未落库】" + detail + ","
|
||||
"请对照 docs/UpsertUserAll踩坑清单.md 排查")
|
||||
if not _pc_gain:
|
||||
log_warning(" 提示:PLAY_USER_PLAY_COUNT_GAIN=0 时 playCount 不变属正常,"
|
||||
"此处看的是 lastPlayDate;若刚上传的就是同一局,"
|
||||
"lastPlayDate 本来就可能相等(假阴性)")
|
||||
return ok
|
||||
|
||||
|
||||
|
||||
+1
-1
@@ -98,7 +98,7 @@ def show_charges(user_id, label):
|
||||
|
||||
def parse_args(argv):
|
||||
opts = {"qr": "", "charge_id": 5, "send": False, "list": False,
|
||||
"debug": False, "skip_entry": False, "wait_before": 5.0}
|
||||
"debug": False, "skip_entry": False, "wait_before": 30.0}
|
||||
i = 0
|
||||
while i < len(argv):
|
||||
a = argv[i]
|
||||
|
||||
@@ -314,6 +314,18 @@ PLAY_LOCAL_PLAY_COUNT = 8
|
||||
# 改成 0 不会报错,但 playSpecial 的位模式就不是真机那种了。
|
||||
PLAY_SPECIAL_NUM = 1024
|
||||
|
||||
# --- 用户总游玩次数增量 ---
|
||||
#
|
||||
# ★ 默认 0 = **userData.playCount / currentPlayCount 都不变**(本局不计入总次数)。
|
||||
# 想复刻真机的「+1」就改成 1。
|
||||
#
|
||||
# 为什么默认改成 0:
|
||||
# playCount 是账号的**总游玩次数**,是玩家可见的统计值;
|
||||
# 本工程做「成绩上传」时不需要改动它,保持与上传前一致更安全。
|
||||
# ★ 改成 0 之后的副作用:「落库回查」不能再靠 playCount 是否 +1 判断,
|
||||
# 会自动改用 lastPlayDate 是否更新来判断(见 verify_upsert_landing)。
|
||||
PLAY_USER_PLAY_COUNT_GAIN = 0
|
||||
|
||||
# --- 曲目历史游玩次数增量 ---
|
||||
#
|
||||
# ★ 默认 0 = 本局**不改动**该曲目的历史游玩次数。
|
||||
@@ -439,6 +451,7 @@ __all__ = [
|
||||
"PLAY_CHARACTERS", "PLAY_CREDIT", "PLAY_USE_TICKET_ID", "PLAY_MODE",
|
||||
"PLAY_IS_TUTORIAL", "PLAY_IS_EVENT_MODE", "PLAY_IS_NEW_FREE", "PLAY_IS_FREEDOM_MODE",
|
||||
"PLAY_LOCAL_PLAY_COUNT", "PLAY_SPECIAL_NUM",
|
||||
"PLAY_USER_PLAY_COUNT_GAIN",
|
||||
"PLAY_MUSIC_PLAY_COUNT_GAIN", "PLAY_MAP_DISTANCE_GAIN", "PLAY_CHARACTER_EXP_GAIN",
|
||||
"PLAY_SHOP_ITEM_IDS",
|
||||
# 闸门
|
||||
|
||||
+74
-2
@@ -87,7 +87,7 @@ namespace MaimaiNetSpy
|
||||
nameof(PreAddCookie), null);
|
||||
n += PatchBySig(h, "Net.NetHttpClient", "Request",
|
||||
new[] { typeof(byte[]), typeof(string), typeof(string) },
|
||||
nameof(PreRequest), null);
|
||||
nameof(PreRequest), nameof(PostRequestDumpHeaders));
|
||||
n += PatchBySig(h, "Net.NetHttpClient", "GetResponse",
|
||||
Type.EmptyTypes,
|
||||
null, nameof(PostGetResponse));
|
||||
@@ -265,13 +265,85 @@ namespace MaimaiNetSpy
|
||||
// 状态清理交给下一次 PreRequest
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// ★ 真实 HTTP 请求头 dump
|
||||
// ------------------------------------------------------------------
|
||||
//
|
||||
// 为什么必须用 postfix:
|
||||
// PreRequest 跑在 Request() 方法体**之前**,那时
|
||||
// Method / ContentType / UserAgent / Content-Encoding 都还没设置。
|
||||
// 只有 postfix 才能看到最终真正要发出去的头(含框架自带的)。
|
||||
//
|
||||
// 目的:把真机的**完整请求头**抓下来,和纯 Python 脚本发的做逐字节对比。
|
||||
// (排查「报文一致但服务端 returnCode=0」这类问题)
|
||||
private static void PostRequestDumpHeaders(Net.NetHttpClient __instance)
|
||||
{
|
||||
try
|
||||
{
|
||||
var fld = AccessTools.Field(typeof(Net.NetHttpClient), "_request");
|
||||
var wr = (fld == null ? null : fld.GetValue(__instance)) as System.Net.HttpWebRequest;
|
||||
if (wr == null) { Line("| [头 dump] _request 取不到"); return; }
|
||||
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("| ---- ★ 真实 HTTP 请求头(HttpWebRequest,含框架自带)----");
|
||||
sb.AppendLine("| [Method] : " + wr.Method);
|
||||
sb.AppendLine("| [ContentType] : " + wr.ContentType);
|
||||
sb.AppendLine("| [UserAgent] : " + wr.UserAgent);
|
||||
sb.AppendLine("| [ContentLength] : " + wr.ContentLength);
|
||||
sb.AppendLine("| [KeepAlive] : " + wr.KeepAlive);
|
||||
sb.AppendLine("| [Expect] : " + wr.Expect);
|
||||
sb.AppendLine("| [AutomaticDecompression] : " + wr.AutomaticDecompression);
|
||||
sb.AppendLine("| [AllowAutoRedirect] : " + wr.AllowAutoRedirect);
|
||||
try
|
||||
{
|
||||
foreach (string k in wr.Headers.AllKeys)
|
||||
sb.AppendLine("| " + k + ": " + wr.Headers[k]);
|
||||
}
|
||||
catch (Exception e2) { sb.AppendLine("| [Headers 枚举失败] " + e2.Message); }
|
||||
try
|
||||
{
|
||||
sb.AppendLine("| [Cookie 头] : "
|
||||
+ (wr.CookieContainer == null ? "(null container)"
|
||||
: "[" + wr.CookieContainer.GetCookieHeader(wr.RequestUri) + "]"));
|
||||
}
|
||||
catch (Exception e3) { sb.AppendLine("| [Cookie 取失败] " + e3.Message); }
|
||||
Line(sb.ToString());
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Line("| [头 dump 失败] " + e);
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// GetResponse —— 解密后的响应
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
private static void PostGetResponse(ref MemoryStream __result)
|
||||
private static void PostGetResponse(Net.NetHttpClient __instance, ref MemoryStream __result)
|
||||
{
|
||||
_respCount++;
|
||||
|
||||
// ★ 响应头 dump(尤其看 UserLoginApi 的 Set-Cookie: JSESSIONID)
|
||||
try
|
||||
{
|
||||
var fldR = AccessTools.Field(typeof(Net.NetHttpClient), "_response");
|
||||
var resp = (fldR == null ? null : fldR.GetValue(__instance)) as System.Net.HttpWebResponse;
|
||||
if (resp != null)
|
||||
{
|
||||
var sh = new StringBuilder();
|
||||
sh.AppendLine("| ---- ★ 真实 HTTP 响应头 ----");
|
||||
sh.AppendLine("| [StatusCode] : " + (int)resp.StatusCode);
|
||||
try
|
||||
{
|
||||
foreach (string k in resp.Headers.AllKeys)
|
||||
sh.AppendLine("| " + k + ": " + resp.Headers[k]);
|
||||
}
|
||||
catch (Exception e4) { sh.AppendLine("| [响应头枚举失败] " + e4.Message); }
|
||||
Line(sh.ToString());
|
||||
}
|
||||
}
|
||||
catch (Exception e5) { Line("| [响应头 dump 失败] " + e5.Message); }
|
||||
|
||||
try
|
||||
{
|
||||
if (__result == null) return;
|
||||
|
||||
+14
-4
@@ -542,8 +542,18 @@ check("节点顺序与 NODE_ORDER 一致", list(ua2.keys()) == list(NODE_ORDER))
|
||||
|
||||
# userData 演算
|
||||
ud = ua2["userData"][0]
|
||||
check("playCount +1", ud["playCount"] == 1258, str(ud["playCount"]))
|
||||
check("currentPlayCount +1", ud["currentPlayCount"] == 186, str(ud["currentPlayCount"]))
|
||||
# ★ 默认 PLAY_USER_PLAY_COUNT_GAIN = 0 -> 总次数**保持不变**
|
||||
check("playCount 不变(默认增量 0)", ud["playCount"] == 1257, str(ud["playCount"]))
|
||||
check("currentPlayCount 不变(默认增量 0)", ud["currentPlayCount"] == 185,
|
||||
str(ud["currentPlayCount"]))
|
||||
# 显式把增量调回 1 -> 复刻真机 +1
|
||||
_ud_gain = build_user_all(baseline, login_date_time=1791159576,
|
||||
auth_date_time=1791134329, playlog_id=7692848101484135861,
|
||||
play=PlayData(user_play_count_gain=1))["userData"][0]
|
||||
check("显式 user_play_count_gain=1 时 playCount +1", _ud_gain["playCount"] == 1258,
|
||||
str(_ud_gain["playCount"]))
|
||||
check("显式 user_play_count_gain=1 时 currentPlayCount +1",
|
||||
_ud_gain["currentPlayCount"] == 186, str(_ud_gain["currentPlayCount"]))
|
||||
check("lastGameId 兜底为 SDGB", ud["lastGameId"] == "SDGB")
|
||||
check("accessCode 兜底为空串", ud["accessCode"] == "")
|
||||
check("lastPlaceId = config.PLACE_ID", ud["lastPlaceId"] == __import__("config").PLACE_ID)
|
||||
@@ -843,8 +853,8 @@ check("extra_data 插入对应位串",
|
||||
_ua_x["isNewKaleidxScopeList"] == "1", _ua_x["isNewKaleidxScopeList"])
|
||||
check("合并后仍是 36 个节点且顺序不变",
|
||||
list(_ua_x.keys()) == list(NODE_ORDER), str(len(_ua_x)))
|
||||
check("合并没有破坏别的节点(userData.playCount 仍 +1)",
|
||||
_ua_x["userData"][0]["playCount"] == 1258, str(_ua_x["userData"][0]["playCount"]))
|
||||
check("合并没有破坏别的节点(userData.playCount 仍为基线值)",
|
||||
_ua_x["userData"][0]["playCount"] == 1257, str(_ua_x["userData"][0]["playCount"]))
|
||||
|
||||
# 只给数组不给位串 -> 按长度补 "0" 并在说明里提示
|
||||
_ua_only_arr = build_user_all(baseline, login_date_time=1791159576,
|
||||
|
||||
Reference in New Issue
Block a user