1- """资源同步器:从 MaaAssistantArknights 提取干员/地图/头像数据 。
1+ """资源同步器:从 MaaAssistantArknights(本地或远程 GitHub)提取数据 。
22
33用法:
4- uv run python -m custom.resources.syncer # 同步全部
5- uv run python -m custom.resources.syncer --maps # 只同步地图
6- uv run python -m custom.resources.syncer --operators # 只同步干员名
7- uv run python -m custom.resources.syncer --avatars "斑点,芬" # 按需下载头像
8-
9- 产出(到 data/):
10- - operator_mapping.json / operator_names.json:干员名 + charId
11- - level_codes.json:关卡代号列表
12- - map/*.json:关卡地图数据(从 Arknights-Tile-Pos 复制)
13- - avatar/{charId}.png:干员部署头像(从 PRTS Wiki 下载,按需)
4+ uv run python -m custom.resources.syncer # 同步全部(本地优先,远程回退)
5+ uv run python -m custom.resources.syncer --all-avatars # 全量下载头像
6+ uv run python -m custom.resources.syncer --avatars "斑点,芬" # 指定干员
7+ uv run python -m custom.resources.syncer --remote # 从 GitHub 下载
148"""
159
1610from __future__ import annotations
1711
1812import json
1913import logging
2014import shutil
15+ import urllib .request
2116from pathlib import Path
2217
2318from custom .utils .runtime_paths import project_root
2419
2520logger = logging .getLogger (__name__ )
2621
22+ # 本地源(开发环境)
2723_MAA_ROOT = Path ("../MaaAssistantArknights" )
28- _BATTLE_DATA = _MAA_ROOT / "resource" / "battle_data.json"
29- _TILE_POS = _MAA_ROOT / "resource" / "Arknights-Tile-Pos"
24+ _BATTLE_DATA_LOCAL = _MAA_ROOT / "resource" / "battle_data.json"
25+ _TILE_POS_LOCAL = _MAA_ROOT / "resource" / "Arknights-Tile-Pos"
3026
31- # PRTS Wiki 头像 URL 模板
27+ # 远程源(用户环境)
28+ _GITHUB = "https://raw.githubusercontent.com/MaaAssistantArknights/MaaAssistantArknights/main"
29+ _BATTLE_DATA_REMOTE = f"{ _GITHUB } /resource/battle_data.json"
30+ _TILE_POS_API = "https://api.github.com/repos/MaaAssistantArknights/MaaAssistantArknights/contents/resource/Arknights-Tile-Pos"
31+
32+ # PRTS Wiki 头像
3233_AVATAR_URL = "https://media.prts.wiki/thumb.php?f=avg_{char_id}.png&w=120"
3334
3435
35- def sync_operators () -> None :
36- """从 battle_data.json 提取干员名 → charId 映射。"""
36+ def _download (url : str , dest : Path ) -> bool :
37+ try :
38+ urllib .request .urlretrieve (url , dest )
39+ return True
40+ except Exception as e : # noqa: BLE001
41+ logger .warning ("下载失败 %s: %s" , url , e )
42+ return False
43+
44+
45+ def _get_battle_data (force_remote : bool = False ) -> dict | None :
46+ """获取 battle_data.json(本地优先,远程回退)。"""
47+ if not force_remote and _BATTLE_DATA_LOCAL .exists ():
48+ return json .loads (_BATTLE_DATA_LOCAL .read_text (encoding = "utf-8" ))
49+
50+ logger .info ("从 GitHub 下载 battle_data.json..." )
51+ tmp = project_root () / "data" / ".battle_data.json"
52+ tmp .parent .mkdir (parents = True , exist_ok = True )
53+ if _download (_BATTLE_DATA_REMOTE , tmp ):
54+ return json .loads (tmp .read_text (encoding = "utf-8" ))
55+ return None
56+
57+
58+ # --- 干员 ---
59+
60+
61+ def sync_operators (force_remote : bool = False ) -> None :
3762 data_dir = project_root () / "data"
3863 data_dir .mkdir (parents = True , exist_ok = True )
3964
40- if not _BATTLE_DATA .exists ():
41- logger .error ("battle_data.json 不存在: %s" , _BATTLE_DATA )
65+ raw = _get_battle_data (force_remote )
66+ if raw is None :
67+ logger .error ("无法获取 battle_data.json" )
4268 return
4369
44- raw = json .loads (_BATTLE_DATA .read_text (encoding = "utf-8" ))
4570 chars = raw .get ("chars" , {})
46-
4771 mapping : dict [str , str ] = {}
4872 names : list [dict [str , str ]] = []
4973 for char_id , info in chars .items ():
@@ -61,7 +85,6 @@ def sync_operators() -> None:
6185 )
6286
6387 names .sort (key = lambda x : (- int (x ["rarity" ]), x ["name" ]))
64-
6588 (data_dir / "operator_mapping.json" ).write_text (
6689 json .dumps (mapping , indent = 2 , ensure_ascii = False ), encoding = "utf-8"
6790 )
@@ -71,104 +94,147 @@ def sync_operators() -> None:
7194 logger .info ("干员数据: %d 名" , len (names ))
7295
7396
74- def sync_maps () -> None :
75- """复制 Arknights-Tile-Pos 地图 JSON 到 data/map/(跳过 #f# 翻转变体)。"""
97+ # --- 地图 ---
98+
99+
100+ def sync_maps (force_remote : bool = False ) -> None :
76101 data_dir = project_root () / "data"
77102 map_dir = data_dir / "map"
78103 map_dir .mkdir (parents = True , exist_ok = True )
79104
80- if not _TILE_POS .exists ():
81- logger .error ("Arknights-Tile-Pos 不存在: %s" , _TILE_POS )
82- return
105+ if not force_remote and _TILE_POS_LOCAL .exists ():
106+ count = _copy_maps_local (_TILE_POS_LOCAL , map_dir )
107+ else :
108+ count = _download_maps_remote (map_dir )
83109
84- count = 0
110+ # 生成 level_codes
85111 codes : dict [str , str ] = {}
86- for src in _TILE_POS .glob ("*.json" ):
87- if "#f#" in src .name :
112+ for p in map_dir .glob ("*.json" ):
113+ if "#f#" in p .name :
88114 continue
89- dst = map_dir / src .name
90- shutil .copy2 (src , dst )
91- count += 1
92- code = src .name .split ("-" )[0 ]
115+ code = p .name .split ("-" )[0 ]
93116 if code not in codes :
94- codes [code ] = src .name
95-
96- sorted_codes = dict (sorted (codes .items ()))
117+ codes [code ] = p .name
97118 (data_dir / "level_codes.json" ).write_text (
98- json .dumps (sorted_codes , indent = 2 , ensure_ascii = False ), encoding = "utf-8"
99- )
100- logger .info (
101- "地图数据: %d 文件 → data/map/ + level_codes.json (%d 关)" ,
102- count ,
103- len (sorted_codes ),
119+ json .dumps (dict (sorted (codes .items ())), indent = 2 , ensure_ascii = False ),
120+ encoding = "utf-8" ,
104121 )
122+ logger .info ("地图数据: %d 文件 (%d 关)" , count , len (codes ))
105123
106124
107- def sync_avatars (operator_names : list [str ] | None = None ) -> None :
108- """按需下载干员头像到 resource/image/avatar/。
125+ def _copy_maps_local (src_dir : Path , dst_dir : Path ) -> int :
126+ count = 0
127+ for src in src_dir .glob ("*.json" ):
128+ if "#f#" in src .name :
129+ continue
130+ shutil .copy2 (src , dst_dir / src .name )
131+ count += 1
132+ return count
133+
134+
135+ def _download_maps_remote (dst_dir : Path ) -> int :
136+ """从 GitHub API 批量下载地图文件(较慢,仅首次/远程时用)。"""
137+ logger .info ("从 GitHub 下载地图列表..." )
138+ try :
139+ with urllib .request .urlopen (_TILE_POS_API ) as resp :
140+ files = json .loads (resp .read ())
141+ except Exception as e : # noqa: BLE001
142+ logger .error ("无法获取地图列表: %s" , e )
143+ return 0
144+
145+ count = 0
146+ total = len (files )
147+ for i , f in enumerate (files ):
148+ if f ["type" ] != "file" or "#f#" in f ["name" ]:
149+ continue
150+ dst = dst_dir / f ["name" ]
151+ if dst .exists ():
152+ count += 1
153+ continue
154+ download_url = f ["download_url" ]
155+ if _download (download_url , dst ):
156+ count += 1
157+ if (i + 1 ) % 200 == 0 :
158+ logger .info ("地图下载进度: %d/%d" , i + 1 , total )
159+ return count
160+
161+
162+ # --- 头像 ---
163+
164+
165+ def sync_avatars (
166+ operator_names : list [str ] | None = None ,
167+ all_avatars : bool = False ,
168+ ) -> None :
169+ """下载干员头像。
109170
110171 Args:
111- operator_names: 要下载的干员名列表。None = 下载全部(慢)。
172+ operator_names: 指定干员名列表。
173+ all_avatars: 下载全部头像。
112174 """
113- import urllib .request
114-
115175 data_dir = project_root () / "data"
116176 avatar_dir = project_root () / "resource" / "image" / "avatar"
117177 avatar_dir .mkdir (parents = True , exist_ok = True )
118178
119- # 加载映射
120179 mapping_path = data_dir / "operator_mapping.json"
121180 if not mapping_path .exists ():
122- logger .error ("operator_mapping.json 不存在,请先运行 --operators " )
181+ logger .error ("operator_mapping.json 不存在,请先同步干员 " )
123182 return
124183 mapping : dict [str , str ] = json .loads (mapping_path .read_text (encoding = "utf-8" ))
125184
126- if operator_names is None :
185+ if all_avatars :
127186 targets = list (mapping .items ())
187+ elif operator_names :
188+ targets = [(n , mapping [n ]) for n in operator_names if n in mapping ]
128189 else :
129- targets = [(name , mapping [name ]) for name in operator_names if name in mapping ]
190+ logger .info ("未指定头像范围(用 --all-avatars 或 --avatars '名1,名2')" )
191+ return
130192
193+ total = len (targets )
131194 downloaded = 0
132- for name , char_id in targets :
195+ for i , ( _name , char_id ) in enumerate ( targets ) :
133196 out_path = avatar_dir / f"{ char_id } .png"
134197 if out_path .exists ():
135- continue # 已存在跳过
136-
198+ continue
137199 url = _AVATAR_URL .format (char_id = char_id )
138- try :
139- urllib .request .urlretrieve (url , out_path )
200+ if _download (url , out_path ):
140201 downloaded += 1
141- logger .info ("下载头像: %s (%s)" , name , char_id )
142- except Exception as e : # noqa: BLE001
143- logger .warning ("下载失败 %s: %s" , name , e )
202+ if (i + 1 ) % 100 == 0 :
203+ logger .info ("头像进度: %d/%d (新下载 %d)" , i + 1 , total , downloaded )
204+
205+ existing = len (list (avatar_dir .glob ("*.png" )))
206+ logger .info ("头像完成: 新下载 %d, 总计 %d 张" , downloaded , existing )
207+
144208
145- logger . info ( "头像下载完成: %d 张 (共 %d 目标)" , downloaded , len ( targets ))
209+ # --- 统一入口 ---
146210
147211
148- def sync_all () -> None :
149- """同步全部资源(干员 + 地图)。"""
150- logger .info ("开始资源同步(源: %s)" , _MAA_ROOT .resolve ())
151- sync_operators ()
152- sync_maps ()
153- logger .info ("资源同步完成。头像按需下载:--avatars '干员名1,干员名2'" )
212+ def sync_all (force_remote : bool = False ) -> None :
213+ logger .info ("资源同步%s..." , "(远程)" if force_remote else "" )
214+ sync_operators (force_remote )
215+ sync_maps (force_remote )
216+ logger .info ("资源同步完成" )
154217
155218
156219if __name__ == "__main__" :
157220 import argparse
158221
159222 logging .basicConfig (level = logging .INFO , format = "%(levelname)s: %(message)s" )
160223 parser = argparse .ArgumentParser (description = "资源同步器" )
161- parser .add_argument ("--operators" , action = "store_true" , help = "只同步干员名" )
162- parser .add_argument ("--maps" , action = "store_true" , help = "只同步地图" )
163- parser .add_argument ("--avatars" , type = str , default = None , help = "下载头像(干员名逗号分隔)" )
224+ parser .add_argument ("--operators" , action = "store_true" )
225+ parser .add_argument ("--maps" , action = "store_true" )
226+ parser .add_argument ("--all-avatars" , action = "store_true" , help = "下载全部头像" )
227+ parser .add_argument ("--avatars" , type = str , help = "指定干员名(逗号分隔)" )
228+ parser .add_argument ("--remote" , action = "store_true" , help = "强制从 GitHub 下载" )
164229 args = parser .parse_args ()
165230
166- if args .avatars is not None :
167- names = [n .strip () for n in args .avatars .split ("," ) if n .strip ()]
168- sync_avatars (names )
231+ if args .all_avatars :
232+ sync_avatars (all_avatars = True )
233+ elif args .avatars :
234+ sync_avatars ([n .strip () for n in args .avatars .split ("," ) if n .strip ()])
169235 elif args .operators :
170- sync_operators ()
236+ sync_operators (args . remote )
171237 elif args .maps :
172- sync_maps ()
238+ sync_maps (args . remote )
173239 else :
174- sync_all ()
240+ sync_all (args . remote )
0 commit comments