Merge branch 'main' into d-v3

This commit is contained in:
辉鸭蛋
2025-08-24 15:56:31 +08:00
17 changed files with 1208 additions and 66 deletions

27
.cnb.yml Normal file
View File

@@ -0,0 +1,27 @@
main:
# 自定义按钮可触发的事件
web_trigger_one:
- docker:
image: python:3.11
imports:
- https://cnb.cool/bettergi/secret/-/blob/main/env.yml
stages:
- name: 下载构建物并上传
script: |
cd .github/workflows
pip install -r requirements.txt
python github_download_and_cnb_upload.py --cnb-token $CNB_TOKEN --github-token $GITHUB_TOKEN
echo done!
api_trigger_one:
- docker:
image: python:3.11
imports:
- https://cnb.cool/bettergi/secret/-/blob/main/env.yml
stages:
- name: API触发上传
script: |
echo run id: $RUN_ID
cd .github/workflows
pip install -r requirements.txt
python github_download_and_cnb_upload.py --cnb-token $CNB_TOKEN --github-token $GITHUB_TOKEN --run-id $RUN_ID
echo api done!

5
.cnb/web_trigger.yml Normal file
View File

@@ -0,0 +1,5 @@
branch:
- buttons:
- name: 触发上传
description: 上传测试版或者正式版
event: web_trigger_one

View File

@@ -1,3 +1,16 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
CNB Release Uploader
依赖:
- requests
- tqdm
安装依赖:
pip install requests tqdm
"""
import os
import json
import requests
@@ -6,6 +19,7 @@ import sys
import argparse
from typing import List, Dict, Optional
from pathlib import Path
from tqdm import tqdm
class CNBReleaseUploader:
@@ -103,13 +117,14 @@ class CNBReleaseUploader:
print(f" 响应内容: {e.response.text}")
return None
def upload_asset(self, upload_url: str, file_path: str) -> bool:
def upload_asset(self, upload_url: str, file_path: str, show_progress: bool = True) -> bool:
"""
上传asset文件
Args:
upload_url: 上传URL
file_path: 本地文件路径
show_progress: 是否显示上传进度条
Returns:
是否上传成功
@@ -123,9 +138,44 @@ class CNBReleaseUploader:
'Authorization': f'Bearer {self.token}',
}
file_size = os.path.getsize(file_path)
file_name = os.path.basename(file_path)
try:
with open(file_path, 'rb') as file:
response = requests.put(upload_url, headers=upload_headers, data=file)
if show_progress and file_size > 0:
# 创建进度条
progress_bar = tqdm(
total=file_size,
unit='B',
unit_scale=True,
unit_divisor=1024,
desc=f"📤 上传 {file_name}",
ncols=80,
bar_format='{desc}: {percentage:3.0f}%|{bar}| {n_fmt}/{total_fmt} [{elapsed}<{remaining}, {rate_fmt}]'
)
# 创建一个包装器来更新进度条
class ProgressFileWrapper:
def __init__(self, file_obj, progress_bar):
self.file_obj = file_obj
self.progress_bar = progress_bar
def read(self, size=-1):
data = self.file_obj.read(size)
if data:
self.progress_bar.update(len(data))
return data
def __getattr__(self, name):
return getattr(self.file_obj, name)
wrapped_file = ProgressFileWrapper(file, progress_bar)
response = requests.put(upload_url, headers=upload_headers, data=wrapped_file)
progress_bar.close()
else:
response = requests.put(upload_url, headers=upload_headers, data=file)
response.raise_for_status()
print(f"📤 上传到 {upload_url} 返回结果: {response.status_code}")
@@ -177,7 +227,7 @@ class CNBReleaseUploader:
return False
def upload_multiple_assets(self, project_path: str, release_id: str,
asset_files: List[str], overwrite: bool = True) -> List[bool]:
asset_files: List[str], overwrite: bool = True, show_progress: bool = True) -> List[bool]:
"""
上传多个assets
@@ -186,6 +236,7 @@ class CNBReleaseUploader:
release_id: release ID
asset_files: asset文件路径列表
overwrite: 是否覆盖现有文件
show_progress: 是否显示进度条
Returns:
每个文件的上传结果列表
@@ -193,6 +244,18 @@ class CNBReleaseUploader:
results = []
print(f"\n📦 开始上传 {len(asset_files)} 个文件到release {release_id}...")
# 计算总文件大小用于整体进度显示
total_size = 0
valid_files = []
for file_path in asset_files:
if os.path.exists(file_path):
total_size += os.path.getsize(file_path)
valid_files.append(file_path)
if show_progress and valid_files:
print(f"📊 总计 {len(valid_files)} 个有效文件,总大小: {total_size / 1024 / 1024:.2f} MB")
print("" + "=" * 60)
for i, file_path in enumerate(asset_files, 1):
if not os.path.exists(file_path):
@@ -216,7 +279,7 @@ class CNBReleaseUploader:
continue
# 2. 上传文件
upload_success = self.upload_asset(upload_info['upload_url'], file_path)
upload_success = self.upload_asset(upload_info['upload_url'], file_path, show_progress)
time.sleep(1)
# 3. 验证上传如果有验证URL
@@ -299,6 +362,7 @@ def main():
parser = argparse.ArgumentParser(description='CNB Release Uploader - JSON配置版本')
parser.add_argument('config', help='JSON配置字符串或JSON配置文件路径')
parser.add_argument('--dry-run', action='store_true', help='只验证配置,不执行上传')
parser.add_argument('--no-progress', action='store_true', help='禁用上传进度条显示')
args = parser.parse_args()
@@ -346,8 +410,9 @@ def main():
asset_files = config.get('asset_files', [])
if asset_files:
overwrite = config.get('overwrite', True)
show_progress = not args.no_progress # 默认显示进度条,除非指定 --no-progress
results = uploader.upload_multiple_assets(
config['project_path'], release_id, asset_files, overwrite
config['project_path'], release_id, asset_files, overwrite, show_progress
)
# 3. 显示结果

94
.github/workflows/cnb_trigger.py vendored Normal file
View File

@@ -0,0 +1,94 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Better Genshin Impact Build Trigger Script
用于触发CNB构建的Python脚本
"""
import requests
import json
import sys
import argparse
def trigger_build(token, branch="main", event="api_trigger_one", runid=None):
"""
触发构建请求
Args:
token (str): 授权token
branch (str): 分支名称默认为main
event (str): 事件类型默认为api_trigger_one
runid (str): 运行ID可选参数
Returns:
dict: API响应结果
"""
url = "https://api.cnb.cool/bettergi/better-genshin-impact/-/build/start"
headers = {
"Accept": "application/json",
"Authorization": f"Bearer {token}",
"Content-Type": "application/json",
"Host": "api.cnb.cool",
"Connection": "keep-alive"
}
data = {
"branch": branch,
"event": event
}
# 如果提供了runid则添加到env中
if runid:
data["env"] = {
"RUN_ID": runid
}
try:
print(f"正在发起构建请求...")
print(f"URL: {url}")
print(f"请求体: {json.dumps(data, indent=2, ensure_ascii=False)}")
response = requests.post(url, headers=headers, json=data)
print(f"响应状态码: {response.status_code}")
if response.status_code == 200:
result = response.json()
print("构建触发成功!")
print(f"响应内容: {json.dumps(result, indent=2, ensure_ascii=False)}")
return result
else:
print(f"请求失败: {response.status_code}")
print(f"错误信息: {response.text}")
return None
except requests.exceptions.RequestException as e:
print(f"请求异常: {e}")
return None
except json.JSONDecodeError as e:
print(f"JSON解析错误: {e}")
print(f"响应内容: {response.text}")
return None
def main():
parser = argparse.ArgumentParser(description="触发Better Genshin Impact构建")
parser.add_argument("token", help="授权token")
parser.add_argument("--branch", default="main", help="分支名称 (默认: main)")
parser.add_argument("--event", default="api_trigger_one", help="事件类型 (默认: api_trigger_one)")
parser.add_argument("--runid", help="运行ID (可选)")
args = parser.parse_args()
if not args.token:
print("错误: 必须提供token参数")
sys.exit(1)
result = trigger_build(args.token, args.branch, args.event, args.runid)
if result is None:
sys.exit(1)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,507 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
GitHub Actions 构建物下载和上传脚本
该脚本用于:
1. 从 GitHub Actions 下载最新的构建物 (BetterGI_7z 和 BetterGI_Install)
2. 解压构建物到本地
3. 调用 cnb_release.py 上传文件到 CNB
使用方法:
python github_download_and_cnb_upload.py --cnb-token YOUR_CNB_TOKEN [--github-token YOUR_GITHUB_TOKEN] [--run-id RUN_ID]
参数说明:
--cnb-token: CNB API Token (必需)
--github-token: GitHub Personal Access Token (可选用于提高API限制)
--run-id: 指定 GitHub Actions 运行 ID (可选,默认获取最新运行)
依赖:
- requests: HTTP 请求库
- tqdm: 进度条显示库
安装依赖pip install -r requirements.txt
"""
import os
import sys
import json
import requests
import zipfile
import tempfile
import shutil
import re
from pathlib import Path
from typing import List, Dict, Optional
from tqdm import tqdm
# 导入 CNBReleaseUploader
from cnb_release import CNBReleaseUploader
class GitHubActionsDownloader:
def __init__(self, token: Optional[str] = None):
"""
初始化 GitHub Actions 下载器
Args:
token: GitHub Personal Access Token (可选用于提高API限制)
"""
self.token = token
self.headers = {
'Accept': 'application/vnd.github.v3+json',
'User-Agent': 'BetterGI-Downloader/1.0.0'
}
if token:
self.headers['Authorization'] = f'token {token}'
def get_latest_workflow_run(self, owner: str, repo: str, workflow_file: str) -> Optional[Dict]:
"""
获取最新的工作流运行
Args:
owner: 仓库所有者
repo: 仓库名称
workflow_file: 工作流文件名
Returns:
最新的工作流运行信息或None
"""
url = f'https://api.github.com/repos/{owner}/{repo}/actions/workflows/{workflow_file}/runs'
params = {
'status': 'completed',
'conclusion': 'success',
'per_page': 1
}
try:
response = requests.get(url, headers=self.headers, params=params)
response.raise_for_status()
data = response.json()
runs = data.get('workflow_runs', [])
if not runs:
print("❌ 没有找到成功完成的工作流运行")
return None
latest_run = runs[0]
print(f"✅ 找到最新的工作流运行:")
print(f" Run ID: {latest_run['id']}")
print(f" 创建时间: {latest_run['created_at']}")
print(f" 状态: {latest_run['status']} / {latest_run['conclusion']}")
print(f" 分支: {latest_run['head_branch']}")
return latest_run
except requests.exceptions.RequestException as e:
print(f"❌ 获取工作流运行失败: {e}")
return None
def get_artifacts(self, owner: str, repo: str, run_id: int) -> List[Dict]:
"""
获取指定运行的构建物列表
Args:
owner: 仓库所有者
repo: 仓库名称
run_id: 运行ID
Returns:
构建物列表
"""
url = f'https://api.github.com/repos/{owner}/{repo}/actions/runs/{run_id}/artifacts'
try:
response = requests.get(url, headers=self.headers)
response.raise_for_status()
data = response.json()
artifacts = data.get('artifacts', [])
print(f"📦 找到 {len(artifacts)} 个构建物:")
for artifact in artifacts:
print(f" - {artifact['name']} ({artifact['size_in_bytes']:,} bytes)")
return artifacts
except requests.exceptions.RequestException as e:
print(f"❌ 获取构建物列表失败: {e}")
return []
def download_artifact(self, owner: str, repo: str, artifact_id: int,
artifact_name: str, download_dir: str) -> Optional[str]:
"""
下载指定的构建物
Args:
owner: 仓库所有者
repo: 仓库名称
artifact_id: 构建物ID
artifact_name: 构建物名称
download_dir: 下载目录
Returns:
下载的文件路径或None
"""
url = f'https://api.github.com/repos/{owner}/{repo}/actions/artifacts/{artifact_id}/zip'
try:
print(f"📥 开始下载构建物: {artifact_name}")
response = requests.get(url, headers=self.headers, stream=True)
response.raise_for_status()
# 获取文件总大小
total_size = int(response.headers.get('content-length', 0))
# 保存到临时文件
zip_path = os.path.join(download_dir, f"{artifact_name}.zip")
# 使用 tqdm 创建进度条
chunk_size = 8192
with open(zip_path, 'wb') as f:
with tqdm(
total=total_size,
unit='B',
unit_scale=True,
unit_divisor=1024,
desc=f"下载 {artifact_name}",
ncols=80,
bar_format='{desc}: {percentage:3.0f}%|{bar}| {n_fmt}/{total_fmt} [{elapsed}<{remaining}, {rate_fmt}]'
) as pbar:
for chunk in response.iter_content(chunk_size=chunk_size):
if chunk:
f.write(chunk)
pbar.update(len(chunk))
print(f"✅ 下载完成: {zip_path}")
return zip_path
except requests.exceptions.RequestException as e:
print(f"❌ 下载构建物失败 ({artifact_name}): {e}")
return None
def extract_artifact(self, zip_path: str, extract_dir: str) -> List[str]:
"""
解压构建物
Args:
zip_path: ZIP文件路径
extract_dir: 解压目录
Returns:
解压出的文件列表
"""
extracted_files = []
try:
print(f"📂 解压构建物: {os.path.basename(zip_path)}")
with zipfile.ZipFile(zip_path, 'r') as zip_ref:
zip_ref.extractall(extract_dir)
extracted_files = [os.path.join(extract_dir, name) for name in zip_ref.namelist()]
print(f"✅ 解压完成,共 {len(extracted_files)} 个文件")
for file_path in extracted_files:
if os.path.isfile(file_path):
size = os.path.getsize(file_path)
print(f" - {os.path.basename(file_path)} ({size:,} bytes)")
return extracted_files
except Exception as e:
print(f"❌ 解压失败: {e}")
return []
def extract_version_from_filename(filename: str) -> Optional[str]:
"""
从文件名中提取版本号
Args:
filename: 文件名
Returns:
版本号或None
"""
# 去除扩展名
import os
filename_without_ext = os.path.splitext(filename)[0]
# 匹配版本号模式,如 v1.2.3, 1.2.3-alpha.1 等
patterns = [
r'v?([0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?)',
r'_v?([0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?)',
]
for pattern in patterns:
match = re.search(pattern, filename_without_ext)
if match:
return match.group(1)
return None
def create_cnb_config(files: List[str], version: str, token: str) -> Dict:
"""
创建CNB上传配置
Args:
files: 要上传的文件列表
version: 版本号
token: CNB token
Returns:
CNB配置字典
"""
# 判断是否为预发布版本
is_prerelease = '-' in version
make_latest = "false" if is_prerelease else "true"
config = {
"token": token,
"project_path": "bettergi/better-genshin-impact",
"base_url": "https://api.cnb.cool",
"overwrite": True,
"release_data": {
"tag_name": f"v{version}",
"name": f"BetterGI v{version}",
"body": f"BetterGI v{version} 自动发布",
"draft": False,
"prerelease": is_prerelease,
"target_commitish": "main",
"make_latest": make_latest
},
"asset_files": files
}
return config
def main():
import argparse
# 解析命令行参数
parser = argparse.ArgumentParser(description='BetterGI 构建物下载和上传工具')
parser.add_argument('--run-id', type=str, help='指定 GitHub Actions 运行 ID如果提供则不会获取最新运行')
parser.add_argument('--github-token', type=str, help='GitHub Personal Access Token')
parser.add_argument('--cnb-token', type=str, required=True, help='CNB API Token (必需)')
args = parser.parse_args()
print("🚀 BetterGI 构建物下载和上传工具")
print("=" * 50)
# 获取 token优先使用命令行参数其次使用环境变量
github_token = args.github_token or os.getenv('GITHUB_TOKEN')
cnb_token = args.cnb_token or os.getenv('CNB_TOKEN')
if not cnb_token:
print("❌ 错误: 请设置 CNB_TOKEN 环境变量")
return 1
if not github_token:
print("⚠️ 警告: 未设置 GITHUB_TOKEN可能会遇到API限制")
# 确定运行 ID
if args.run_id:
print(f"\n🎯 使用指定的运行 ID: {args.run_id}")
run_id = args.run_id
else:
# 创建下载器来获取最新运行 ID
downloader = GitHubActionsDownloader(github_token)
print("\n🔍 查找最新的工作流运行...")
latest_run = downloader.get_latest_workflow_run('babalae', 'better-genshin-impact', 'publish.yml')
if not latest_run:
return 1
run_id = str(latest_run['id'])
# 使用当前目录下的固定目录以action运行ID命名
work_dir = os.path.join(os.getcwd(), 'github_actions_cache', run_id)
download_dir = os.path.join(work_dir, 'downloads')
extract_dir = os.path.join(work_dir, 'extracted')
print(f"\n📁 使用工作目录: {work_dir}")
# 检查是否已存在解压后的文件
all_files = []
version = None
# 检查解压目录是否已存在且包含文件
if os.path.exists(extract_dir):
print("🔍 检查已存在的构建物...")
existing_files = []
# 预期的构建物名称
expected_artifacts = ['BetterGI_7z', 'BetterGI_Install']
for artifact_name in expected_artifacts:
artifact_extract_dir = os.path.join(extract_dir, artifact_name)
if os.path.exists(artifact_extract_dir):
for root, dirs, files in os.walk(artifact_extract_dir):
for file in files:
file_path = os.path.join(root, file)
existing_files.append(file_path)
# 尝试从文件名提取版本号
if not version:
filename = os.path.basename(file_path)
extracted_version = extract_version_from_filename(filename)
if extracted_version:
version = extracted_version
if existing_files and version:
print(f"✅ 发现已存在的构建物 ({len(existing_files)} 个文件),跳过下载")
print(f"📋 检测到版本号: {version}")
all_files = existing_files
else:
print("⚠️ 已存在目录但未找到有效文件,将重新下载")
# 如果没有找到已存在的文件,则进行下载和解压
if not all_files:
print("📥 需要下载构建物,正在获取构建物信息...")
# 如果还没有创建下载器,现在创建
if 'downloader' not in locals():
downloader = GitHubActionsDownloader(github_token)
# 获取构建物列表
print("\n📦 获取构建物列表...")
artifacts = downloader.get_artifacts('babalae', 'better-genshin-impact', int(run_id))
if not artifacts:
return 1
# 筛选需要的构建物
target_artifacts = []
for artifact in artifacts:
if artifact['name'] in ['BetterGI_7z', 'BetterGI_Install']:
target_artifacts.append(artifact)
if len(target_artifacts) != 2:
print(f"❌ 错误: 期望找到2个构建物实际找到 {len(target_artifacts)}")
return 1
print("📥 开始下载和解压构建物...")
os.makedirs(download_dir, exist_ok=True)
os.makedirs(extract_dir, exist_ok=True)
for artifact in target_artifacts:
# 下载
zip_path = downloader.download_artifact(
'babalae', 'better-genshin-impact',
artifact['id'], artifact['name'], download_dir
)
if not zip_path:
continue
# 解压
artifact_extract_dir = os.path.join(extract_dir, artifact['name'])
os.makedirs(artifact_extract_dir, exist_ok=True)
extracted_files = downloader.extract_artifact(zip_path, artifact_extract_dir)
# 收集文件并提取版本号
for file_path in extracted_files:
if os.path.isfile(file_path):
all_files.append(file_path)
# 尝试从文件名提取版本号
if not version:
filename = os.path.basename(file_path)
extracted_version = extract_version_from_filename(filename)
if extracted_version:
version = extracted_version
print(f"📋 检测到版本号: {version}")
if not all_files:
print("❌ 错误: 没有找到可上传的文件")
return 1
if not version:
print("❌ 错误: 无法从文件名中提取版本号")
return 1
print(f"\n📋 准备上传 {len(all_files)} 个文件:")
for file_path in all_files:
size = os.path.getsize(file_path)
print(f" - {os.path.basename(file_path)} ({size:,} bytes)")
# 创建CNB配置
print("\n⚙️ 创建CNB配置...")
cnb_config = create_cnb_config(all_files, version, cnb_token)
# 保存配置文件
config_path = os.path.join(work_dir, 'cnb_config.json')
with open(config_path, 'w', encoding='utf-8') as f:
json.dump(cnb_config, f, indent=2, ensure_ascii=False)
print(f"✅ 配置文件已保存: {config_path}")
# 直接调用 CNBReleaseUploader
print("\n🚀 开始上传到CNB...")
try:
# 创建 CNBReleaseUploader 实例
uploader = CNBReleaseUploader(
token=cnb_config['token'],
base_url=cnb_config.get('base_url', 'https://api.cnb.cool')
)
# 创建 release
print(f"📝 创建 release: {cnb_config['release_data']['name']}")
release_result = uploader.create_release(
project_path=cnb_config['project_path'],
release_data=cnb_config['release_data']
)
if not release_result:
print("❌ 创建 release 失败")
return 1
print(f"✅ Release 创建成功: {release_result['name']}")
# 上传文件
print(f"📤 开始上传 {len(cnb_config['asset_files'])} 个文件...")
upload_results = uploader.upload_multiple_assets(
project_path=cnb_config['project_path'],
release_id=release_result['id'],
asset_files=cnb_config['asset_files'],
overwrite=cnb_config.get('overwrite', True)
)
# 检查上传结果
success_count = sum(1 for result in upload_results if result)
total_count = len(upload_results)
print(f"\n📊 上传结果汇总:")
print(f" ✅ 成功: {success_count}/{total_count}")
if success_count < total_count:
print(f" ❌ 失败: {total_count - success_count}/{total_count}")
for i, result in enumerate(upload_results):
if not result:
file_name = os.path.basename(cnb_config['asset_files'][i])
print(f" - {file_name}: 上传失败")
if success_count == total_count:
print("\n🎉 所有文件上传完成!")
return 0
else:
print("\n❌ 部分文件上传失败")
return 1
except Exception as e:
print(f"❌ CNB上传失败: {e}")
return 1
if __name__ == '__main__':
try:
exit_code = main()
sys.exit(exit_code)
except KeyboardInterrupt:
print("\n⚠️ 用户中断操作")
sys.exit(1)
except Exception as e:
print(f"\n💥 程序异常: {e}")
sys.exit(1)

View File

@@ -21,6 +21,11 @@ on:
description: '创建 GitHub Release 草稿'
required: true
default: false
upload-to-steambird:
type: boolean
description: '上传到 Steambird'
required: true
default: false
jobs:
# Add validation job to check version format
@@ -446,7 +451,7 @@ jobs:
upload_token: ${{ secrets.MirrorChyanUploadToken }}
cnb_uploading:
if: github.repository_owner == 'babalae'
if: github.repository_owner == 'babalae' && false
needs: [validate, build_dist, build_installer]
runs-on: ubuntu-latest
steps:
@@ -460,7 +465,7 @@ jobs:
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install requests
pip install -r .github/workflows/requirements.txt
- uses: actions/download-artifact@v4
with:
@@ -537,3 +542,55 @@ jobs:
# 执行上传
python cnb_release.py cnb_config.json
cnb_trigger:
if: github.repository_owner == 'babalae'
needs: [validate, build_dist, build_installer]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.x'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r .github/workflows/requirements.txt
- name: Trigger CNB Build
env:
CNB_TOKEN: ${{ secrets.CNB_TOKEN }}
run: |
python .github/workflows/cnb_trigger.py "$CNB_TOKEN" --runid ${{ github.run_id }}
upload_to_steambird:
if: ${{ inputs.upload-to-steambird && github.repository_owner == 'babalae' }}
needs: [validate, build_dist, build_installer]
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v4
with:
path: artifacts
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Prepare files for upload
run: |
mkdir -p upload_files
# 只复制 7z 文件和 Install.exe 文件
find artifacts -name "*.7z" -exec cp {} upload_files/ \;
find artifacts -name "BetterGI.Install.*.exe" -exec cp {} upload_files/ \;
echo "📦 准备上传的文件:"
ls -la upload_files/
- name: Upload to Steambird
env:
TUS_USER: "bgi"
TUS_PASS: ${{ secrets.TUS_PASS }}
run: |
wget https://uploads.steambird.pub/upload.sh
chmod +x upload.sh
./upload.sh https://uploads.steambird.pub/dav/bgi/ upload_files/BetterGI.Install.*.exe

3
.github/workflows/requirements.txt vendored Normal file
View File

@@ -0,0 +1,3 @@
# CNB Release Uploader 依赖
requests>=2.25.0
tqdm>=4.60.0

5
.gitignore vendored
View File

@@ -24,6 +24,7 @@ bld/
Tmp/
/packages/
node_modules/
github_actions_cache/
*.zip
@@ -31,4 +32,6 @@ node_modules/
.idea
.trae
.claude
CLAUDE.md
CLAUDE.md
__pycache__/

View File

@@ -2,7 +2,7 @@
<PropertyGroup>
<AssemblyName>BetterGI</AssemblyName>
<Version>0.48.2-alpha.4</Version>
<Version>0.49.0</Version>
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0-windows10.0.22621.0</TargetFramework>
@@ -63,6 +63,7 @@
<PackageReference Include="Microsoft.ML.OnnxRuntime.Managed" Version="1.21.0" />
<PackageReference Include="Microsoft.Toolkit.Uwp.Notifications" Version="7.1.3" />
<PackageReference Include="Microsoft.Web.WebView2" Version="1.0.2592.51" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Ookii.Dialogs.Wpf" Version="5.0.1" />
<PackageReference Include="OpenCvSharp4.WpfExtensions" Version="4.11.0.20250507" />
<PackageReference Include="OpenCvSharp4.Extensions" Version="4.11.0.20250507" />

View File

@@ -0,0 +1,85 @@
using System.Collections.Frozen;
using System.Collections.Generic;
namespace BetterGenshinImpact.GameTask.AutoArtifactSalvage
{
/// <summary>
/// 圣遗物词条
/// </summary>
public class ArtifactAffix
{
public ArtifactAffix(ArtifactAffixType type, float value)
{
Type = type;
Value = value;
}
public ArtifactAffixType Type { get; private set; }
public float Value { get; private set; }
public static FrozenDictionary<ArtifactAffixType, string> DefaultStrDic { get; } = new Dictionary<ArtifactAffixType, string>() {
{ ArtifactAffixType.ATK, "攻击力" },
{ ArtifactAffixType.ATKPercent, "攻击力" },
{ ArtifactAffixType.DEF, "防御力" },
{ ArtifactAffixType.DEFPercent, "防御力" },
{ ArtifactAffixType.HP, "生命值" },
{ ArtifactAffixType.HPPercent, "生命值" },
{ ArtifactAffixType.CRITRate, "暴击率" },
{ ArtifactAffixType.CRITDMG, "暴击伤害" },
{ ArtifactAffixType.ElementalMastery, "元素精通" },
{ ArtifactAffixType.EnergyRecharge, "元素充能效率" },
{ ArtifactAffixType.HealingBonus, "治疗加成" },
{ ArtifactAffixType.PhysicalDMGBonus, "物理伤害加成" },
{ ArtifactAffixType.PyroDMGBonus, "火元素伤害加成" },
{ ArtifactAffixType.HydroDMGBonus, "水元素伤害加成" },
{ ArtifactAffixType.DendroDMGBonus, "草元素伤害加成" },
{ ArtifactAffixType.ElectroDMGBonus, "雷元素伤害加成" },
{ ArtifactAffixType.AnemoDMGBonus, "风元素伤害加成" },
{ ArtifactAffixType.CryoDMGBonus, "冰元素伤害加成" },
{ ArtifactAffixType.GeoDMGBonus, "岩元素伤害加成" }
}.ToFrozenDictionary();
}
public enum ArtifactAffixType
{
ATK,
ATKPercent,
DEF,
DEFPercent,
HP,
HPPercent,
CRITRate,
CRITDMG,
ElementalMastery,
EnergyRecharge,
HealingBonus,
PhysicalDMGBonus,
/// <summary>
/// 火
/// </summary>
PyroDMGBonus,
/// <summary>
/// 水
/// </summary>
HydroDMGBonus,
/// <summary>
/// 草
/// </summary>
DendroDMGBonus,
/// <summary>
/// 雷
/// </summary>
ElectroDMGBonus,
/// <summary>
/// 风
/// </summary>
AnemoDMGBonus,
/// <summary>
/// 冰
/// </summary>
CryoDMGBonus,
/// <summary>
/// 岩
/// </summary>
GeoDMGBonus,
}
}

View File

@@ -0,0 +1,38 @@
namespace BetterGenshinImpact.GameTask.AutoArtifactSalvage
{
/// <summary>
/// 圣遗物数值面板信息
/// </summary>
public class ArtifactStat
{
public ArtifactStat(string name, ArtifactAffix mainAffix, ArtifactAffix[] minorAffix, int level)
{
Name = name;
MainAffix = mainAffix;
MinorAffixes = minorAffix;
Level = level;
}
/// <summary>
/// 名称
/// </summary>
public string Name { get; private set; }
/// <summary>
/// 主词条
/// </summary>
public ArtifactAffix MainAffix { get; private set; }
/// <summary>
/// 副词条数组
/// </summary>
public ArtifactAffix[] MinorAffixes { get; private set; }
/// <summary>
/// 等级
/// </summary>
public int Level { get; private set; }
// PS圣遗物的种类和品质在点击查看之前就可以通过识别图标获悉所以不必在此模型类中获取
}
}

View File

@@ -1,4 +1,4 @@
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.ComponentModel;
using System;
namespace BetterGenshinImpact.GameTask.AutoArtifactSalvage;
@@ -6,7 +6,17 @@ namespace BetterGenshinImpact.GameTask.AutoArtifactSalvage;
[Serializable]
public partial class AutoArtifactSalvageConfig : ObservableObject
{
// JavaScript
[ObservableProperty]
private string _javaScript =
@"(async function (artifact) {
var hasATK = Array.from(artifact.MinorAffixes).some(affix => affix.Type == 'ATK');
var hasDEF = Array.from(artifact.MinorAffixes).some(affix => affix.Type == 'DEF');
Output = hasATK && hasDEF;
})(ArtifactStat);";
// 正则表达式
[Obsolete]
[ObservableProperty]
private string _regularExpression = @"(?=[\S\s]*攻击力\+[\d]*\n)(?=[\S\s]*防御力\+[\d]*\n)";

View File

@@ -1,30 +1,32 @@
using BetterGenshinImpact.Core.Recognition;
using BetterGenshinImpact.Core.Recognition.OCR;
using BetterGenshinImpact.Core.Recognition.OpenCv;
using BetterGenshinImpact.Core.Simulator;
using BetterGenshinImpact.Core.Simulator.Extensions;
using BetterGenshinImpact.GameTask.Common;
using BetterGenshinImpact.GameTask.Common.BgiVision;
using BetterGenshinImpact.GameTask.Common.Element.Assets;
using BetterGenshinImpact.GameTask.Common.Job;
using BetterGenshinImpact.GameTask.Model.Area;
using BetterGenshinImpact.GameTask.Model.GameUI;
using BetterGenshinImpact.Helpers;
using BetterGenshinImpact.Helpers.Extensions;
using Fischless.WindowsInput;
using Microsoft.ClearScript;
using Microsoft.ClearScript.V8;
using Microsoft.Extensions.Localization;
using Microsoft.Extensions.Logging;
using OpenCvSharp;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using BetterGenshinImpact.Core.Recognition;
using BetterGenshinImpact.Core.Simulator;
using BetterGenshinImpact.GameTask.Common.Element.Assets;
using BetterGenshinImpact.Helpers.Extensions;
using Microsoft.Extensions.Logging;
using Vanara.PInvoke;
using static BetterGenshinImpact.GameTask.Common.TaskControl;
using BetterGenshinImpact.Core.Simulator.Extensions;
using Microsoft.Extensions.Localization;
using System.Globalization;
using BetterGenshinImpact.Helpers;
using System.Text.RegularExpressions;
using BetterGenshinImpact.GameTask.Model.Area;
using System.Collections.Generic;
using Fischless.WindowsInput;
using OpenCvSharp;
using System.Linq;
using BetterGenshinImpact.Core.Recognition.OpenCv;
using BetterGenshinImpact.Core.Recognition.OCR;
using BetterGenshinImpact.GameTask.Common;
using BetterGenshinImpact.GameTask.Common.Job;
using BetterGenshinImpact.GameTask.Model.GameUI;
namespace BetterGenshinImpact.GameTask.AutoArtifactSalvage;
@@ -46,19 +48,21 @@ public class AutoArtifactSalvageTask : ISoloTask
private readonly string[] numOfStarLocalizedString;
private readonly string? regularExpression;
private readonly string? javaScript;
private readonly int? maxNumToCheck;
private readonly bool returnToMainUi = true;
public AutoArtifactSalvageTask(int star, string? regularExpression = null, int? maxNumToCheck = null)
private readonly CultureInfo cultureInfo;
public AutoArtifactSalvageTask(int star, string? javaScript = null, int? maxNumToCheck = null)
{
this.star = star;
this.regularExpression = regularExpression;
this.javaScript = javaScript;
this.maxNumToCheck = maxNumToCheck;
IStringLocalizer<AutoArtifactSalvageTask> stringLocalizer = App.GetService<IStringLocalizer<AutoArtifactSalvageTask>>() ?? throw new NullReferenceException();
CultureInfo cultureInfo = new CultureInfo(TaskContext.Instance().Config.OtherConfig.GameCultureInfoName);
this.cultureInfo = new CultureInfo(TaskContext.Instance().Config.OtherConfig.GameCultureInfoName);
quickSelectLocalizedString = stringLocalizer.WithCultureGet(cultureInfo, "快速选择");
numOfStarLocalizedString =
[
@@ -237,7 +241,7 @@ public class AutoArtifactSalvageTask : ISoloTask
{
logger.LogInformation("完成{Star}星圣遗物快速分解", star);
await Delay(400, ct);
if (regularExpression != null)
if (javaScript != null)
{
input.Mouse.LeftButtonClick();
await Delay(1000, ct);
@@ -254,9 +258,9 @@ public class AutoArtifactSalvageTask : ISoloTask
}
// 分解5星
if (regularExpression != null)
if (javaScript != null)
{
await Salvage5Star(this.regularExpression, this.maxNumToCheck ?? throw new ArgumentException($"{nameof(this.maxNumToCheck)}不能为空"));
await Salvage5Star(this.javaScript, this.maxNumToCheck ?? throw new ArgumentException($"{nameof(this.maxNumToCheck)}不能为空"));
logger.LogInformation("筛选完毕,请复查并手动分解");
}
else
@@ -270,7 +274,7 @@ public class AutoArtifactSalvageTask : ISoloTask
}
}
private async Task Salvage5Star(string regularExpression, int maxNumToCheck)
private async Task Salvage5Star(string javaScript, int maxNumToCheck)
{
int count = maxNumToCheck;
@@ -291,15 +295,15 @@ public class AutoArtifactSalvageTask : ISoloTask
if (GetArtifactStatus(itemRegion1.SrcMat) == ArtifactStatus.Selected)
{
using ImageRegion card = ra1.DeriveCrop(new Rect((int)(ra1.Width * 0.70), (int)(ra1.Width * 0.055), (int)(ra1.Width * 0.24), (int)(ra1.Width * 0.29)));
string affixes = GetArtifactAffixes(card.SrcMat, OcrFactory.Paddle);
ArtifactStat artifact = GetArtifactStat(card.SrcMat, OcrFactory.Paddle, this.cultureInfo, out string allText);
if (IsMatchRegularExpression(affixes, regularExpression, out string msg))
if (IsMatchJavaScript(artifact, javaScript))
{
logger.LogInformation(message: msg);
// logger.LogInformation(message: msg);
}
else
{
itemRegion.Click();
itemRegion.Click(); // 反选取消
await Delay(100, ct);
}
}
@@ -314,6 +318,45 @@ public class AutoArtifactSalvageTask : ISoloTask
}
}
/// <summary>
/// 是否匹配JavaScript的计算结果
/// </summary>
/// <param name="artifact">作为JS入参JS使用“ArtifactStat”获取</param>
/// <param name="javaScript"></param>
/// <param name="engine">由调用者控制生命周期</param>
/// <returns>是否匹配。取JS的“Output”作为出参</returns>
/// <exception cref="InvalidOperationException"></exception>
/// <exception cref="Exception"></exception>
public static bool IsMatchJavaScript(ArtifactStat artifact, string javaScript)
{
using V8ScriptEngine engine = new V8ScriptEngine(V8ScriptEngineFlags.UseCaseInsensitiveMemberBinding | V8ScriptEngineFlags.DisableGlobalMembers);
try
{
// 传入输入参数
engine.Script.ArtifactStat = artifact;
// 执行JavaScript代码
engine.Execute(javaScript);
// 检查是否有输出
if (!engine.Script.propertyIsEnumerable("Output"))
{
throw new InvalidOperationException("JavaScript没有设置Output输出");
}
if (engine.Script.Output is not bool)
{
throw new InvalidOperationException("JavaScript的Output输出不是布尔类型");
}
return (bool)engine.Script.Output;
}
catch (ScriptEngineException ex)
{
throw new Exception($"JavaScript execution error: {ex.Message}", ex);
}
}
public static bool IsMatchRegularExpression(string affixes, string regularExpression, out string msg)
{
Match match = Regex.Match(affixes, regularExpression);
@@ -336,10 +379,140 @@ public class AutoArtifactSalvageTask : ISoloTask
return match.Success;
}
public static string GetArtifactAffixes(Mat src, IOcrService ocrService)
public static ArtifactStat GetArtifactStat(Mat src, IOcrService ocrService, CultureInfo cultureInfo, out string allText)
{
var ocrResult = ocrService.OcrResult(src);
return ocrResult.Text;
allText = ocrResult.Text;
var lines = ocrResult.Text.Split('\n');
string percentStr = "%";
// 名称
string name = lines[0];
#region
var defaultMainAffix = ArtifactAffix.DefaultStrDic.Select(kvp => kvp.Value).Distinct();
string mainAffixTypeLine = lines.Single(l => defaultMainAffix.Contains(l));
ArtifactAffixType mainAffixType = ArtifactAffix.DefaultStrDic.First(kvp => kvp.Value == mainAffixTypeLine).Key;
string mainAffixValueLine = lines.Select(l =>
{
string pattern = @"^(\d+\.?\d*)(%?)$";
pattern = pattern.Replace("%", percentStr); // 这样一行一行写只是为了IDE能保持正则字符串高亮
Match match = Regex.Match(l, pattern);
if (match.Success)
{
return match.Groups[1].Value;
}
else
{
return null;
}
}).Where(l => l != null).Cast<string>().Single();
if (!float.TryParse(mainAffixValueLine, NumberStyles.Any, cultureInfo, out float value))
{
throw new Exception($"未识别的主词条数值:{mainAffixValueLine}");
}
ArtifactAffix mainAffix = new ArtifactAffix(mainAffixType, value);
#endregion
#region
ArtifactAffix[] minorAffixes = lines.Select(l =>
{
string pattern = @"^[•·]?([^+]+)\+(\d+\.?\d*)(%?)$";
pattern = pattern.Replace("%", percentStr);
Match match = Regex.Match(l, pattern);
if (match.Success)
{
ArtifactAffixType artifactAffixType;
var dic = ArtifactAffix.DefaultStrDic;
if (match.Groups[1].Value.Contains(dic[ArtifactAffixType.ATK]))
{
if (String.IsNullOrEmpty(match.Groups[3].Value))
{
artifactAffixType = ArtifactAffixType.ATK;
}
else
{
artifactAffixType = ArtifactAffixType.ATKPercent;
}
}
else if (match.Groups[1].Value.Contains(dic[ArtifactAffixType.DEF]))
{
if (String.IsNullOrEmpty(match.Groups[3].Value))
{
artifactAffixType = ArtifactAffixType.DEF;
}
else
{
artifactAffixType = ArtifactAffixType.DEFPercent;
}
}
else if (match.Groups[1].Value.Contains(dic[ArtifactAffixType.HP]))
{
if (String.IsNullOrEmpty(match.Groups[3].Value))
{
artifactAffixType = ArtifactAffixType.HP;
}
else
{
artifactAffixType = ArtifactAffixType.HPPercent;
}
}
else if (match.Groups[1].Value.Contains(dic[ArtifactAffixType.CRITRate]))
{
artifactAffixType = ArtifactAffixType.CRITRate;
}
else if (match.Groups[1].Value.Contains(dic[ArtifactAffixType.CRITDMG]))
{
artifactAffixType = ArtifactAffixType.CRITDMG;
}
else if (match.Groups[1].Value.Contains(dic[ArtifactAffixType.ElementalMastery]))
{
artifactAffixType = ArtifactAffixType.ElementalMastery;
}
else if (match.Groups[1].Value.Contains(dic[ArtifactAffixType.EnergyRecharge]))
{
artifactAffixType = ArtifactAffixType.EnergyRecharge;
}
else
{
throw new Exception($"未识别的副词条:{match.Groups[1].Value}");
}
if (!float.TryParse(match.Groups[2].Value, NumberStyles.Any, cultureInfo, out float value))
{
throw new Exception($"未识别的副词条数值:{match.Groups[2].Value}");
}
return new ArtifactAffix(artifactAffixType, value);
}
else
{
return null;
}
}).Where(a => a != null).Cast<ArtifactAffix>().ToArray();
#endregion
#region
string levelLine = lines.Select(l =>
{
string pattern = @"^\+(\d*)$";
Match match = Regex.Match(l, pattern);
if (match.Success)
{
return match.Groups[1].Value;
}
else
{
return null;
}
}).Where(l => l != null).Cast<string>().Single();
if (!int.TryParse(levelLine, out int level) || level < 0 || level > 20)
{
throw new Exception($"未识别的等级:{levelLine}");
}
#endregion
return new ArtifactStat(name, mainAffix, minorAffixes, level);
}
public static ArtifactStatus GetArtifactStatus(Mat src)

View File

@@ -2291,7 +2291,11 @@
Grid.Column="0"
Foreground="{ui:ThemeResource TextFillColorTertiaryBrush}"
TextWrapping="Wrap">
指定匹配表达式逐一筛选分解支持5星圣遗物
指定匹配表达式逐一筛选分解支持5星圣遗物 -
<Hyperlink Command="{Binding GoToArtifactSalvageUrlCommand}"
Foreground="{ui:ThemeResource TextFillColorSecondaryBrush}">
点击查看使用教程
</Hyperlink>
</ui:TextBlock>
<controls:TwoStateButton Grid.Row="0"
Grid.RowSpan="2"
@@ -2343,20 +2347,28 @@
<ui:TextBlock Grid.Row="0"
Grid.Column="0"
FontTypography="Body"
Text="正则表达式"
TextWrapping="Wrap" />
TextWrapping="Wrap">
JavaScript -
<Hyperlink Command="{Binding GoToArtifactSalvageUrlCommand}"
Foreground="{ui:ThemeResource TextFillColorSecondaryBrush}">
点击查看使用教程
</Hyperlink>
</ui:TextBlock>
<ui:TextBlock Grid.Row="1"
Grid.Column="0"
Foreground="{ui:ThemeResource TextFillColorTertiaryBrush}"
Text="只要满足的圣遗物都会被选中"
TextWrapping="Wrap" />
TextWrapping="Wrap">
只要满足的圣遗物都会被选中
<LineBreak/>
JS接受ArtifactStat作为入参应对Output赋值一个布尔值作为返回
</ui:TextBlock>
<ui:TextBox Grid.Row="0"
Grid.RowSpan="2"
Grid.Column="1"
MinWidth="180"
MaxWidth="800"
Margin="0,0,36,0"
Text="{Binding Config.AutoArtifactSalvageConfig.RegularExpression, Mode=TwoWay}"
Text="{Binding Config.AutoArtifactSalvageConfig.JavaScript, Mode=TwoWay}"
TextWrapping="Wrap" Cursor="IBeam" />
</Grid>
<Grid Margin="16">

View File

@@ -1,8 +1,9 @@
using BetterGenshinImpact.Core.Recognition.OCR;
using BetterGenshinImpact.Core.Recognition.OCR;
using BetterGenshinImpact.GameTask;
using BetterGenshinImpact.GameTask.AutoArtifactSalvage;
using BetterGenshinImpact.GameTask.Common;
using BetterGenshinImpact.GameTask.Model.Area;
using BetterGenshinImpact.Helpers.Extensions;
using System.Globalization;
using System.Windows;
namespace BetterGenshinImpact.View.Windows;
@@ -13,14 +14,14 @@ public partial class OcrDialog
private readonly double yRatio;
private readonly double widthRatio;
private readonly double heightRatio;
private readonly string? regularExpression;
public OcrDialog(double xRatio, double yRatio, double widthRatio, double heightRatio, string title, string? regularExpression = null)
private readonly string? javaScript;
public OcrDialog(double xRatio, double yRatio, double widthRatio, double heightRatio, string title, string? javaScript = null)
{
this.xRatio = xRatio;
this.yRatio = yRatio;
this.widthRatio = widthRatio;
this.heightRatio = heightRatio;
this.regularExpression = regularExpression;
this.javaScript = javaScript;
InitializeComponent();
@@ -36,11 +37,12 @@ public partial class OcrDialog
this.Screenshot.Source = bitmapImage;
this.TxtRecognized.Text = OcrFactory.Paddle.OcrResult(card.SrcMat).Text;
if (this.regularExpression != null)
ArtifactStat artifact = AutoArtifactSalvageTask.GetArtifactStat(card.SrcMat, OcrFactory.Paddle, new CultureInfo(TaskContext.Instance().Config.OtherConfig.GameCultureInfoName), out string allText);
this.TxtRecognized.Text = allText;
if (this.javaScript != null)
{
AutoArtifactSalvageTask.IsMatchRegularExpression(this.TxtRecognized.Text, this.regularExpression, out string msg);
this.RegexResult.Text = msg;
bool isMatch = AutoArtifactSalvageTask.IsMatchJavaScript(artifact, this.javaScript);
this.RegexResult.Text = isMatch ? "匹配" : "不匹配";
}
this.UpdateLayout();
}

View File

@@ -527,14 +527,20 @@ public partial class TaskSettingsPageViewModel : ViewModel
{
SwitchArtifactSalvageEnabled = true;
await new TaskRunner()
.RunSoloTaskAsync(new AutoArtifactSalvageTask(int.Parse(Config.AutoArtifactSalvageConfig.MaxArtifactStar), Config.AutoArtifactSalvageConfig.RegularExpression, Config.AutoArtifactSalvageConfig.MaxNumToCheck));
.RunSoloTaskAsync(new AutoArtifactSalvageTask(int.Parse(Config.AutoArtifactSalvageConfig.MaxArtifactStar), Config.AutoArtifactSalvageConfig.JavaScript, Config.AutoArtifactSalvageConfig.MaxNumToCheck));
SwitchArtifactSalvageEnabled = false;
}
[RelayCommand]
private async Task OnGoToArtifactSalvageUrlAsync()
{
await Launcher.LaunchUriAsync(new Uri("https://bettergi.com/feats/task/artifactSalvage.html"));
}
[RelayCommand]
private void OnOpenArtifactSalvageTestOCRWindow()
{
OcrDialog ocrDialog = new OcrDialog(0.70, 0.098, 0.24, 0.52, "圣遗物分解", this.Config.AutoArtifactSalvageConfig.RegularExpression);
OcrDialog ocrDialog = new OcrDialog(0.70, 0.098, 0.24, 0.52, "圣遗物分解", this.Config.AutoArtifactSalvageConfig.JavaScript);
ocrDialog.ShowDialog();
}

View File

@@ -1,13 +1,13 @@
using BetterGenshinImpact.GameTask.AutoArtifactSalvage;
using BetterGenshinImpact.GameTask.AutoArtifactSalvage;
using BetterGenshinImpact.GameTask.Model.GameUI;
using BetterGenshinImpact.UnitTest.CoreTests.RecognitionTests.OCRTests;
using OpenCvSharp;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using static BetterGenshinImpact.GameTask.AutoArtifactSalvage.AutoArtifactSalvageTask;
namespace BetterGenshinImpact.UnitTest.GameTaskTests.AutoArtifactSalvageTests
@@ -73,7 +73,7 @@ namespace BetterGenshinImpact.UnitTest.GameTaskTests.AutoArtifactSalvageTests
private static ConcurrentDictionary<string, string> PaddleResultDic { get; } = new ConcurrentDictionary<string, string>();
/// <summary>
/// 测试获取分解圣遗物界面右侧圣遗物的词缀等属性,结果应正确
/// 测试获取分解圣遗物界面右侧圣遗物的词缀等属性,使用正则表达式,结果应正确
/// </summary>
/// <param name="screenshot"></param>
[Theory]
@@ -88,15 +88,17 @@ namespace BetterGenshinImpact.UnitTest.GameTaskTests.AutoArtifactSalvageTests
[InlineData(@"ArtifactAffixes.png", @"攻击力\+[\d]*\n", false)]
[InlineData(@"ArtifactAffixes.png", @"防御力\+[\d.]*%\n", false)]
[InlineData(@"ArtifactAffixes.png", @"防御力\+[\d]*\n")]
public void GetArtifactAffixes_ShouldBeRight(string screenshot, string pattern, bool isMatch = true)
public void GetArtifactStat_RegexPatternShouldBeRight(string screenshot, string pattern, bool isMatch = true)
{
//
CultureInfo cultureInfo = new CultureInfo("zh-Hans");
//
string result = PaddleResultDic.GetOrAdd(screenshot, screenshot_ =>
{
using Mat mat = new Mat(@$"..\..\..\Assets\AutoArtifactSalvage\{screenshot_}");
return AutoArtifactSalvageTask.GetArtifactAffixes(mat, paddle.Get());
GetArtifactStat(mat, paddle.Get(), cultureInfo, out string allText);
return allText;
});
//
@@ -109,5 +111,57 @@ namespace BetterGenshinImpact.UnitTest.GameTaskTests.AutoArtifactSalvageTests
Assert.DoesNotMatch(pattern, result);
}
}
/// <summary>
/// 测试获取分解圣遗物界面右侧圣遗物的各种结构化信息,结果应正确
/// </summary>
/// <param name="screenshot"></param>
[Theory]
[InlineData(@"ArtifactAffixes.png")]
public void GetArtifactStat_AffixesShouldBeRight(string screenshot)
{
//
CultureInfo cultureInfo = new CultureInfo("zh-Hans");
//
using Mat mat = new Mat(@$"..\..\..\Assets\AutoArtifactSalvage\{screenshot}");
ArtifactStat artifact = GetArtifactStat(mat, paddle.Get(), cultureInfo, out string _);
//
Assert.Equal("异种的期许", artifact.Name);
Assert.True(artifact.MainAffix.Type == ArtifactAffixType.HP);
Assert.True(artifact.MainAffix.Value == 717f);
Assert.Contains(artifact.MinorAffixes, a => a.Type == ArtifactAffixType.ElementalMastery && a.Value == 16f);
Assert.Contains(artifact.MinorAffixes, a => a.Type == ArtifactAffixType.EnergyRecharge && a.Value == 6.5f);
Assert.Contains(artifact.MinorAffixes, a => a.Type == ArtifactAffixType.ATKPercent && a.Value == 5.8f);
Assert.Contains(artifact.MinorAffixes, a => a.Type == ArtifactAffixType.DEF && a.Value == 23f);
Assert.True(artifact.Level == 0);
}
[Theory]
[InlineData(@"ArtifactAffixes.png", @"(async function (artifact) {
var hasATK = Array.from(artifact.MinorAffixes).some(affix => affix.Type == 'ATK');
var hasDEF = Array.from(artifact.MinorAffixes).some(affix => affix.Type == 'DEF');
Output = hasATK && hasDEF;
})(ArtifactStat);", false)]
[InlineData(@"ArtifactAffixes.png", @"(async function (artifact) {
var level = artifact.Level;
var hasATKPercent = Array.from(artifact.MinorAffixes).some(affix => affix.Type == 'ATKPercent');
var hasDEF = Array.from(artifact.MinorAffixes).some(affix => affix.Type == 'DEF');
Output = level == 0 && hasATKPercent && hasDEF;
})(ArtifactStat);", true)]
public void IsMatchJavaScript_JSShouldBeRight(string screenshot, string js, bool expected)
{
//
CultureInfo cultureInfo = new CultureInfo("zh-Hans");
//
using Mat mat = new Mat(@$"..\..\..\Assets\AutoArtifactSalvage\{screenshot}");
ArtifactStat artifact = GetArtifactStat(mat, paddle.Get(), cultureInfo, out string _);
bool result = IsMatchJavaScript(artifact, js);
//
Assert.Equal(expected, result);
}
}
}