feat(task): 配置中支持储存多个培育方案并支持来回切换
This commit is contained in:
parent
4e4b91d670
commit
41e7c8b4a8
|
@ -10,6 +10,7 @@ kotonebot-ui/.vite
|
|||
dumps*/
|
||||
config.json
|
||||
config.v*.json
|
||||
conf/
|
||||
reports/
|
||||
tmp/
|
||||
res/sprites_compiled/
|
||||
|
|
|
@ -0,0 +1,221 @@
|
|||
import os
|
||||
import json
|
||||
import uuid
|
||||
import re
|
||||
import logging
|
||||
from typing import Literal
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
|
||||
from .const import ProduceAction, RecommendCardDetectionMode
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class ConfigBaseModel(BaseModel):
|
||||
model_config = ConfigDict(use_attribute_docstrings=True)
|
||||
|
||||
|
||||
class ProduceData(ConfigBaseModel):
|
||||
enabled: bool = False
|
||||
"""是否启用培育"""
|
||||
mode: Literal['regular', 'pro', 'master'] = 'regular'
|
||||
"""
|
||||
培育模式。
|
||||
进行一次 REGULAR 培育需要 ~30min,进行一次 PRO 培育需要 ~1h(具体视设备性能而定)。
|
||||
"""
|
||||
produce_count: int = 1
|
||||
"""培育的次数。"""
|
||||
idols: list[str] = []
|
||||
"""
|
||||
要培育偶像的 IdolCardSkin.id。将会按顺序循环选择培育。
|
||||
"""
|
||||
memory_sets: list[int] = []
|
||||
"""要使用的回忆编成编号,从 1 开始。将会按顺序循环选择使用。"""
|
||||
support_card_sets: list[int] = []
|
||||
"""要使用的支援卡编成编号,从 1 开始。将会按顺序循环选择使用。"""
|
||||
auto_set_memory: bool = False
|
||||
"""是否自动编成回忆。此选项优先级高于回忆编成编号。"""
|
||||
auto_set_support_card: bool = False
|
||||
"""是否自动编成支援卡。此选项优先级高于支援卡编成编号。"""
|
||||
use_pt_boost: bool = False
|
||||
"""是否使用支援强化 Pt 提升。"""
|
||||
use_note_boost: bool = False
|
||||
"""是否使用笔记数提升。"""
|
||||
follow_producer: bool = False
|
||||
"""是否关注租借了支援卡的制作人。"""
|
||||
self_study_lesson: Literal['dance', 'visual', 'vocal'] = 'dance'
|
||||
"""自习课类型。"""
|
||||
prefer_lesson_ap: bool = False
|
||||
"""
|
||||
优先 SP 课程。
|
||||
|
||||
启用后,若出现 SP 课程,则会优先执行 SP 课程,而不是推荐课程。
|
||||
若出现多个 SP 课程,随机选择一个。
|
||||
"""
|
||||
actions_order: list[ProduceAction] = [
|
||||
ProduceAction.RECOMMENDED,
|
||||
ProduceAction.VISUAL,
|
||||
ProduceAction.VOCAL,
|
||||
ProduceAction.DANCE,
|
||||
ProduceAction.ALLOWANCE,
|
||||
ProduceAction.OUTING,
|
||||
ProduceAction.STUDY,
|
||||
ProduceAction.CONSULT,
|
||||
ProduceAction.REST,
|
||||
]
|
||||
"""
|
||||
行动优先级
|
||||
|
||||
每一周的行动将会按这里设置的优先级执行。
|
||||
"""
|
||||
recommend_card_detection_mode: RecommendCardDetectionMode = RecommendCardDetectionMode.NORMAL
|
||||
"""
|
||||
推荐卡检测模式
|
||||
|
||||
严格模式下,识别速度会降低,但识别准确率会提高。
|
||||
"""
|
||||
use_ap_drink: bool = False
|
||||
"""
|
||||
AP 不足时自动使用 AP 饮料
|
||||
"""
|
||||
skip_commu: bool = True
|
||||
"""检测并跳过交流"""
|
||||
|
||||
|
||||
class ProduceSolution(ConfigBaseModel):
|
||||
"""培育方案"""
|
||||
id: str
|
||||
"""方案唯一标识符"""
|
||||
name: str
|
||||
"""方案名称"""
|
||||
description: str | None = None
|
||||
"""方案描述"""
|
||||
data: ProduceData
|
||||
"""培育数据"""
|
||||
|
||||
|
||||
class ProduceSolutionManager:
|
||||
"""培育方案管理器"""
|
||||
|
||||
SOLUTIONS_DIR = "conf/produce"
|
||||
|
||||
def __init__(self):
|
||||
"""初始化管理器,确保目录存在"""
|
||||
os.makedirs(self.SOLUTIONS_DIR, exist_ok=True)
|
||||
|
||||
def _sanitize_filename(self, name: str) -> str:
|
||||
"""
|
||||
清理文件名中的非法字符
|
||||
|
||||
:param name: 原始名称
|
||||
:return: 清理后的文件名
|
||||
"""
|
||||
# 替换 \/:*?"<>| 为下划线
|
||||
return re.sub(r'[\\/:*?"<>|]', '_', name)
|
||||
|
||||
def _get_file_path(self, name: str) -> str:
|
||||
"""
|
||||
根据方案名称获取文件路径
|
||||
|
||||
:param name: 方案名称
|
||||
:return: 文件路径
|
||||
"""
|
||||
safe_name = self._sanitize_filename(name)
|
||||
return os.path.join(self.SOLUTIONS_DIR, f"{safe_name}.json")
|
||||
|
||||
def new(self, name: str) -> ProduceSolution:
|
||||
"""
|
||||
创建新的培育方案
|
||||
|
||||
:param name: 方案名称
|
||||
:return: 新创建的方案
|
||||
"""
|
||||
solution = ProduceSolution(
|
||||
id=uuid.uuid4().hex,
|
||||
name=name,
|
||||
data=ProduceData()
|
||||
)
|
||||
return solution
|
||||
|
||||
def list(self) -> list[ProduceSolution]:
|
||||
"""
|
||||
列出所有培育方案
|
||||
|
||||
:return: 方案列表
|
||||
"""
|
||||
solutions = []
|
||||
if not os.path.exists(self.SOLUTIONS_DIR):
|
||||
return solutions
|
||||
|
||||
for filename in os.listdir(self.SOLUTIONS_DIR):
|
||||
if filename.endswith('.json'):
|
||||
try:
|
||||
file_path = os.path.join(self.SOLUTIONS_DIR, filename)
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
solution = ProduceSolution.model_validate_json(data)
|
||||
solutions.append(solution)
|
||||
logger.info(f"Loaded produce solution from {file_path}")
|
||||
except Exception:
|
||||
logger.warning(f"Failed to load produce solution from {file_path}")
|
||||
continue
|
||||
|
||||
return solutions
|
||||
|
||||
def delete(self, id: str) -> None:
|
||||
"""
|
||||
删除指定ID的培育方案
|
||||
|
||||
:param id: 方案ID
|
||||
"""
|
||||
if not os.path.exists(self.SOLUTIONS_DIR):
|
||||
return
|
||||
|
||||
for filename in os.listdir(self.SOLUTIONS_DIR):
|
||||
if filename.endswith('.json'):
|
||||
try:
|
||||
file_path = os.path.join(self.SOLUTIONS_DIR, filename)
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
if data.get('id') == id:
|
||||
os.remove(file_path)
|
||||
return
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
def save(self, id: str, solution: ProduceSolution) -> None:
|
||||
"""
|
||||
保存培育方案
|
||||
|
||||
:param id: 方案ID
|
||||
:param solution: 方案对象
|
||||
"""
|
||||
# 确保ID一致
|
||||
solution.id = id
|
||||
|
||||
file_path = self._get_file_path(solution.name)
|
||||
with open(file_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(solution.model_dump_json(indent=4), f, ensure_ascii=False)
|
||||
|
||||
def read(self, id: str) -> ProduceSolution:
|
||||
"""
|
||||
读取指定ID的培育方案
|
||||
|
||||
:param id: 方案ID
|
||||
:return: 方案对象
|
||||
:raises FileNotFoundError: 当方案不存在时
|
||||
"""
|
||||
if not os.path.exists(self.SOLUTIONS_DIR):
|
||||
raise FileNotFoundError(f"Solution with id '{id}' not found")
|
||||
|
||||
for filename in os.listdir(self.SOLUTIONS_DIR):
|
||||
if filename.endswith('.json'):
|
||||
try:
|
||||
file_path = os.path.join(self.SOLUTIONS_DIR, filename)
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
if data.get('id') == id:
|
||||
return ProduceSolution.model_validate_json(data)
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
raise FileNotFoundError(f"Solution with id '{id}' not found")
|
|
@ -2,13 +2,12 @@ from typing import TypeVar, Literal, Sequence
|
|||
from pydantic import BaseModel, ConfigDict
|
||||
|
||||
from kotonebot import config
|
||||
from kotonebot.kaa.config.produce import ProduceSolution, ProduceSolutionManager
|
||||
from .const import (
|
||||
ConfigEnum,
|
||||
Priority,
|
||||
APShopItems,
|
||||
DailyMoneyShopItems,
|
||||
ProduceAction,
|
||||
RecommendCardDetectionMode,
|
||||
)
|
||||
|
||||
T = TypeVar('T')
|
||||
|
@ -70,68 +69,8 @@ class ContestConfig(ConfigBaseModel):
|
|||
class ProduceConfig(ConfigBaseModel):
|
||||
enabled: bool = False
|
||||
"""是否启用培育"""
|
||||
mode: Literal['regular', 'pro', 'master'] = 'regular'
|
||||
"""
|
||||
培育模式。
|
||||
进行一次 REGULAR 培育需要 ~30min,进行一次 PRO 培育需要 ~1h(具体视设备性能而定)。
|
||||
"""
|
||||
produce_count: int = 1
|
||||
"""培育的次数。"""
|
||||
idols: list[str] = []
|
||||
"""
|
||||
要培育偶像的 IdolCardSkin.id。将会按顺序循环选择培育。
|
||||
"""
|
||||
memory_sets: list[int] = []
|
||||
"""要使用的回忆编成编号,从 1 开始。将会按顺序循环选择使用。"""
|
||||
support_card_sets: list[int] = []
|
||||
"""要使用的支援卡编成编号,从 1 开始。将会按顺序循环选择使用。"""
|
||||
auto_set_memory: bool = False
|
||||
"""是否自动编成回忆。此选项优先级高于回忆编成编号。"""
|
||||
auto_set_support_card: bool = False
|
||||
"""是否自动编成支援卡。此选项优先级高于支援卡编成编号。"""
|
||||
use_pt_boost: bool = False
|
||||
"""是否使用支援强化 Pt 提升。"""
|
||||
use_note_boost: bool = False
|
||||
"""是否使用笔记数提升。"""
|
||||
follow_producer: bool = False
|
||||
"""是否关注租借了支援卡的制作人。"""
|
||||
self_study_lesson: Literal['dance', 'visual', 'vocal'] = 'dance'
|
||||
"""自习课类型。"""
|
||||
prefer_lesson_ap: bool = False
|
||||
"""
|
||||
优先 SP 课程。
|
||||
|
||||
启用后,若出现 SP 课程,则会优先执行 SP 课程,而不是推荐课程。
|
||||
若出现多个 SP 课程,随机选择一个。
|
||||
"""
|
||||
actions_order: list[ProduceAction] = [
|
||||
ProduceAction.RECOMMENDED,
|
||||
ProduceAction.VISUAL,
|
||||
ProduceAction.VOCAL,
|
||||
ProduceAction.DANCE,
|
||||
ProduceAction.ALLOWANCE,
|
||||
ProduceAction.OUTING,
|
||||
ProduceAction.STUDY,
|
||||
ProduceAction.CONSULT,
|
||||
ProduceAction.REST,
|
||||
]
|
||||
"""
|
||||
行动优先级
|
||||
|
||||
每一周的行动将会按这里设置的优先级执行。
|
||||
"""
|
||||
recommend_card_detection_mode: RecommendCardDetectionMode = RecommendCardDetectionMode.NORMAL
|
||||
"""
|
||||
推荐卡检测模式
|
||||
|
||||
严格模式下,识别速度会降低,但识别准确率会提高。
|
||||
"""
|
||||
use_ap_drink: bool = False
|
||||
"""
|
||||
AP 不足时自动使用 AP 饮料
|
||||
"""
|
||||
skip_commu: bool = True
|
||||
"""检测并跳过交流"""
|
||||
selected_solution_id: str | None = None
|
||||
"""选中的培育方案ID"""
|
||||
|
||||
class MissionRewardConfig(ConfigBaseModel):
|
||||
enabled: bool = False
|
||||
|
@ -284,4 +223,12 @@ class BaseConfig(ConfigBaseModel):
|
|||
def conf() -> BaseConfig:
|
||||
"""获取当前配置数据"""
|
||||
c = config.to(BaseConfig).current
|
||||
return c.options
|
||||
return c.options
|
||||
|
||||
def produce_solution() -> ProduceSolution:
|
||||
"""获取当前培育方案"""
|
||||
id = conf().produce.selected_solution_id
|
||||
if id is None:
|
||||
raise ValueError("No produce solution selected")
|
||||
# TODO: 这里需要缓存,不能每次都从磁盘读取
|
||||
return ProduceSolutionManager().read(id)
|
||||
|
|
|
@ -9,6 +9,7 @@ from kotonebot import (
|
|||
sleep,
|
||||
Interval,
|
||||
)
|
||||
from kotonebot.kaa.config.schema import produce_solution
|
||||
from kotonebot.primitives import Rect
|
||||
from kotonebot.kaa.tasks import R
|
||||
from .p_drink import acquire_p_drink
|
||||
|
@ -188,7 +189,7 @@ def fast_acquisitions() -> AcquisitionType | None:
|
|||
|
||||
# 跳过未读交流
|
||||
logger.debug("Check skip commu...")
|
||||
if conf().produce.skip_commu and handle_unread_commu(img):
|
||||
if produce_solution().data.skip_commu and handle_unread_commu(img):
|
||||
return "SkipCommu"
|
||||
device.click(10, 10)
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@ import logging
|
|||
from typing_extensions import assert_never
|
||||
from typing import Literal
|
||||
|
||||
from kotonebot.kaa.config.schema import produce_solution
|
||||
from kotonebot.kaa.game_ui.schedule import Schedule
|
||||
from kotonebot.kaa.tasks import R
|
||||
from ..actions import loading
|
||||
|
@ -193,7 +194,7 @@ def practice():
|
|||
|
||||
def threshold_predicate(card_count: int, result: CardDetectResult):
|
||||
border_scores = (result.left_score, result.right_score, result.top_score, result.bottom_score)
|
||||
is_strict_mode = conf().produce.recommend_card_detection_mode == RecommendCardDetectionMode.STRICT
|
||||
is_strict_mode = produce_solution().data.recommend_card_detection_mode == RecommendCardDetectionMode.STRICT
|
||||
if is_strict_mode:
|
||||
return (
|
||||
result.score >= 0.05
|
||||
|
@ -225,7 +226,7 @@ def exam(type: Literal['mid', 'final']):
|
|||
logger.info("Exam started")
|
||||
|
||||
def threshold_predicate(card_count: int, result: CardDetectResult):
|
||||
is_strict_mode = conf().produce.recommend_card_detection_mode == RecommendCardDetectionMode.STRICT
|
||||
is_strict_mode = produce_solution().data.recommend_card_detection_mode == RecommendCardDetectionMode.STRICT
|
||||
total = lambda t: result.score >= t
|
||||
def borders(t):
|
||||
# 卡片数量小于三时无遮挡,以及最后一张卡片也总是无遮挡
|
||||
|
@ -423,7 +424,7 @@ def produce_end():
|
|||
# [screenshots/produce_end/end_follow.png]
|
||||
elif image.find(R.InPurodyuusu.ButtonCancel):
|
||||
logger.info("Follow producer dialog found. Click to close.")
|
||||
if conf().produce.follow_producer:
|
||||
if produce_solution().data.follow_producer:
|
||||
logger.info("Follow producer")
|
||||
device.click(image.expect_wait(R.InPurodyuusu.ButtonFollowNoIcon))
|
||||
else:
|
||||
|
@ -507,12 +508,12 @@ def week_normal(week_first: bool = False):
|
|||
action: ProduceAction | None = None
|
||||
# SP 课程
|
||||
if (
|
||||
conf().produce.prefer_lesson_ap
|
||||
produce_solution().data.prefer_lesson_ap
|
||||
and handle_sp_lesson()
|
||||
):
|
||||
action = ProduceAction.DANCE
|
||||
else:
|
||||
actions = conf().produce.actions_order
|
||||
actions = produce_solution().data.actions_order
|
||||
for action in actions:
|
||||
logger.debug("Checking action: %s", action)
|
||||
if action := handle_action(action):
|
||||
|
@ -540,7 +541,7 @@ def week_normal(week_first: bool = False):
|
|||
def week_final_lesson():
|
||||
until_action_scene()
|
||||
action: ProduceAction | None = None
|
||||
actions = conf().produce.actions_order
|
||||
actions = produce_solution().data.actions_order
|
||||
for action in actions:
|
||||
logger.debug("Checking action: %s", action)
|
||||
if action := handle_action(action, True):
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
"""
|
||||
from logging import getLogger
|
||||
|
||||
from kotonebot.kaa.config.schema import produce_solution
|
||||
from kotonebot.kaa.game_ui import dialog
|
||||
|
||||
from kotonebot.kaa.tasks import R
|
||||
|
@ -66,7 +67,7 @@ def enter_study():
|
|||
R.InPurodyuusu.TextSelfStudyVocal
|
||||
]):
|
||||
logger.info("授業 type: Self study.")
|
||||
target = conf().produce.self_study_lesson
|
||||
target = produce_solution().data.self_study_lesson
|
||||
if target == 'dance':
|
||||
logger.debug("Clicking on lesson dance.")
|
||||
device.double_click(image.expect(R.InPurodyuusu.TextSelfStudyDance))
|
||||
|
|
|
@ -3,6 +3,7 @@ from itertools import cycle
|
|||
from typing import Optional, Literal
|
||||
from typing_extensions import assert_never
|
||||
|
||||
from kotonebot.kaa.config.schema import produce_solution
|
||||
from kotonebot.ui import user
|
||||
from kotonebot.kaa.tasks import R
|
||||
from kotonebot.kaa.config import conf
|
||||
|
@ -242,7 +243,7 @@ def do_produce(
|
|||
result = False
|
||||
break
|
||||
if not result:
|
||||
if conf().produce.use_ap_drink:
|
||||
if produce_solution().data.use_ap_drink:
|
||||
# [kotonebot-resource\sprites\jp\produce\screenshot_no_enough_ap_1.png]
|
||||
# [kotonebot-resource\sprites\jp\produce\screenshot_no_enough_ap_2.png]
|
||||
# [kotonebot-resource\sprites\jp\produce\screenshot_no_enough_ap_3.png]
|
||||
|
@ -351,11 +352,11 @@ def do_produce(
|
|||
|
||||
# 4. 选择道具 [screenshots/produce/screenshot_produce_start_4_end.png]
|
||||
# TODO: 如果道具不足,这里加入推送提醒
|
||||
if conf().produce.use_note_boost:
|
||||
if produce_solution().data.use_note_boost:
|
||||
if image.find(R.Produce.CheckboxIconNoteBoost):
|
||||
device.click()
|
||||
sleep(0.1)
|
||||
if conf().produce.use_pt_boost:
|
||||
if produce_solution().data.use_pt_boost:
|
||||
if image.find(R.Produce.CheckboxIconSupportPtBoost):
|
||||
device.click()
|
||||
sleep(0.1)
|
||||
|
@ -384,14 +385,14 @@ def produce():
|
|||
"""
|
||||
培育任务
|
||||
"""
|
||||
if not conf().produce.enabled:
|
||||
if not produce_solution().data.enabled:
|
||||
logger.info('Produce is disabled.')
|
||||
return
|
||||
import time
|
||||
count = conf().produce.produce_count
|
||||
idols = conf().produce.idols
|
||||
memory_sets = conf().produce.memory_sets
|
||||
mode = conf().produce.mode
|
||||
count = produce_solution().data.produce_count
|
||||
idols = produce_solution().data.idols
|
||||
memory_sets = produce_solution().data.memory_sets
|
||||
mode = produce_solution().data.mode
|
||||
# 数据验证
|
||||
if count < 0:
|
||||
user.warning('配置有误', '培育次数不能小于 0。将跳过本次培育。')
|
||||
|
@ -402,7 +403,7 @@ def produce():
|
|||
for i in range(count):
|
||||
start_time = time.time()
|
||||
idol = next(idol_iterator)
|
||||
if conf().produce.auto_set_memory:
|
||||
if produce_solution().data.auto_set_memory:
|
||||
memory_set = None
|
||||
else:
|
||||
memory_set = next(memory_set_iterator, None)
|
||||
|
@ -426,12 +427,12 @@ if __name__ == '__main__':
|
|||
from kotonebot.kaa.common import BaseConfig
|
||||
from kotonebot.kaa.main import Kaa
|
||||
|
||||
conf().produce.enabled = True
|
||||
conf().produce.mode = 'pro'
|
||||
conf().produce.produce_count = 1
|
||||
# conf().produce.idols = ['i_card-skin-hski-3-002']
|
||||
conf().produce.memory_sets = [1]
|
||||
conf().produce.auto_set_memory = False
|
||||
produce_solution().data.enabled = True
|
||||
produce_solution().data.mode = 'pro'
|
||||
produce_solution().data.produce_count = 1
|
||||
# produce_solution().data.idols = ['i_card-skin-hski-3-002']
|
||||
produce_solution().data.memory_sets = [1]
|
||||
produce_solution().data.auto_set_memory = False
|
||||
# do_produce(PIdol.月村手毬_初声, 'pro', 5)
|
||||
produce()
|
||||
# a()
|
||||
|
|
Loading…
Reference in New Issue