From c0b9a10ae996b2b91d5b147ef1b488c0ea811f3d Mon Sep 17 00:00:00 2001 From: maipulse Date: Wed, 4 Nov 2026 19:32:34 +0800 Subject: [PATCH] release: v1.0.3 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 一键跑图:自选区域推到终点;设置里可关「给」的弹图与音效 --- android/app/build.gradle | 4 +- astral/api/UpsertUserAll.py | 35 +- maipulse/app.py | 68 +- maipulse/core/runmap.py | 254 ++++ maipulse/data/icons/508031.png | Bin 0 -> 27932 bytes maipulse/data/master/map.json | 1927 ++++++++++++++++++++++++++++ maipulse/html/assets/icons/map.svg | 19 + maipulse/html/index.html | 223 +++- 8 files changed, 2502 insertions(+), 28 deletions(-) create mode 100644 maipulse/core/runmap.py create mode 100644 maipulse/data/icons/508031.png create mode 100644 maipulse/data/master/map.json create mode 100644 maipulse/html/assets/icons/map.svg diff --git a/android/app/build.gradle b/android/app/build.gradle index 9b88cd9..b32b594 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -12,8 +12,8 @@ android { minSdk 24 targetSdk 34 // 和网页里的 APP_VERSION 对齐,升级时一起改 - versionCode 102 - versionName "1.0.2" + versionCode 103 + versionName "1.0.3" ndk { abiFilters "arm64-v8a" diff --git a/astral/api/UpsertUserAll.py b/astral/api/UpsertUserAll.py index bcde612..ce843cc 100644 --- a/astral/api/UpsertUserAll.py +++ b/astral/api/UpsertUserAll.py @@ -736,6 +736,12 @@ class PlayData: default_factory=lambda: config.PLAY_MUSIC_PLAY_COUNT_GAIN) map_distance_gain: int = field(default_factory=lambda: config.PLAY_MAP_DISTANCE_GAIN) character_exp_gain: int = field(default_factory=lambda: config.PLAY_CHARACTER_EXP_GAIN) + #: ★ 目标地图:0 = 不切换(沿用服务器下发的 selectMapId,只推进当前选中的那张)。 + #: 非 0 = 先把 userData.selectMapId 切到该 mapId,再把 distance 加在它身上。 + #: 注意:服务端是否接受客户端改 selectMapId 尚未在真机验证 —— 真机样本里 + #: selectMapId 只出现在 Get 侧,Upsert 侧从没被改过。若服务端忽略该字段, + #: 推进仍会落在「服务器当前选中的地图」上,而不是这里指定的那张。 + select_map_id: int = 0 # ---- 里程 / 登录奖励 ---- # ★ 这两个是**登录奖励发放**才产生的,普通游玩结算不回传(留空)。 @@ -1757,6 +1763,11 @@ def apply_play_effects( ud["dateTime"] = int(auth_date_time) if ud.get("banState") is None: ud["banState"] = 0 + # ★ 切区域:只有显式给了目标 mapId 才写入;否则保持服务器下发的 selectMapId。 + # 放在这里(而不是差分段)是为了让下面 userMapList 能读到"切换后"的值。 + _want_map = int(getattr(play, "select_map_id", 0) or 0) + if _want_map: + ud["selectMapId"] = _want_map # ---------------- userExtend ---------------- ext_list = user_all.get("userExtend") or [] @@ -1824,17 +1835,31 @@ def apply_play_effects( if "userMapList" in user_all: if play.map_distance_gain: baseline = user_all.get("_baseline_map") or [] - map_id = None ud = (user_all.get("userData") or [{}])[0] - if isinstance(ud, dict): - map_id = ud.get("selectMapId") + want = int(getattr(play, "select_map_id", 0) or 0) + map_id = want or (ud.get("selectMapId") if isinstance(ud, dict) else None) target = None for item in baseline: if isinstance(item, dict) and item.get("mapId") == map_id: target = copy.deepcopy(item) break - if target is None and baseline and isinstance(baseline[0], dict): - target = copy.deepcopy(baseline[0]) + if target is None: + if map_id: + # ★ 目标地图不在玩家基线里(没进过这个区域): + # 按 GetUserMap 返回的字段形状造一条全新记录,distance 从 0 起算。 + # 这条会因「新状态有、基线没有」在差分环节被保留, + # isNewMapList 自动打 "1"(新增)。 + target = { + "mapId": int(map_id), + "distance": 0, + "isLock": False, + "isClear": False, + "isComplete": False, + "unlockFlag": 0, + } + log_info("userMapList:目标地图 " + str(map_id) + " 不在基线中,按新记录构造") + elif baseline and isinstance(baseline[0], dict): + target = copy.deepcopy(baseline[0]) if target is not None: target["distance"] = int(target.get("distance", 0) or 0) + play.map_distance_gain user_all["userMapList"] = [target] diff --git a/maipulse/app.py b/maipulse/app.py index e4eb201..ce3407c 100644 --- a/maipulse/app.py +++ b/maipulse/app.py @@ -37,6 +37,7 @@ from flask import Flask, jsonify, request, send_file # noqa: E402 import api # noqa: E402 from core import accounts, charge, items, repair, session, shop, unlock, b50, profile # noqa: E402 from core import characters, mileage, history, recommend as recommend_mod # noqa: E402 +from core import runmap # noqa: E402 app = Flask(__name__, static_folder=None) LOCK = threading.Lock() @@ -370,9 +371,13 @@ def api_ds(): @app.post("/api/logout") @guarded def api_logout(): - if not S["logged_in"]: + # ★ 只要还有 token 就去登出:热重启/异常后 logged_in 可能已经是 False, + # 但服务端会话还活着,这时候不踢掉就会卡 isLogin=True,再扫码 returnCode=100。 + if not S.get("logged_in") and not S.get("token"): return fail("未登录") resp = session.logout(S) + S.update({"logged_in": False, "is_login": False, "token": "", + "login_id": "", "login_at": 0}) return ok(returnCode=getattr(resp, "returnCode", None)) @@ -387,11 +392,11 @@ def api_shutdown(): """ info = {} try: - if S.get("logged_in"): + if S.get("logged_in") or S.get("token"): resp = session.logout(S) info["logout"] = getattr(resp, "returnCode", None) else: - info["logout"] = "未登录,跳过" + info["logout"] = "没有会话,跳过" except Exception as exc: # noqa: BLE001 info["logoutError"] = str(exc) threading.Timer(0.8, lambda: os._exit(0)).start() @@ -408,20 +413,24 @@ def api_reload(): 登录态和会话都保住。只重载 core 包内的纯逻辑模块,不碰 api / Flask。 """ import importlib - names = ["core.settle", "core.play", "core.items", "core.unlock", - "core.charge", "core.repair", "core.session", "core.shop", - "core.b50", "core.profile", "core.characters", "core.mileage"] + import pkgutil + import core as _core_pkg + # ★ 自动发现 core 包下所有已加载的模块,别再手写列表: + # 漏一个(比如后加的 runmap)就会出现「改了代码、reload 报成功、实际没生效」。 + names = sorted("core." + m.name for m in pkgutil.iter_modules(_core_pkg.__path__)) done = [] + skipped = [] for name in names: mod = sys.modules.get(name) if mod is None: + skipped.append(name) # 没被 import 过的不用管 continue try: importlib.reload(mod) done.append(name) except Exception as exc: # noqa: BLE001 return fail("重载 %s 失败:%s" % (name, exc)) - return ok(reloaded=done, logged=bool(S.get("logged_in"))) + return ok(reloaded=done, skipped=skipped, logged=bool(S.get("logged_in"))) @app.get("/api/state") @@ -655,6 +664,47 @@ def api_unlock(): return ok(**result) +# ---------------------------------------------------------------- 一键跑图 + +@app.get("/api/runmap") +@guarded +def api_runmap_list(): + """区域列表 + 玩家当前进度(只读)。""" + need_login() + try: + return ok(**runmap.overview(S)) + except Exception as exc: # noqa: BLE001 + return fail(str(exc)) + + +@app.post("/api/runmap") +@guarded +def api_runmap_write(): + """一键跑图:把选中区域推到终点,或推指定距离。默认 dry-run。""" + need_login() + body = request.get_json(silent=True) or {} + try: + map_id = int(body.get("mapId") or 0) + distance = int(body.get("distance") or 0) + step = int(body.get("step") or 0) + except (TypeError, ValueError): + return fail("mapId / distance / step 必须都是数字") + if not map_id: + return fail("没有选区区域") + mode = str(body.get("mode") or "fill") + if mode not in ("fill", "delta"): + return fail("mode 只能是 fill 或 delta") + if mode == "delta" and distance <= 0: + return fail("delta 模式下 distance 必须大于 0") + try: + result = runmap.run(S, map_id, mode=mode, distance=distance, step=step, + dry_run=bool(body.get("dryRun", True)), + cooldown=float(body.get("cooldown", 0))) + return ok(**result) + except Exception as exc: # noqa: BLE001 + return fail(str(exc)) + + # ---------------------------------------------------------------- 小黑屋 @app.post("/api/unstick") @@ -874,8 +924,8 @@ def api_master(): # ---------------------------------------------------------------- 更新 -APP_VERSION = "1.0.2" -APP_BUILD = "2026-10-30" +APP_VERSION = "1.0.3" +APP_BUILD = "2026-11-04" @app.get("/api/version") diff --git a/maipulse/core/runmap.py b/maipulse/core/runmap.py new file mode 100644 index 0000000..9e128bb --- /dev/null +++ b/maipulse/core/runmap.py @@ -0,0 +1,254 @@ +# -*- coding: utf-8 -*- +"""一键跑图:把指定区域的地图进度推到终点。 + +机制(不是独立接口,走 UpsertUserAll 的差分): + userData.selectMapId 指向本次推进的地图 + userMapList 只回传「distance 变了」的那一条 + PlayData.map_distance_gain 每局推进的距离 + +地图表来自游戏包 StreamingAssets/<版本>/map/MapXXXXXX/Map.xml: + TreasureExDatas 里每条奖励带一个 Distance, + Flag=End 那条就是跑完这张图的终点距离。 + +★ 两个现场才看得出来的不确定点(代码已按最乐观实现,需要真实账号验证): + 1. 服务端是否接受客户端改写 selectMapId。真机样本里 selectMapId 只出现在 + Get 侧,Upsert 侧从没被改过。若不接受,推进仍会落在服务器当前选中的图上。 + 2. 一局能走多远。真机一局 11000,跑完 Phigros 要 68 局;一次性 +750000 + 可能被拒绝或截断。所以默认分段(step)推进,而不是一把梭。 +""" +from __future__ import annotations + +import json +import os + +import api + +from . import settle, session + +BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +MAP_JSON = os.path.join(BASE_DIR, "data", "master", "map.json") + +#: 真机一局走过的距离(机台实测值) +STEP_DISTANCE = 11000 + +_CATALOG = None + + +def catalog() -> list: + """全部区域(含终点距离)。按 mapId 升序。""" + global _CATALOG + if _CATALOG is None: + if not os.path.isfile(MAP_JSON): + _CATALOG = [] + else: + with open(MAP_JSON, encoding="utf-8") as fp: + _CATALOG = json.load(fp) + return _CATALOG + + +def find_area(map_id: int) -> dict: + mid = int(map_id) + for row in catalog(): + try: + if int(row.get("mapId") or 0) == mid: + return row + except (TypeError, ValueError): + continue + return {} + + +def progress(snapshot: dict) -> dict: + """从快照取玩家各地图进度:mapId -> {distance, isClear, isComplete, isLock}。 + + ★ 节点可能是「Get 响应包装」{"userMapList": [...]},要先拆一层。 + """ + node = (snapshot or {}).get("userMapList") + if isinstance(node, dict): + node = node.get("userMapList") or [] + out = {} + for row in node or []: + if not isinstance(row, dict): + continue + try: + mid = int(row.get("mapId") or 0) + except (TypeError, ValueError): + continue + if not mid: + continue + out[mid] = { + "distance": int(row.get("distance") or 0), + "isClear": bool(row.get("isClear")), + "isComplete": bool(row.get("isComplete")), + "isLock": bool(row.get("isLock")), + } + return out + + +def overview(state: dict) -> dict: + """区域列表 + 玩家进度,给前端渲染。""" + user_id = int(state["user_id"]) + snapshot = api.GetNeedUserData(user_id, token=state["token"], save=False) + have = progress(snapshot) + select = None + node = snapshot.get("userData") + # ★ Get 侧 userData 是两层嵌套:{"userData": {"userId":…, "userData": {...}, "banState":0}} + # 内层是**单个 dict**,不是 list;Upsert 侧才可能是 list。两种都要兼容。 + if isinstance(node, dict): + inner = node.get("userData") + if isinstance(inner, dict): + node = [inner] + elif isinstance(inner, list): + node = inner + elif isinstance(node, list): + node = node + if isinstance(node, dict): + node = [node] + if isinstance(node, list) and node and isinstance(node[0], dict): + try: + select = int(node[0].get("selectMapId") or 0) or None + except (TypeError, ValueError): + select = None + + areas = [] + for row in catalog(): + mid = int(row.get("mapId") or 0) + cur = have.get(mid) or {} + end = int(row.get("endDistance") or 0) + dist = int(cur.get("distance") or 0) + area = dict(row) + area.update({ + "current": dist, + "cleared": bool(cur.get("isClear") or cur.get("isComplete")), + "entered": mid in have, + "selected": mid == select, + "remain": max(0, end - dist) if end else None, + "percent": (round(dist * 100.0 / end, 1) if end else None), + }) + areas.append(area) + return {"areas": areas, "selectMapId": select, "entered": len(have)} + + +def plan(map_id: int, mode: str = "fill", distance: int = 0, + current: int = 0, step: int = 0) -> list: + """算出要分几段推,每段推多少。返回 [gain, gain, ...]。""" + area = find_area(map_id) + end = int(area.get("endDistance") or 0) + + if mode == "fill": + total = max(0, end - int(current or 0)) + else: + total = max(0, int(distance or 0)) + if total <= 0: + return [] + + step = int(step or 0) + if step <= 0: + # 没指定就按「不超过终点的 1/8、也不超过 10 万」切,避免单包过猛 + cap = max(STEP_DISTANCE, min(100000, (end or total) // 8 or STEP_DISTANCE)) + step = max(STEP_DISTANCE, cap) + + gains, left = [], total + while left > 0: + take = min(step, left) + gains.append(take) + left -= take + return gains + + +def run(state: dict, map_id: int, *, mode: str = "fill", distance: int = 0, + step: int = 0, dry_run: bool = False, cooldown: float = 0.0, + verify: bool = False) -> dict: + """把指定区域推进到终点(mode=fill)或推指定距离(mode=delta)。""" + mid = int(map_id) + area = find_area(mid) + if not area: + raise RuntimeError("没有这个区域:%s" % mid) + + user_id = int(state["user_id"]) + probe = api.GetNeedUserData(user_id, token=state["token"], save=False) + cur = (progress(probe).get(mid) or {}).get("distance") or 0 + gains = plan(mid, mode=mode, distance=distance, current=cur, step=step) + + result = { + "mode": mode, + "mapId": mid, + "name": area.get("name"), + "endDistance": int(area.get("endDistance") or 0), + "before": cur, + "total": sum(gains), + "batches": len(gains), + "done": 0, + "failed": 0, + "dryRun": bool(dry_run), + "returnCode": None, + "errors": [], + "steps": [], + } + if not gains: + result["note"] = "已经是终点,不用推" + return result + + for index, gain in enumerate(gains, 1): + one = _run_once(state, mid, gain, dry_run, cooldown, verify) + result["returnCode"] = one.get("returnCode") + if one.get("error"): + result["failed"] += gain + result["errors"].append("第 %d 段:%s" % (index, one["error"])) + break + result["done"] += gain + result["steps"].append({"seq": index, "gain": gain, + "after": one.get("after"), + "returnCode": one.get("returnCode")}) + if dry_run: + break + result["after"] = result["before"] + result["done"] + return result + + +def _run_once(state: dict, map_id: int, gain: int, + dry_run: bool, cooldown: float, verify: bool = False) -> dict: + """发一局,推进一小段。 + + ★ verify=False(默认):发完不回头读。回读要再拉一次全量快照, + 一段就多几秒,跑几十段时几乎全是等回读。进度靠本地累加, + 真要看落地效果,跑完点一次「读进度」就行。 + """ + user_id = int(state["user_id"]) + session.fresh(state) + who = session.ids() + snapshot = api.GetNeedUserData(user_id, token=state["token"], save=False) + + play_data, playlog = settle.prepare(state, snapshot) + play_data.map_distance_gain = int(gain) + play_data.select_map_id = int(map_id) + + settle.ensure_play_window(state) + settle.upload(state, playlog) + + patch = settle.patch(state, snapshot, play_data) + resp = api.UpsertUserAll( + user_id, state["login_id"], + login_date_time=state["login_date_time"], + auth_date_time=state["date_time"], + token=state["token"], + snapshot=snapshot, + play=play_data, + user_playlog_list=[playlog], + patch=patch, + verify_landing=bool(verify), + auto_characters=False, + place_id=who["place_id"], place_name=who["place_name"], + region_id=who["region_id"], region_name=who["region_name"], + client_id=who["client_id"], + cooldown_sec=float(cooldown), + dry_run=bool(dry_run), + confirm=not dry_run, + ) + one = { + "returnCode": getattr(resp, "returnCode", None), + "apiName": getattr(resp, "apiName", ""), + } + if verify and not dry_run: + after = api.GetNeedUserData(user_id, token=state["token"], save=False) + one["after"] = (progress(after).get(int(map_id)) or {}).get("distance") + return one \ No newline at end of file diff --git a/maipulse/data/icons/508031.png b/maipulse/data/icons/508031.png new file mode 100644 index 0000000000000000000000000000000000000000..ca966eba2e9a87f0535cda98903013bc16167f20 GIT binary patch literal 27932 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 + + + + + + + \ No newline at end of file diff --git a/maipulse/html/index.html b/maipulse/html/index.html index a801772..6feacfd 100644 --- a/maipulse/html/index.html +++ b/maipulse/html/index.html @@ -316,6 +316,7 @@ .card[data-tab]{display:none;grid-column:1/-1} body[data-tab="home"] .card[data-tab="home"], body[data-tab="ticket"] .card[data-tab="ticket"], + body[data-tab="map"] .card[data-tab="map"], body[data-tab="b50"] .card[data-tab="b50"], body[data-tab="risk"] .card[data-tab="risk"], body[data-tab="settings"] .card[data-tab="settings"]{display:block} @@ -556,6 +557,50 @@
+
+

一键跑图

+

推进区域地图进度。走 UpsertUserAll 的 userMapList 差分 —— + 只回传 distance 变化的那一条。

+
+
+ + +
+
+
+
+ + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+ + +
+
+
+
⚠️ 高风险区 @@ -724,6 +769,8 @@
+
+
@@ -774,6 +821,7 @@