@sophiacyl/dsh-plugin-waves 0.0.0-stage → 0.1.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/DSH-GO-VERIFICATION.md +57 -0
- package/ICON.png +0 -0
- package/LICENSE +674 -0
- package/README.md +251 -2
- package/cordis.patch.yml +8 -0
- package/install.ps1 +0 -0
- package/install.sh +209 -0
- package/lib/client.js +1511 -0
- package/lib/host/aliashint.js +70 -0
- package/lib/host/attachments.js +25 -0
- package/lib/host/command.js +34 -0
- package/lib/host/consts.js +16 -0
- package/lib/host/dataguide.js +35 -0
- package/lib/host/imagebridge.js +41 -0
- package/lib/host/native-result.js +91 -0
- package/lib/host/native.js +191 -0
- package/lib/host/proxy.js +59 -0
- package/lib/host/results.js +67 -0
- package/lib/host/service.js +263 -0
- package/lib/host/tool.js +185 -0
- package/lib/host/troubleshoot.js +31 -0
- package/lib/host/worldbook.js +80 -0
- package/lib/index.js +74 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +136 -4
- package/service/ICON.png +0 -0
- package/service/ai/AI_DATA.md +221 -0
- package/service/knowledge/additional/manual.jsonl +5 -0
- package/service/knowledge/wiki/baike.jsonl +1231 -0
- package/service/knowledge/wiki/chengjiu.jsonl +36 -0
- package/service/knowledge/wiki/gonglve.jsonl +383 -0
- package/service/knowledge/wiki/huodong.jsonl +427 -0
- package/service/knowledge/wiki/renwu.jsonl +412 -0
- package/service/knowledge/wiki/tujian.jsonl +2889 -0
- package/service/knowledge/wiki/zixun.jsonl +47 -0
- package/service/plugins/__init__.py +0 -0
- package/service/pyproject.toml +40 -0
- package/service/rover/__init__.py +0 -0
- package/service/rover/ai_core/__init__.py +0 -0
- package/service/rover/ai_core/models.py +50 -0
- package/service/rover/ai_core/rag/__init__.py +5 -0
- package/service/rover/ai_core/rag/knowledge.py +23 -0
- package/service/rover/ai_core/register.py +86 -0
- package/service/rover/ai_core/resource.py +6 -0
- package/service/rover/ai_core/skills/__init__.py +0 -0
- package/service/rover/ai_core/skills/operations.py +8 -0
- package/service/rover/api.py +720 -0
- package/service/rover/api_render.py +161 -0
- package/service/rover/app_life.py +154 -0
- package/service/rover/aps.py +47 -0
- package/service/rover/avatar.py +85 -0
- package/service/rover/bot.py +133 -0
- package/service/rover/builtin.py +152 -0
- package/service/rover/cards/__init__.py +26 -0
- package/service/rover/cards/_shared.py +129 -0
- package/service/rover/cards/char_list.py +258 -0
- package/service/rover/cards/char_panel.py +1353 -0
- package/service/rover/cards/echo_list.py +206 -0
- package/service/rover/cards/matrix_roles.py +25 -0
- package/service/rover/cards/rank_boards.py +714 -0
- package/service/rover/cards/rank_total.py +664 -0
- package/service/rover/compat.py +215 -0
- package/service/rover/config.py +163 -0
- package/service/rover/custom_panel.py +220 -0
- package/service/rover/data_store.py +31 -0
- package/service/rover/gss.py +19 -0
- package/service/rover/handler.py +191 -0
- package/service/rover/help/__init__.py +0 -0
- package/service/rover/help/draw_new_plugin_help.py +332 -0
- package/service/rover/help/model.py +16 -0
- package/service/rover/help/utils.py +7 -0
- package/service/rover/hooks.py +420 -0
- package/service/rover/html_actions.py +236 -0
- package/service/rover/i18n.py +19 -0
- package/service/rover/knowledge.py +235 -0
- package/service/rover/logger.py +102 -0
- package/service/rover/main.py +33 -0
- package/service/rover/media.py +222 -0
- package/service/rover/message_models.py +23 -0
- package/service/rover/models.py +92 -0
- package/service/rover/native_media.py +64 -0
- package/service/rover/ocr.py +79 -0
- package/service/rover/pool.py +43 -0
- package/service/rover/restart.py +36 -0
- package/service/rover/segment.py +143 -0
- package/service/rover/server.py +131 -0
- package/service/rover/status/__init__.py +0 -0
- package/service/rover/status/models.py +10 -0
- package/service/rover/status/plugin_status.py +17 -0
- package/service/rover/subscribe.py +148 -0
- package/service/rover/sv.py +403 -0
- package/service/rover/templates/char_list.html +182 -0
- package/service/rover/templates/char_panel.html +465 -0
- package/service/rover/templates/echo_list.html +221 -0
- package/service/rover/templates/rank_all_total.html +193 -0
- package/service/rover/templates/rank_board.html +107 -0
- package/service/rover/templates/rank_level_total.html +57 -0
- package/service/rover/templates/rank_matrix_total.html +66 -0
- package/service/rover/templates/rank_phantom_total.html +180 -0
- package/service/rover/templates/rank_slash_total.html +38 -0
- package/service/rover/trigger.py +98 -0
- package/service/rover/utils/__init__.py +0 -0
- package/service/rover/utils/boardcast/__init__.py +0 -0
- package/service/rover/utils/boardcast/models.py +15 -0
- package/service/rover/utils/cookie_manager/__init__.py +0 -0
- package/service/rover/utils/cookie_manager/qrlogin.py +24 -0
- package/service/rover/utils/database/__init__.py +0 -0
- package/service/rover/utils/database/base_models.py +1771 -0
- package/service/rover/utils/database/models.py +80 -0
- package/service/rover/utils/database/startup.py +60 -0
- package/service/rover/utils/default_bg/bg.jpg +0 -0
- package/service/rover/utils/download_resource/__init__.py +0 -0
- package/service/rover/utils/download_resource/download_core.py +164 -0
- package/service/rover/utils/download_resource/download_file.py +46 -0
- package/service/rover/utils/fonts/__init__.py +0 -0
- package/service/rover/utils/fonts/fonts.py +33 -0
- package/service/rover/utils/image/__init__.py +0 -0
- package/service/rover/utils/image/convert.py +51 -0
- package/service/rover/utils/image/image_tools.py +30 -0
- package/service/rover/utils/image/utils.py +70 -0
- package/service/rover/utils/plugins_config/__init__.py +0 -0
- package/service/rover/utils/plugins_config/gs_config.py +506 -0
- package/service/rover/utils/plugins_config/models.py +182 -0
- package/service/rover/web_app.py +3 -0
- package/service/rover/webconsole/__init__.py +0 -0
- package/service/rover/webconsole/mount_app.py +26 -0
- package/service/tools/migrate.py +351 -0
- package/service/tools/regression.py +87 -0
- package/service/tools/smoke.py +63 -0
- package/service/tools/update_plugins.py +212 -0
- package/service/tools/verify_actions.py +104 -0
- package/service/tools/verify_card.py +82 -0
- package/service/tools/wwdata.py +665 -0
- package/tests/compat.test.js +181 -0
- package/tests/imagebridge.test.js +58 -0
- package/tests/media.test.js +68 -0
- package/tests/native.test.js +127 -0
- package/tests/runtime.test.js +69 -0
- package/tests/service.test.js +48 -0
|
@@ -0,0 +1,714 @@
|
|
|
1
|
+
"""练度总排行 / 无尽总排行 / 矩阵总排行(含单队) 的 HTML 卡片。
|
|
2
|
+
|
|
3
|
+
取数沿用原绘图函数, 版式按 PIL 版逐坐标搬运; 构图出错时回退原图。
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import asyncio
|
|
7
|
+
import base64
|
|
8
|
+
import importlib
|
|
9
|
+
import time
|
|
10
|
+
from io import BytesIO
|
|
11
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
12
|
+
|
|
13
|
+
from rover.cards import _shared
|
|
14
|
+
from rover.logger import logger
|
|
15
|
+
|
|
16
|
+
_HEADER_H = 510
|
|
17
|
+
_TITLE_H = 500
|
|
18
|
+
_ROW_H = 110
|
|
19
|
+
|
|
20
|
+
_scratch: Dict[str, Any] = {}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _plugin(module, path: str):
|
|
24
|
+
"""按插件根包取模块, 不写死插件的导入路径。"""
|
|
25
|
+
return importlib.import_module(f"{module.__name__.rsplit('.', 2)[0]}.{path}")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _sibling(module, name: str):
|
|
29
|
+
return importlib.import_module(f"{module.__name__.rsplit('.', 1)[0]}.{name}")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _font_css_url(module) -> str:
|
|
33
|
+
try:
|
|
34
|
+
return _shared.font_css_url(_plugin(module, "utils.render_utils"))
|
|
35
|
+
except Exception:
|
|
36
|
+
return ""
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _css(color: Any, alpha: Optional[float] = None) -> str:
|
|
40
|
+
"""插件里的颜色有 (r,g,b) 元组和 '#RRGGBB'/名字两种写法, 统一成 CSS。"""
|
|
41
|
+
if color is None:
|
|
42
|
+
return "inherit"
|
|
43
|
+
if isinstance(color, str):
|
|
44
|
+
return color
|
|
45
|
+
r, g, b = color[0], color[1], color[2]
|
|
46
|
+
if alpha is None and len(color) > 3:
|
|
47
|
+
alpha = color[3] / 255
|
|
48
|
+
if alpha is None:
|
|
49
|
+
return f"rgb({r},{g},{b})"
|
|
50
|
+
return f"rgba({r},{g},{b},{alpha:.3f})"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _data_uri(image, quality: int = 0) -> str:
|
|
54
|
+
buffer = BytesIO()
|
|
55
|
+
if quality:
|
|
56
|
+
image.save(buffer, format="WEBP", quality=quality)
|
|
57
|
+
mime = "webp"
|
|
58
|
+
else:
|
|
59
|
+
image.save(buffer, format="PNG")
|
|
60
|
+
mime = "png"
|
|
61
|
+
return f"data:image/{mime};base64," + base64.b64encode(buffer.getvalue()).decode()
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class _Assets:
|
|
65
|
+
"""重复出现的图(角色头像/合鸣/徽章底)只内联一次, 行内按 class 引用。"""
|
|
66
|
+
|
|
67
|
+
def __init__(self) -> None:
|
|
68
|
+
self.items: List[Dict[str, str]] = []
|
|
69
|
+
self._index: Dict[Any, str] = {}
|
|
70
|
+
|
|
71
|
+
def add(self, key: Any, image, quality: int = 0) -> str:
|
|
72
|
+
if image is None:
|
|
73
|
+
return ""
|
|
74
|
+
name = self._index.get(key)
|
|
75
|
+
if name is None:
|
|
76
|
+
name = f"g{len(self.items)}"
|
|
77
|
+
self.items.append({"cls": name, "uri": _data_uri(image, quality)})
|
|
78
|
+
self._index[key] = name
|
|
79
|
+
return name
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _draw_ctx(module):
|
|
83
|
+
"""fit_text 要一个画布量文字宽度, 全局借一张 1x1。"""
|
|
84
|
+
ctx = _scratch.get("draw")
|
|
85
|
+
if ctx is None:
|
|
86
|
+
ctx = module.ImageDraw.Draw(module.Image.new("RGBA", (1, 1)))
|
|
87
|
+
_scratch["draw"] = ctx
|
|
88
|
+
return ctx
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _fit_name(module, text: str, max_width: int) -> Tuple[int, str]:
|
|
92
|
+
fonts = tuple(getattr(module, f"waves_font_{s}") for s in (20, 18, 16, 14, 12))
|
|
93
|
+
font, fitted = module.fit_text(_draw_ctx(module), str(text), max_width, fonts)
|
|
94
|
+
return int(font.size), fitted
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _mask_gradient(mask, width: int, height: int) -> str:
|
|
98
|
+
"""标题遮罩: 照 PIL 的等比放大 + 底部裁切采样透明度, 转成竖向渐变。"""
|
|
99
|
+
resized = mask.convert("RGBA").resize((width, mask.height * width // mask.width))
|
|
100
|
+
band = resized.crop((0, resized.height - height, width, resized.height)).getchannel("A")
|
|
101
|
+
x = width // 2
|
|
102
|
+
solid = 0
|
|
103
|
+
for y in range(height):
|
|
104
|
+
if band.getpixel((x, y)) < 255:
|
|
105
|
+
break
|
|
106
|
+
solid = y
|
|
107
|
+
stops = [f"rgb(0,0,0) {solid}px"]
|
|
108
|
+
for y in range(solid, height, 8):
|
|
109
|
+
stops.append(f"rgba(0,0,0,{band.getpixel((x, y)) / 255:.3f}) {y}px")
|
|
110
|
+
stops.append(f"rgba(0,0,0,0) {height}px")
|
|
111
|
+
return "linear-gradient(to bottom, " + ", ".join(stops) + ")"
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _title_image(module, name: str, width: int):
|
|
115
|
+
"""标题底图: 大图直接裁, 小图(矩阵)按宽度等比放大后再裁/垫。"""
|
|
116
|
+
image = module.Image.open(module.TEXT_PATH / name).convert("RGBA")
|
|
117
|
+
if name.endswith(".png"):
|
|
118
|
+
image = image.resize((width, int(image.height * width / image.width)))
|
|
119
|
+
if image.height <= _TITLE_H:
|
|
120
|
+
padded = module.Image.new("RGBA", (width, _TITLE_H), (0, 0, 0, 0))
|
|
121
|
+
padded.paste(image, (0, _TITLE_H - image.height))
|
|
122
|
+
return padded
|
|
123
|
+
return image.crop((0, 0, width, _TITLE_H))
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _header(module, title_name: str, title_file: str, width: int, subs: List[str]) -> dict:
|
|
127
|
+
icon = module.get_ICON().resize((128, 128))
|
|
128
|
+
return {
|
|
129
|
+
"title": title_name,
|
|
130
|
+
"art": _data_uri(_title_image(module, title_file, width).convert("RGB"), 82),
|
|
131
|
+
"icon": _data_uri(icon),
|
|
132
|
+
"mask": _mask_gradient(
|
|
133
|
+
module.Image.open(module.TEXT_PATH / "char_mask.png"), width, _TITLE_H
|
|
134
|
+
),
|
|
135
|
+
"subs": [s for s in subs if s],
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _rank_slot(module, assets: _Assets, rank_id: int) -> dict:
|
|
140
|
+
"""名次: 前三名贴牌, 其余色块底板, 位数越多底板越宽(照原版三档)。"""
|
|
141
|
+
badge = _sibling(module, "rank_badge")
|
|
142
|
+
slot: Dict[str, Any] = {"medal": assets.add(("medal", rank_id), badge._load_badge(rank_id))}
|
|
143
|
+
if slot["medal"]:
|
|
144
|
+
return slot
|
|
145
|
+
if rank_id > 1000:
|
|
146
|
+
slot["box"] = {"left": 10, "w": 100, "text": "999+"}
|
|
147
|
+
elif rank_id > 999:
|
|
148
|
+
slot["box"] = {"left": 10, "w": 100, "text": str(rank_id)}
|
|
149
|
+
elif rank_id > 99:
|
|
150
|
+
slot["box"] = {"left": 25, "w": 75, "text": str(rank_id)}
|
|
151
|
+
else:
|
|
152
|
+
slot["box"] = {"left": 40, "w": 50, "text": str(rank_id)}
|
|
153
|
+
return slot
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _bot_badge(module, assets: _Assets, background: str, name: str, x: int, y: int) -> Optional[dict]:
|
|
157
|
+
"""bot 主人徽章: 底图按 388x72 → 208x39 等比缩放后居中, 名字压在中间。"""
|
|
158
|
+
if not name:
|
|
159
|
+
return None
|
|
160
|
+
try:
|
|
161
|
+
image = _plugin(module, "utils.image").get_bot_bg(background or "")
|
|
162
|
+
except Exception as e:
|
|
163
|
+
logger.debug(f"[鸣潮·榜单卡] 徽章底图取用失败: {e}")
|
|
164
|
+
image = None
|
|
165
|
+
badge: Dict[str, Any] = {"name": name, "cls": "", "w": 0, "h": 0, "x": x, "y": y}
|
|
166
|
+
if image is not None:
|
|
167
|
+
badge["cls"] = assets.add(("botbg", background or "", id(image)), image)
|
|
168
|
+
badge["w"] = round(image.width * 208 / 388)
|
|
169
|
+
badge["h"] = round(image.height * 39 / 72)
|
|
170
|
+
return badge
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _char_command(module, char_id: Any) -> str:
|
|
174
|
+
"""自己那一行的角色头像点开面板。"""
|
|
175
|
+
try:
|
|
176
|
+
name = _plugin(module, "utils.name_convert").char_id_to_char_name(str(char_id))
|
|
177
|
+
except Exception:
|
|
178
|
+
return ""
|
|
179
|
+
if not name:
|
|
180
|
+
return ""
|
|
181
|
+
try:
|
|
182
|
+
prefix = _plugin(module, "wutheringwaves_config").PREFIX
|
|
183
|
+
except Exception:
|
|
184
|
+
prefix = "ww"
|
|
185
|
+
return f"{prefix}{name}面板"
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _base_context(module, width: int, bar, bg) -> dict:
|
|
189
|
+
return {
|
|
190
|
+
"card_w": width,
|
|
191
|
+
"bar": _data_uri(bar),
|
|
192
|
+
"bg": _data_uri(bg.convert("RGB"), 72),
|
|
193
|
+
"font_css_url": _font_css_url(module),
|
|
194
|
+
"grey": _css(module.GREY),
|
|
195
|
+
"gold": _css(getattr(module, "SPECIAL_GOLD", (234, 183, 4))),
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _now() -> str:
|
|
200
|
+
return time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
# ---------------------------------------------------------------- 练度总排行
|
|
204
|
+
|
|
205
|
+
_LEVEL_CHAR_SIZE = 40
|
|
206
|
+
_LEVEL_CHAR_SPACING = 45
|
|
207
|
+
_LEVEL_CHAR_X = 570
|
|
208
|
+
_LEVEL_CHAR_Y = 35
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _level_char_mask(module) -> str:
|
|
212
|
+
"""角色头像用的小遮罩: 底部一段渐隐, 按百分比写以适配任意尺寸。"""
|
|
213
|
+
mask = module.Image.open(module.TEXT_PATH / "char_mask.png").convert("RGBA")
|
|
214
|
+
band = mask.resize((_LEVEL_CHAR_SIZE, _LEVEL_CHAR_SIZE)).getchannel("A")
|
|
215
|
+
stops = []
|
|
216
|
+
for y in range(_LEVEL_CHAR_SIZE):
|
|
217
|
+
stops.append(f"rgba(0,0,0,{band.getpixel((_LEVEL_CHAR_SIZE // 2, y)) / 255:.3f}) {y * 100 / _LEVEL_CHAR_SIZE:.1f}%")
|
|
218
|
+
return "linear-gradient(to bottom, " + ", ".join(stops) + ")"
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
async def _build_level_rank(module, bot, ev, pages):
|
|
222
|
+
self_uid = await module.WavesBind.get_uid_by_game(ev.user_id, ev.bot_id) or ""
|
|
223
|
+
item = module.TotalRankRequest(
|
|
224
|
+
page=pages,
|
|
225
|
+
page_num=module.RANK_PAGE_SIZE,
|
|
226
|
+
waves_id=self_uid,
|
|
227
|
+
version=module.get_version(dynamic=True, waves_id=self_uid, pages=pages),
|
|
228
|
+
)
|
|
229
|
+
rank_info = await module.get_rank(item)
|
|
230
|
+
if not rank_info:
|
|
231
|
+
return "获取练度总排行失败"
|
|
232
|
+
if rank_info.message and not rank_info.data:
|
|
233
|
+
return rank_info.message
|
|
234
|
+
if not rank_info.data or not rank_info.data.score_details:
|
|
235
|
+
return "暂无排行数据"
|
|
236
|
+
|
|
237
|
+
details = rank_info.data.score_details
|
|
238
|
+
avatars = await asyncio.gather(
|
|
239
|
+
*[module.get_avatar(d.user_id, getattr(d, "sender_avatar", "")) for d in details]
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
top_chars = {}
|
|
243
|
+
for detail in details:
|
|
244
|
+
chars = sorted(
|
|
245
|
+
detail.char_score_details or [], key=lambda c: c.phantom_score, reverse=True
|
|
246
|
+
)[:10]
|
|
247
|
+
top_chars[detail.waves_id] = chars
|
|
248
|
+
char_ids = {module.randomize_special_char_id(c.char_id) for cs in top_chars.values() for c in cs}
|
|
249
|
+
char_avatars = dict(
|
|
250
|
+
zip(char_ids, await asyncio.gather(*[module.get_square_avatar(i) for i in char_ids]))
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
assets = _Assets()
|
|
254
|
+
width = 1300
|
|
255
|
+
rows: List[dict] = []
|
|
256
|
+
for detail, avatar in zip(details, avatars):
|
|
257
|
+
is_self = bool(self_uid) and detail.waves_id == self_uid
|
|
258
|
+
name_size, name_text = _fit_name(module, detail.kuro_name, 130)
|
|
259
|
+
best = top_chars.get(detail.waves_id) or []
|
|
260
|
+
chars: List[dict] = []
|
|
261
|
+
for index, char in enumerate(best):
|
|
262
|
+
display_id = module.randomize_special_char_id(char.char_id)
|
|
263
|
+
icon = char_avatars.get(display_id)
|
|
264
|
+
if icon is None:
|
|
265
|
+
continue
|
|
266
|
+
chars.append(
|
|
267
|
+
{
|
|
268
|
+
"x": _LEVEL_CHAR_X + index * _LEVEL_CHAR_SPACING,
|
|
269
|
+
"cls": assets.add(("char", display_id), icon.resize((40, 40)), 88),
|
|
270
|
+
"score": f"{int(char.phantom_score)}",
|
|
271
|
+
"cmd": _char_command(module, display_id) if is_self else "",
|
|
272
|
+
}
|
|
273
|
+
)
|
|
274
|
+
row = {
|
|
275
|
+
"avatar": _data_uri(avatar, 80),
|
|
276
|
+
"name": name_text,
|
|
277
|
+
"name_size": name_size,
|
|
278
|
+
"count": len(detail.char_score_details or []),
|
|
279
|
+
"uid": f"特征码: {module.hide_uid(detail.waves_id, user_pref='on' if detail.hide_uid else '')}",
|
|
280
|
+
"uid_x": 350,
|
|
281
|
+
"uid_color": _css(module.RED) if is_self else "white",
|
|
282
|
+
"badge": _bot_badge(
|
|
283
|
+
module, assets, getattr(detail, "background", ""), getattr(detail, "alias_name", ""), 346, 61
|
|
284
|
+
),
|
|
285
|
+
"chars": chars,
|
|
286
|
+
"best": f"{int(best[0].phantom_score)}" if best else "",
|
|
287
|
+
"total": f"{detail.total_score:.1f}",
|
|
288
|
+
}
|
|
289
|
+
row.update(_rank_slot(module, assets, detail.rank))
|
|
290
|
+
rows.append(row)
|
|
291
|
+
|
|
292
|
+
context = _base_context(
|
|
293
|
+
module, width, module.Image.open(module.TEXT_PATH / "bar1.png"),
|
|
294
|
+
module.get_custom_waves_bg(bg="bg9", crop=False),
|
|
295
|
+
)
|
|
296
|
+
context.update(
|
|
297
|
+
header=_header(module, "#练度总排行", "totalrank.jpg", width, [_now()]),
|
|
298
|
+
cond={
|
|
299
|
+
"line1": "1. 综合所有角色的声骸分数。具备声骸套装的角色,全量刷新面板后更新排行位置。",
|
|
300
|
+
"line2": "2. 显示前10个最强角色,刷新单角色影响此处显示但不计入总分。",
|
|
301
|
+
"notes": "排行标准:以所有角色评分(分数>=175)总和为排序的综合排名",
|
|
302
|
+
},
|
|
303
|
+
char_mask=_level_char_mask(module),
|
|
304
|
+
red=_css(module.RED),
|
|
305
|
+
rows=rows,
|
|
306
|
+
assets=assets.items,
|
|
307
|
+
)
|
|
308
|
+
return context
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
# ---------------------------------------------------------------- 无尽总排行
|
|
312
|
+
|
|
313
|
+
_SLASH_CHAR_SIZE = 45
|
|
314
|
+
_SLASH_HALF_X = 570
|
|
315
|
+
_SLASH_HALF_SPACING = 250
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
async def _build_slash_rank(module, bot, ev, page):
|
|
319
|
+
waves_id = await module.WavesBind.get_uid_by_game(ev.user_id, ev.bot_id) or ""
|
|
320
|
+
item = module.SlashRankItem(
|
|
321
|
+
page=page,
|
|
322
|
+
page_num=module.RANK_PAGE_SIZE,
|
|
323
|
+
waves_id=waves_id,
|
|
324
|
+
version=module.get_version(dynamic=True, waves_id=waves_id, pages=page),
|
|
325
|
+
)
|
|
326
|
+
rank_info = await module.get_rank(item)
|
|
327
|
+
if not rank_info:
|
|
328
|
+
return "获取排行失败"
|
|
329
|
+
if rank_info.message and not rank_info.data:
|
|
330
|
+
return rank_info.message
|
|
331
|
+
if not rank_info.data or not rank_info.data.rank_list:
|
|
332
|
+
return "暂无排行数据"
|
|
333
|
+
|
|
334
|
+
rank_list = rank_info.data.rank_list
|
|
335
|
+
avatars = await asyncio.gather(
|
|
336
|
+
*[module.get_avatar(r.user_id, getattr(r, "sender_avatar", "")) for r in rank_list]
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
char_ids = {
|
|
340
|
+
module.randomize_special_char_id(c.char_id)
|
|
341
|
+
for r in rank_list
|
|
342
|
+
for half in r.half_list
|
|
343
|
+
for c in half.char_detail
|
|
344
|
+
}
|
|
345
|
+
char_avatars = dict(
|
|
346
|
+
zip(char_ids, await asyncio.gather(*[module.get_square_avatar(i) for i in char_ids]))
|
|
347
|
+
)
|
|
348
|
+
buff_urls = {half.buff_icon for r in rank_list for half in r.half_list if half.buff_icon}
|
|
349
|
+
buff_icons = dict(
|
|
350
|
+
zip(
|
|
351
|
+
buff_urls,
|
|
352
|
+
await asyncio.gather(
|
|
353
|
+
*[module.pic_download_from_url(module.SLASH_PATH, u) for u in buff_urls],
|
|
354
|
+
return_exceptions=True,
|
|
355
|
+
),
|
|
356
|
+
)
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
assets = _Assets()
|
|
360
|
+
width = 1300
|
|
361
|
+
rows: List[dict] = []
|
|
362
|
+
for rank, avatar in zip(rank_list, avatars):
|
|
363
|
+
is_self = bool(waves_id) and rank.waves_id == waves_id
|
|
364
|
+
name_size, name_text = _fit_name(module, rank.kuro_name, 130)
|
|
365
|
+
gold_total = sum(
|
|
366
|
+
c.chain + 1
|
|
367
|
+
for half in rank.half_list
|
|
368
|
+
for c in half.char_detail
|
|
369
|
+
if c.chain >= 0 and module.is_limited_5star(c.char_id)
|
|
370
|
+
)
|
|
371
|
+
halves: List[dict] = []
|
|
372
|
+
for half_index, half in enumerate(rank.half_list):
|
|
373
|
+
offset = 10 if half_index == 0 else 0
|
|
374
|
+
base_x = _SLASH_HALF_X + half_index * _SLASH_HALF_SPACING + offset
|
|
375
|
+
chars: List[dict] = []
|
|
376
|
+
for role_index, char in enumerate(half.char_detail):
|
|
377
|
+
display_id = module.randomize_special_char_id(char.char_id)
|
|
378
|
+
if module.get_char_model(display_id) is None:
|
|
379
|
+
continue
|
|
380
|
+
icon = char_avatars.get(display_id)
|
|
381
|
+
if icon is None:
|
|
382
|
+
continue
|
|
383
|
+
chars.append(
|
|
384
|
+
{
|
|
385
|
+
"x": base_x + role_index * 50,
|
|
386
|
+
"cls": assets.add(("char", display_id), icon.resize((45, 45)), 88),
|
|
387
|
+
"chain": char.chain,
|
|
388
|
+
"chain_color": _css(module.CHAIN_COLOR[char.chain], 0.9) if char.chain != -1 else "",
|
|
389
|
+
"cmd": _char_command(module, display_id) if is_self else "",
|
|
390
|
+
}
|
|
391
|
+
)
|
|
392
|
+
buff = buff_icons.get(half.buff_icon)
|
|
393
|
+
halves.append(
|
|
394
|
+
{
|
|
395
|
+
"chars": chars,
|
|
396
|
+
"buff": assets.add(("buff", half.buff_icon), buff if not isinstance(buff, BaseException) else None, 88),
|
|
397
|
+
"buff_x": 720 + half_index * _SLASH_HALF_SPACING + offset,
|
|
398
|
+
"buff_color": _css(module.COLOR_QUALITY[half.buff_quality]),
|
|
399
|
+
"score": f"{half.score}",
|
|
400
|
+
"score_x": 670 + half_index * _SLASH_HALF_SPACING + offset,
|
|
401
|
+
"score_color": _css(module.get_score_color(half.score)),
|
|
402
|
+
}
|
|
403
|
+
)
|
|
404
|
+
row = {
|
|
405
|
+
"avatar": _data_uri(avatar, 80),
|
|
406
|
+
"name": name_text,
|
|
407
|
+
"name_size": name_size,
|
|
408
|
+
"gold": f"角色限定{gold_total}金",
|
|
409
|
+
"uid": f"特征码: {module.hide_uid(rank.waves_id, user_pref='on' if rank.hide_uid else '')}",
|
|
410
|
+
"uid_x": 350,
|
|
411
|
+
"uid_color": _css(module.RED) if is_self else "white",
|
|
412
|
+
"badge": _bot_badge(
|
|
413
|
+
module, assets, getattr(rank, "background", ""), rank.alias_name or "", 346, 61
|
|
414
|
+
),
|
|
415
|
+
"halves": halves,
|
|
416
|
+
"score": f"{rank.score}",
|
|
417
|
+
"score_color": _css(module.get_score_color(rank.score)),
|
|
418
|
+
}
|
|
419
|
+
row.update(_rank_slot(module, assets, rank.rank))
|
|
420
|
+
rows.append(row)
|
|
421
|
+
|
|
422
|
+
period = ""
|
|
423
|
+
if rank_info.data.start_date:
|
|
424
|
+
rank_dt = module.parse_rank_date(rank_info.data.start_date)
|
|
425
|
+
if rank_dt:
|
|
426
|
+
period = f"第{module.get_slash_period_number(rank_dt)}期"
|
|
427
|
+
|
|
428
|
+
context = _base_context(
|
|
429
|
+
module, width, module.Image.open(module.TEXT_PATH / "bar1.png"),
|
|
430
|
+
module.get_waves_bg(bg="bg9", crop=False),
|
|
431
|
+
)
|
|
432
|
+
context.update(
|
|
433
|
+
header=_header(module, "#无尽总排行", "slash.jpg", width, [period, _now()]),
|
|
434
|
+
rows=rows,
|
|
435
|
+
assets=assets.items,
|
|
436
|
+
)
|
|
437
|
+
return context
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
# ---------------------------------------------------------------- 矩阵总排行
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
def _crystal_gradient(module) -> str:
|
|
444
|
+
colors = _sibling(module, "_colors").CRYSTAL_COLORS
|
|
445
|
+
return "linear-gradient(90deg, " + ", ".join(_css(c) for c in colors) + ")"
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
def _matrix_score_style(module, color) -> str:
|
|
449
|
+
return "" if color == module.CRYSTAL_SENTINEL else _css(color)
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
async def _build_matrix_rank(module, bot, ev, single_team, page, char_ids):
|
|
453
|
+
waves_id = await module.WavesBind.get_uid_by_game(ev.user_id, ev.bot_id) or ""
|
|
454
|
+
item = module.MatrixRankItem(
|
|
455
|
+
page=page,
|
|
456
|
+
page_num=module.RANK_PAGE_SIZE,
|
|
457
|
+
waves_id=waves_id,
|
|
458
|
+
version=module.get_version(dynamic=True, waves_id=waves_id, pages=page),
|
|
459
|
+
single_team=single_team,
|
|
460
|
+
char_ids=char_ids or [],
|
|
461
|
+
)
|
|
462
|
+
rank_info = await module.get_rank(item)
|
|
463
|
+
if not rank_info:
|
|
464
|
+
return "获取矩阵排行失败"
|
|
465
|
+
if rank_info.message and not rank_info.data:
|
|
466
|
+
return rank_info.message
|
|
467
|
+
if not rank_info.data or not rank_info.data.rank_list:
|
|
468
|
+
return "暂无该队伍的排行数据" if char_ids else "暂无排行数据"
|
|
469
|
+
|
|
470
|
+
rank_list = rank_info.data.rank_list
|
|
471
|
+
width = module.MATRIX_SINGLE_TOTAL_WIDTH if single_team else module.MATRIX_TOTAL_WIDTH
|
|
472
|
+
board_7digit = any(r.score >= 1_000_000 for r in rank_list)
|
|
473
|
+
board_6digit = any(r.score >= 100_000 for r in rank_list)
|
|
474
|
+
team_limit = 1 if single_team else 2
|
|
475
|
+
char_size = 55 if single_team else 45
|
|
476
|
+
char_spacing = 60 if single_team else 50
|
|
477
|
+
char_y = 28 if single_team else 20
|
|
478
|
+
buff_size = 60 if single_team else 50
|
|
479
|
+
buff_y = 25 if single_team else 15
|
|
480
|
+
chain_size = 20 if single_team else 15
|
|
481
|
+
team_base_x = (580 if board_6digit else 600) if single_team else 575
|
|
482
|
+
team_spacing = 232 if (not single_team and board_7digit) else 250
|
|
483
|
+
|
|
484
|
+
avatars = await asyncio.gather(
|
|
485
|
+
*[module.get_avatar(r.user_id, getattr(r, "sender_avatar", "")) for r in rank_list]
|
|
486
|
+
)
|
|
487
|
+
shown_teams = [
|
|
488
|
+
sorted(r.teams, key=lambda t: t.score, reverse=True)[:team_limit] for r in rank_list
|
|
489
|
+
]
|
|
490
|
+
ids = set()
|
|
491
|
+
icon_urls = set()
|
|
492
|
+
for teams in shown_teams:
|
|
493
|
+
for team in teams:
|
|
494
|
+
ids.update(module.randomize_special_char_id(c.char_id) for c in team.char_detail)
|
|
495
|
+
if team.buff_icon:
|
|
496
|
+
icon_urls.add(team.buff_icon)
|
|
497
|
+
if not team.char_detail:
|
|
498
|
+
icon_urls.update(team.role_icons)
|
|
499
|
+
char_avatars = dict(
|
|
500
|
+
zip(ids, await asyncio.gather(*[module.get_square_avatar(i) for i in ids]))
|
|
501
|
+
)
|
|
502
|
+
downloads = dict(
|
|
503
|
+
zip(
|
|
504
|
+
icon_urls,
|
|
505
|
+
await asyncio.gather(
|
|
506
|
+
*[module.pic_download_from_url(module.MATRIX_PATH, u) for u in icon_urls],
|
|
507
|
+
return_exceptions=True,
|
|
508
|
+
),
|
|
509
|
+
)
|
|
510
|
+
)
|
|
511
|
+
|
|
512
|
+
assets = _Assets()
|
|
513
|
+
rows: List[dict] = []
|
|
514
|
+
for rank, avatar, shown in zip(rank_list, avatars, shown_teams):
|
|
515
|
+
is_self = bool(waves_id) and rank.waves_id == waves_id
|
|
516
|
+
name_size, name_text = _fit_name(
|
|
517
|
+
module,
|
|
518
|
+
rank.kuro_name,
|
|
519
|
+
module.MATRIX_SINGLE_NAME_MAX_WIDTH if single_team else module.MATRIX_TOTAL_NAME_MAX_WIDTH,
|
|
520
|
+
)
|
|
521
|
+
uid_text = module.hide_uid(rank.waves_id, user_pref="on" if rank.hide_uid else "")
|
|
522
|
+
teams: List[dict] = []
|
|
523
|
+
for team_index, team in enumerate(shown):
|
|
524
|
+
base_x = team_base_x + team_index * team_spacing
|
|
525
|
+
chars: List[dict] = []
|
|
526
|
+
for role_index, char in enumerate(team.char_detail):
|
|
527
|
+
display_id = module.randomize_special_char_id(char.char_id)
|
|
528
|
+
if module.get_char_model(display_id) is None:
|
|
529
|
+
continue
|
|
530
|
+
icon = char_avatars.get(display_id)
|
|
531
|
+
if icon is None:
|
|
532
|
+
continue
|
|
533
|
+
chars.append(
|
|
534
|
+
{
|
|
535
|
+
"x": base_x + role_index * char_spacing,
|
|
536
|
+
"cls": assets.add(("char", display_id, char_size), icon.resize((char_size, char_size)), 88),
|
|
537
|
+
"chain": char.chain,
|
|
538
|
+
"chain_color": _css(module.CHAIN_COLOR[char.chain], 0.9) if char.chain != -1 else "",
|
|
539
|
+
"round": False,
|
|
540
|
+
"cmd": _char_command(module, display_id) if is_self else "",
|
|
541
|
+
}
|
|
542
|
+
)
|
|
543
|
+
if not team.char_detail and team.role_icons:
|
|
544
|
+
for role_index, url in enumerate(team.role_icons):
|
|
545
|
+
icon = downloads.get(url)
|
|
546
|
+
if icon is None or isinstance(icon, BaseException):
|
|
547
|
+
continue
|
|
548
|
+
chars.append(
|
|
549
|
+
{
|
|
550
|
+
"x": base_x + role_index * char_spacing,
|
|
551
|
+
"cls": assets.add(("url", url, char_size), icon.resize((char_size, char_size)), 88),
|
|
552
|
+
"chain": -1,
|
|
553
|
+
"chain_color": "",
|
|
554
|
+
"round": True,
|
|
555
|
+
"cmd": "",
|
|
556
|
+
}
|
|
557
|
+
)
|
|
558
|
+
filled = len(team.char_detail) or len(team.role_icons)
|
|
559
|
+
buff = downloads.get(team.buff_icon)
|
|
560
|
+
score_color = module.get_team_score_color(team.score)
|
|
561
|
+
teams.append(
|
|
562
|
+
{
|
|
563
|
+
"chars": chars,
|
|
564
|
+
"holes": [base_x + i * char_spacing for i in range(filled, 3)],
|
|
565
|
+
"buff": assets.add(
|
|
566
|
+
("buff", team.buff_icon, buff_size),
|
|
567
|
+
buff.resize((buff_size, buff_size)) if buff is not None and not isinstance(buff, BaseException) else None,
|
|
568
|
+
88,
|
|
569
|
+
),
|
|
570
|
+
"buff_x": base_x + char_spacing * 3 + 10,
|
|
571
|
+
"score": f"{'最高单队得分' if team_index == 0 else '次高单队得分'}: {team.score}",
|
|
572
|
+
"score_x": base_x + 105,
|
|
573
|
+
"score_color": _matrix_score_style(module, score_color),
|
|
574
|
+
}
|
|
575
|
+
)
|
|
576
|
+
score_color = (
|
|
577
|
+
module.get_team_score_color(rank.score) if single_team else module.get_score_color(rank.score)
|
|
578
|
+
)
|
|
579
|
+
team_count = rank.team_count if rank.team_count else len(rank.teams)
|
|
580
|
+
row = {
|
|
581
|
+
"avatar": _data_uri(avatar, 80),
|
|
582
|
+
"name": name_text,
|
|
583
|
+
"name_size": name_size,
|
|
584
|
+
"uid": f"特征码: {uid_text}" if single_team else uid_text,
|
|
585
|
+
"uid_x": 350 if single_team else 210,
|
|
586
|
+
"uid_color": _css(module.RED) if is_self else "white",
|
|
587
|
+
"gold": module._get_matrix_team_char_gold_count(rank.teams) if single_team else 0,
|
|
588
|
+
"team_count": f"上场队伍数量: {team_count}" if team_count else "",
|
|
589
|
+
"badge": _bot_badge(
|
|
590
|
+
module,
|
|
591
|
+
assets,
|
|
592
|
+
getattr(rank, "background", ""),
|
|
593
|
+
rank.alias_name or "",
|
|
594
|
+
346 if single_team else 350,
|
|
595
|
+
60,
|
|
596
|
+
),
|
|
597
|
+
"teams": teams,
|
|
598
|
+
"score": f"{rank.score}",
|
|
599
|
+
"score_x": 950 if single_team else 1140,
|
|
600
|
+
"score_color": _matrix_score_style(module, score_color),
|
|
601
|
+
}
|
|
602
|
+
row.update(_rank_slot(module, assets, rank.rank))
|
|
603
|
+
rows.append(row)
|
|
604
|
+
|
|
605
|
+
period = ""
|
|
606
|
+
if rank_info.data.start_date:
|
|
607
|
+
rank_dt = module.parse_rank_date(rank_info.data.start_date)
|
|
608
|
+
if rank_dt:
|
|
609
|
+
period = f"第{module.get_matrix_period_number(rank_dt)}期"
|
|
610
|
+
subs = [period, _now()]
|
|
611
|
+
if period:
|
|
612
|
+
subs.append(module._matrix_team_label(char_ids))
|
|
613
|
+
|
|
614
|
+
context = _base_context(
|
|
615
|
+
module, width, module._get_matrix_rank_bar(width), module.get_waves_bg(bg="bg9", crop=False)
|
|
616
|
+
)
|
|
617
|
+
context.update(
|
|
618
|
+
header=_header(
|
|
619
|
+
module,
|
|
620
|
+
"#矩阵单队总排行" if single_team else "#矩阵总排行",
|
|
621
|
+
"matrix.png",
|
|
622
|
+
width,
|
|
623
|
+
subs,
|
|
624
|
+
),
|
|
625
|
+
single_team=single_team,
|
|
626
|
+
char_size=char_size,
|
|
627
|
+
char_y=char_y,
|
|
628
|
+
chain_size=chain_size,
|
|
629
|
+
buff_size=buff_size,
|
|
630
|
+
buff_y=buff_y,
|
|
631
|
+
hole_first_y=19 if single_team else 16,
|
|
632
|
+
hole_second_y=35 if single_team else 32,
|
|
633
|
+
crystal=_crystal_gradient(module),
|
|
634
|
+
red=_css(module.RED),
|
|
635
|
+
rows=rows,
|
|
636
|
+
assets=assets.items,
|
|
637
|
+
)
|
|
638
|
+
return context
|
|
639
|
+
|
|
640
|
+
|
|
641
|
+
# ---------------------------------------------------------------- 补丁挂载
|
|
642
|
+
|
|
643
|
+
|
|
644
|
+
def _render(template_name: str, context: dict):
|
|
645
|
+
card = _shared.render(template_name, context)
|
|
646
|
+
logger.info(f"[鸣潮·榜单卡] {template_name} {len(card) / 1024 / 1024:.2f}MB")
|
|
647
|
+
return card
|
|
648
|
+
|
|
649
|
+
|
|
650
|
+
def _bind(module, name: str, func) -> None:
|
|
651
|
+
"""功能模块 import 时已按值绑定, 包内同名引用一并换掉。"""
|
|
652
|
+
import sys
|
|
653
|
+
|
|
654
|
+
setattr(module, name, func)
|
|
655
|
+
package = sys.modules.get(module.__name__.rsplit(".", 1)[0])
|
|
656
|
+
if package is not None and hasattr(package, name):
|
|
657
|
+
setattr(package, name, func)
|
|
658
|
+
|
|
659
|
+
|
|
660
|
+
def patch_level_rank(module) -> None:
|
|
661
|
+
original = module.draw_total_rank
|
|
662
|
+
|
|
663
|
+
async def draw_total_rank(bot, ev, pages):
|
|
664
|
+
try:
|
|
665
|
+
context = await _build_level_rank(module, bot, ev, pages)
|
|
666
|
+
if isinstance(context, str):
|
|
667
|
+
return context
|
|
668
|
+
return _render("rank_level_total.html", context)
|
|
669
|
+
except Exception as e:
|
|
670
|
+
logger.exception(f"[鸣潮·榜单卡] 练度总排行渲染失败, 回退原图: {e}")
|
|
671
|
+
return await original(bot, ev, pages)
|
|
672
|
+
|
|
673
|
+
_bind(module, "draw_total_rank", draw_total_rank)
|
|
674
|
+
|
|
675
|
+
|
|
676
|
+
def patch_slash_rank(module) -> None:
|
|
677
|
+
original = module.draw_all_slash_rank_card
|
|
678
|
+
|
|
679
|
+
async def draw_all_slash_rank_card(bot, ev, page: int = 1):
|
|
680
|
+
try:
|
|
681
|
+
context = await _build_slash_rank(module, bot, ev, page)
|
|
682
|
+
if isinstance(context, str):
|
|
683
|
+
return context
|
|
684
|
+
return _render("rank_slash_total.html", context)
|
|
685
|
+
except Exception as e:
|
|
686
|
+
logger.exception(f"[鸣潮·榜单卡] 无尽总排行渲染失败, 回退原图: {e}")
|
|
687
|
+
return await original(bot, ev, page)
|
|
688
|
+
|
|
689
|
+
_bind(module, "draw_all_slash_rank_card", draw_all_slash_rank_card)
|
|
690
|
+
|
|
691
|
+
|
|
692
|
+
def patch_matrix_rank(module) -> None:
|
|
693
|
+
original = module.draw_all_matrix_rank_card
|
|
694
|
+
|
|
695
|
+
async def draw_all_matrix_rank_card(bot, ev, single_team=False, page=1, char_ids=None):
|
|
696
|
+
try:
|
|
697
|
+
context = await _build_matrix_rank(module, bot, ev, single_team, page, char_ids)
|
|
698
|
+
if isinstance(context, str):
|
|
699
|
+
return context
|
|
700
|
+
return _render("rank_matrix_total.html", context)
|
|
701
|
+
except Exception as e:
|
|
702
|
+
logger.exception(f"[鸣潮·榜单卡] 矩阵总排行渲染失败, 回退原图: {e}")
|
|
703
|
+
return await original(bot, ev, single_team, page, char_ids)
|
|
704
|
+
|
|
705
|
+
_bind(module, "draw_all_matrix_rank_card", draw_all_matrix_rank_card)
|
|
706
|
+
|
|
707
|
+
|
|
708
|
+
PATCHES = {
|
|
709
|
+
"XutheringWavesUID": {
|
|
710
|
+
"wutheringwaves_rank.draw_total_rank_card": patch_level_rank,
|
|
711
|
+
"wutheringwaves_rank.slash_rank": patch_slash_rank,
|
|
712
|
+
"wutheringwaves_rank.matrix_rank": patch_matrix_rank,
|
|
713
|
+
}
|
|
714
|
+
}
|