feat(*): 日常新增支持指定购买商品 & 部分优化

1. 日常新增支持指定购买商品
2. 新增 DispatcherContext.expand,允许在一个 dispatcher 函数内复用其他 dispatcher 函数
3. 修复 make_resources.py 生成结果中部分变量命名格式不正确的问题
This commit is contained in:
XcantloadX 2025-02-06 18:54:57 +08:00
parent 58a8a8da72
commit 4154c5541e
11 changed files with 341 additions and 31 deletions

View File

@ -115,7 +115,7 @@ def action(
pass_through: bool = False,
priority: int = 0,
screenshot_mode: ScreenshotMode | None = None,
dispatcher: Literal[True] = True,
dispatcher: Literal[True, 'fragment'] = True,
) -> Callable[[Callable[Concatenate[DispatcherContext, P], R]], Callable[P, R]]:
"""
`action` 装饰器用于标记一个函数为动作函数
@ -135,6 +135,9 @@ def action(
"""
...
# TODO: 需要找个地方统一管理这些属性名
ATTR_ORIGINAL_FUNC = '_kb_inner'
ATTR_ACTION_MARK = '__kb_action_mark'
def action(*args, **kwargs):
def _register(func: Callable, name: str, description: str|None = None, priority: int = 0) -> Action:
description = description or func.__doc__ or ''
@ -143,7 +146,6 @@ def action(*args, **kwargs):
logger.debug(f'Action "{name}" registered.')
return action
if len(args) == 1 and isinstance(args[0], Callable):
func = args[0]
action = _register(_placeholder, func.__name__, func.__doc__)
@ -154,6 +156,8 @@ def action(*args, **kwargs):
ContextStackVars.pop()
current_callstack.pop()
return ret
setattr(_wrapper, ATTR_ORIGINAL_FUNC, func)
setattr(_wrapper, ATTR_ACTION_MARK, True)
action.func = _wrapper
return _wrapper
else:
@ -163,7 +167,7 @@ def action(*args, **kwargs):
priority = kwargs.get('priority', 0)
screenshot_mode = kwargs.get('screenshot_mode', None)
dispatcher = kwargs.get('dispatcher', False)
if dispatcher:
if dispatcher == True or dispatcher == 'fragment':
if not (screenshot_mode is None or screenshot_mode == 'manual'):
raise ValueError('`screenshot_mode` must be None or "manual" when `dispatcher=True`.')
screenshot_mode = 'manual'
@ -175,7 +179,7 @@ def action(*args, **kwargs):
return func
else:
if dispatcher:
func = dispatcher_decorator(func) # type: ignore
func = dispatcher_decorator(func, fragment=(dispatcher == 'fragment')) # type: ignore
def _wrapper(*args: P.args, **kwargs: P.kwargs):
current_callstack.append(action)
vars = ContextStackVars.push(screenshot_mode=screenshot_mode)
@ -183,6 +187,8 @@ def action(*args, **kwargs):
ContextStackVars.pop()
current_callstack.pop()
return ret
setattr(_wrapper, ATTR_ORIGINAL_FUNC, func)
setattr(_wrapper, ATTR_ACTION_MARK, True)
action.func = _wrapper
return _wrapper
return _action_decorator

View File

@ -3,7 +3,7 @@ import logging
import inspect
from logging import Logger
from types import CodeType
from typing import Any, Callable, Concatenate, TypeVar, ParamSpec, Literal, Protocol
from typing import Annotated, Any, Callable, Concatenate, TypeVar, ParamSpec, Literal, Protocol, cast
from typing_extensions import Self
from dataclasses import dataclass
@ -15,22 +15,75 @@ P = ParamSpec('P')
R = TypeVar('R')
ThenAction = Literal['click', 'log']
DoAction = Literal['click']
# TODO: 需要找个地方统一管理这些属性名
ATTR_DISPATCHER_MARK = '__kb_dispatcher_mark'
ATTR_ORIGINAL_FUNC = '_kb_inner'
class DispatchFunc: pass
wrapper_to_func: dict[Callable, Callable] = {}
class DispatcherContext:
def __init__(self):
self.finished: bool = False
self._first_run: bool = True
def finish(self):
"""标记已完成 dispatcher 循环。循环将在下次条件检测时退出。"""
self.finished = True
def dispatcher(func: Callable[Concatenate[DispatcherContext, P], R]) -> Callable[P, R]:
"""
def expand(self, func: Annotated[Callable[[], Any], DispatchFunc], ignore_finish: bool = True):
"""
调用其他 dispatcher 函数
使用 `expand` 和直接调用的区别是
* 直接调用会执行 while 循环直到满足结束条件
* 而使用 `expand` 则只会执行一次效果类似于将目标函数里的代码直接复制粘贴过来
"""
# 获取原始函数
original_func = func
while not getattr(original_func, ATTR_DISPATCHER_MARK, False):
original_func = getattr(original_func, ATTR_ORIGINAL_FUNC)
original_func = getattr(original_func, ATTR_ORIGINAL_FUNC)
if not original_func:
raise ValueError(f'{repr(func)} is not a dispatcher function.')
elif not callable(original_func):
raise ValueError(f'{repr(original_func)} is not callable.')
original_func = cast(Callable[[DispatcherContext], Any], original_func)
old_finished = self.finished
ret = original_func(self)
if ignore_finish:
self.finished = old_finished
return ret
@property
def beginning(self) -> bool:
"""是否为第一次运行"""
return self._first_run
@property
def finishing(self) -> bool:
"""是否即将结束运行"""
return self.finished
def dispatcher(
func: Callable[Concatenate[DispatcherContext, P], R],
*,
fragment: bool = False
) -> Annotated[Callable[P, R], DispatchFunc]:
"""
注意\n
此装饰器必须在应用 @action/@task 装饰器后再应用 `screenshot_mode='manual'` 参数必须设置
或者也可以使用 @action/@task 装饰器中的 `dispatcher=True` 参数
那么就没有上面两个要求了
:param fragment:
片段模式默认不启用
启用后被装饰函数将会只执行依次
而不会一直循环到 ctx.finish() 被调用
"""
ctx = DispatcherContext()
def wrapper(*args: P.args, **kwargs: P.kwargs):
@ -38,5 +91,19 @@ def dispatcher(func: Callable[Concatenate[DispatcherContext, P], R]) -> Callable
from kotonebot import device
device.update_screenshot()
ret = func(ctx, *args, **kwargs)
ctx._first_run = False
return ret
return wrapper
def fragment_wrapper(*args: P.args, **kwargs: P.kwargs):
from kotonebot import device
device.update_screenshot()
return func(ctx, *args, **kwargs)
setattr(wrapper, ATTR_ORIGINAL_FUNC, func)
setattr(fragment_wrapper, ATTR_ORIGINAL_FUNC, func)
setattr(wrapper, ATTR_DISPATCHER_MARK, True)
setattr(fragment_wrapper, ATTR_DISPATCHER_MARK, True)
wrapper_to_func[wrapper] = func
if fragment:
return fragment_wrapper
else:
return wrapper

View File

@ -1,4 +1,4 @@
from typing import Literal
from typing import Literal, Dict
from enum import IntEnum, Enum
from pydantic import BaseModel
@ -102,13 +102,136 @@ class PIdol(Enum):
藤田ことね_冠菊 = ["藤田ことね", "冠菊"]
藤田ことね_初声 = ["藤田ことね", "初声"]
藤田ことね_学園生活 = ["藤田ことね", "学園生活"]
class DailyMoneyShopItems(IntEnum):
"""日常商店物品"""
Recommendations = -1
"""所有推荐商品"""
LessonNote = 0
"""レッスンノート"""
VeteranNote = 1
"""ベテランノート"""
SupportEnhancementPt = 2
"""サポート強化Pt 支援强化Pt"""
SenseNoteVocal = 3
"""センスノート(ボーカル)感性笔记(声乐)"""
SenseNoteDance = 4
"""センスノート(ダンス)感性笔记(舞蹈)"""
SenseNoteVisual = 5
"""センスノート(ビジュアル)感性笔记(形象)"""
LogicNoteVocal = 6
"""ロジックノート(ボーカル)理性笔记(声乐)"""
LogicNoteDance = 7
"""ロジックノート(ダンス)理性笔记(舞蹈)"""
LogicNoteVisual = 8
"""ロジックノート(ビジュアル)理性笔记(形象)"""
AnomalyNoteVocal = 9
"""アノマリーノート(ボーカル)非凡笔记(声乐)"""
AnomalyNoteDance = 10
"""アノマリーノート(ダンス)非凡笔记(舞蹈)"""
AnomalyNoteVisual = 11
"""アノマリーノート(ビジュアル)非凡笔记(形象)"""
RechallengeTicket = 12
"""再挑戦チケット 重新挑战券"""
RecordKey = 13
# 碎片
"""記録の鍵 解锁交流的物品"""
IdolPiece_倉本千奈_WonderScale = 14
"""倉本千奈 WonderScale 碎片"""
IdolPiece_篠泽广_光景 = 15
"""篠泽广 光景 碎片"""
IdolPiece_紫云清夏_TameLieOneStep = 16
"""紫云清夏 Tame-Lie-One-Step 碎片"""
@classmethod
def to_ui_text(cls, item: "DailyMoneyShopItems") -> str:
"""获取枚举值对应的UI显示文本"""
MAP = {
cls.Recommendations: "所有推荐商品",
cls.LessonNote: "课程笔记",
cls.VeteranNote: "老手笔记",
cls.SupportEnhancementPt: "支援强化点数",
cls.SenseNoteVocal: "感性笔记(声乐)",
cls.SenseNoteDance: "感性笔记(舞蹈)",
cls.SenseNoteVisual: "感性笔记(形象)",
cls.LogicNoteVocal: "理性笔记(声乐)",
cls.LogicNoteDance: "理性笔记(舞蹈)",
cls.LogicNoteVisual: "理性笔记(形象)",
cls.AnomalyNoteVocal: "非凡笔记(声乐)",
cls.AnomalyNoteDance: "非凡笔记(舞蹈)",
cls.AnomalyNoteVisual: "非凡笔记(形象)",
cls.RechallengeTicket: "重新挑战券",
cls.RecordKey: "记录钥匙",
cls.IdolPiece_倉本千奈_WonderScale: "倉本千奈 WonderScale 碎片",
cls.IdolPiece_篠泽广_光景: "篠泽广 光景 碎片",
cls.IdolPiece_紫云清夏_TameLieOneStep: "紫云清夏 Tame-Lie-One-Step 碎片"
}
return MAP.get(item, str(item))
@classmethod
def all(cls) -> list[tuple[str, 'DailyMoneyShopItems']]:
"""获取所有枚举值及其对应的UI显示文本"""
return [(cls.to_ui_text(item), item) for item in cls]
def to_resource(self):
from . import R
match self:
case DailyMoneyShopItems.Recommendations:
return R.Daily.TextShopRecommended
case DailyMoneyShopItems.LessonNote:
return R.Shop.ItemLessonNote
case DailyMoneyShopItems.VeteranNote:
return R.Shop.ItemVeteranNote
case DailyMoneyShopItems.SupportEnhancementPt:
return R.Shop.ItemSupportEnhancementPt
case DailyMoneyShopItems.SenseNoteVocal:
return R.Shop.ItemSenseNoteVocal
case DailyMoneyShopItems.SenseNoteDance:
return R.Shop.ItemSenseNoteDance
case DailyMoneyShopItems.SenseNoteVisual:
return R.Shop.ItemSenseNoteVisual
case DailyMoneyShopItems.LogicNoteVocal:
return R.Shop.ItemLogicNoteVocal
case DailyMoneyShopItems.LogicNoteDance:
return R.Shop.ItemLogicNoteDance
case DailyMoneyShopItems.LogicNoteVisual:
return R.Shop.ItemLogicNoteVisual
case DailyMoneyShopItems.AnomalyNoteVocal:
return R.Shop.ItemAnomalyNoteVocal
case DailyMoneyShopItems.AnomalyNoteDance:
return R.Shop.ItemAnomalyNoteDance
case DailyMoneyShopItems.AnomalyNoteVisual:
return R.Shop.ItemAnomalyNoteVisual
case DailyMoneyShopItems.RechallengeTicket:
return R.Shop.ItemRechallengeTicket
case DailyMoneyShopItems.RecordKey:
return R.Shop.ItemRecordKey
case DailyMoneyShopItems.IdolPiece_倉本千奈_WonderScale:
return R.Shop.IdolPiece.倉本千奈_WonderScale
case DailyMoneyShopItems.IdolPiece_篠泽广_光景:
return R.Shop.IdolPiece.篠泽广_光景
case DailyMoneyShopItems.IdolPiece_紫云清夏_TameLieOneStep:
return R.Shop.IdolPiece.紫云清夏_TameLieOneStep
case _:
raise ValueError(f"Unknown daily shop item: {self}")
class PurchaseConfig(BaseModel):
enabled: bool = False
"""是否启用商店购买"""
money_enabled: bool = False
"""是否启用金币购买"""
money_items: list[DailyMoneyShopItems] = []
"""金币商店要购买的物品"""
money_refresh_on: Literal['never', 'not_found', 'always'] = 'never'
"""
金币商店刷新逻辑
* never: 从不刷新
* not_found: 仅当要购买的物品不存在时刷新
* always: 总是刷新
"""
ap_enabled: bool = False
"""是否启用AP购买"""
ap_items: list[Literal[0, 1, 2, 3]] = []

View File

@ -1,4 +1,5 @@
import logging
from typing import Optional
from . import R
from .common import conf, PIdol
@ -95,6 +96,9 @@ def select_idol(target_titles: list[str] | PIdol):
def do_produce(idol: PIdol | None = None):
"""
进行培育流程
前置条件可导航至首页的任意页面\n
结束状态游戏首页\n
:param idol: 要培育的偶像如果为 None则使用配置文件中的偶像
"""
@ -162,14 +166,23 @@ def do_produce(idol: PIdol | None = None):
wait_loading_end()
hajime_regular()
@task('培育')
def produce_task():
def produce_task(count: Optional[int] = None):
"""
培育任务
:param count:
培育次数若为 None则从配置文件中读入
"""
import time
start_time = time.time()
do_produce()
end_time = time.time()
logger.info(f"Produce time used: {format_time(end_time - start_time)}")
if count is None:
count = conf().produce.produce_count
for _ in range(count):
start_time = time.time()
do_produce()
end_time = time.time()
logger.info(f"Produce time used: {format_time(end_time - start_time)}")
if __name__ == '__main__':
import logging

View File

@ -1,14 +1,104 @@
"""从商店购买物品"""
import logging
from typing import Optional
from typing_extensions import deprecated
from . import R
from .common import conf
from .common import conf, DailyMoneyShopItems
from kotonebot.backend.util import cropped
from kotonebot import task, device, image, ocr, action, sleep
from kotonebot.backend.dispatch import DispatcherContext, dispatcher
from .actions.scenes import goto_home, goto_shop, at_daily_shop
logger = logging.getLogger(__name__)
@action('购买 Money 物品', screenshot_mode='manual-inherit')
def money_items2(items: Optional[list[DailyMoneyShopItems]] = None):
"""
购买 Money 物品
前置条件商店页面的 マニー Tab\n
结束状态-
:param items: 要购买的物品列表默认为 None None 时使用配置文件里的设置
"""
# 前置条件:[screenshots\shop\money1.png]
logger.info(f'Purchasing マニー items.')
if items is None:
items = conf().purchase.money_items
device.update_screenshot()
if DailyMoneyShopItems.Recommendations in items:
dispatch_recommended_items()
items.remove(DailyMoneyShopItems.Recommendations)
finished = []
max_scroll = 3
scroll = 0
while items:
for item in items:
if image.find(item.to_resource()):
logger.info(f'Purchasing {item.to_ui_text(item)}...')
device.click()
dispatch_purchase_dialog()
finished.append(item)
items = [item for item in items if item not in finished]
# 全都买完了
if not items:
break
# 还有,翻页后继续
else:
device.swipe_scaled(x1=0.5, x2=0.5, y1=0.8, y2=0.5)
sleep(0.5)
device.update_screenshot()
scroll += 1
if scroll >= max_scroll:
break
logger.info(f'Purchasing money items completed. {len(finished)} item(s) purchased.')
if items:
logger.info(f'{len(items)} item(s) not purchased: {", ".join([item.to_ui_text(item) for item in items])}')
@action('购买推荐商品', dispatcher=True)
def dispatch_recommended_items(ctx: DispatcherContext):
"""
购买推荐商品
前置条件商店页面的 マニー Tab\n
结束状态-
"""
# 前置条件:[screenshots\shop\money1.png]
if ctx.beginning:
logger.info(f'Start purchasing recommended items.')
if image.find(R.Daily.TextShopRecommended):
logger.info(f'Clicking on recommended item.') # TODO: 计数
device.click()
elif ctx.expand(dispatch_purchase_dialog):
pass
elif image.find(R.Daily.IconTitleDailyShop) and not image.find(R.Daily.TextShopRecommended):
logger.info(f'No recommended item found. Finished.')
ctx.finish()
@action('确认购买', dispatcher=True)
def dispatch_purchase_dialog(ctx: DispatcherContext):
"""
确认购买
前置条件购买确认对话框\n
结束状态对话框关闭后原来的界面
"""
# 前置条件:[screenshots\shop\dialog.png]
if image.find(R.Daily.ButtonShopCountAdd, colored=True):
logger.debug('Adjusting quantity(+1)...')
device.click()
elif image.find(R.Common.ButtonConfirm):
logger.debug('Confirming purchase...')
# device.click()
device.click(image.expect(R.InPurodyuusu.ButtonCancel))
ctx.finish()
@deprecated('改用 `money_items2`')
@action('购买 Money 物品')
def money_items():
"""
@ -99,7 +189,7 @@ def purchase():
# 购买マニー物品
if conf().purchase.money_enabled:
image.expect_wait(R.Daily.IconShopMoney)
money_items()
money_items2()
sleep(0.5)
else:
logger.info('Money purchase is disabled.')
@ -121,6 +211,4 @@ if __name__ == '__main__':
import logging
logging.basicConfig(level=logging.INFO, format='[%(asctime)s] [%(levelname)s] [%(name)s] [%(funcName)s] [%(lineno)d] %(message)s')
logger.setLevel(logging.DEBUG)
# money_items()
# ap_items([0, 1, 3])
purchase()
dispatch_recommended_items()

View File

@ -10,7 +10,7 @@ from kotonebot.config.manager import load_config, save_config
from kotonebot.tasks.common import (
BaseConfig, APShopItems, PurchaseConfig, ActivityFundsConfig,
PresentsConfig, AssignmentConfig, ContestConfig, ProduceConfig,
MissionRewardConfig, PIdol
MissionRewardConfig, PIdol, DailyMoneyShopItems
)
from kotonebot.config.base_config import UserConfig, BackendConfig
@ -104,6 +104,7 @@ class KotoneBotUI:
money_enabled: bool,
ap_enabled: bool,
ap_items: List[str],
money_items: List[DailyMoneyShopItems],
activity_funds_enabled: bool,
presents_enabled: bool,
assignment_enabled: bool,
@ -125,9 +126,9 @@ class KotoneBotUI:
) -> str:
ap_items_enum: List[Literal[0, 1, 2, 3]] = []
ap_items_map: Dict[str, APShopItems] = {
"获取支援强化 Pt 提升": APShopItems.PRODUCE_PT_UP,
"获取笔记数提升": APShopItems.PRODUCE_NOTE_UP,
"挑战券": APShopItems.RECHALLENGE,
"支援强化点数提升": APShopItems.PRODUCE_PT_UP,
"笔记数提升": APShopItems.PRODUCE_NOTE_UP,
"重新挑战券": APShopItems.RECHALLENGE,
"回忆再生成券": APShopItems.REGENERATE_MEMORY
}
for item in ap_items:
@ -141,6 +142,7 @@ class KotoneBotUI:
purchase=PurchaseConfig(
enabled=purchase_enabled,
money_enabled=money_enabled,
money_items=money_items,
ap_enabled=ap_enabled,
ap_items=ap_items_enum
),
@ -217,7 +219,7 @@ class KotoneBotUI:
outputs=[task_status]
)
def _create_purchase_settings(self) -> Tuple[gr.Checkbox, gr.Checkbox, gr.Checkbox, gr.Dropdown]:
def _create_purchase_settings(self) -> Tuple[gr.Checkbox, gr.Checkbox, gr.Checkbox, gr.Dropdown, gr.Dropdown]:
with gr.Column():
gr.Markdown("### 商店购买设置")
purchase_enabled = gr.Checkbox(
@ -229,6 +231,15 @@ class KotoneBotUI:
label="启用金币购买",
value=self.current_config.options.purchase.money_enabled
)
# 添加金币商店商品选择
money_items = gr.Dropdown(
multiselect=True,
choices=list(DailyMoneyShopItems.all()),
value=self.current_config.options.purchase.money_items,
label="金币商店购买物品"
)
ap_enabled = gr.Checkbox(
label="启用AP购买",
value=self.current_config.options.purchase.ap_enabled
@ -237,9 +248,9 @@ class KotoneBotUI:
# 转换枚举值为显示文本
selected_items: List[str] = []
ap_items_map = {
APShopItems.PRODUCE_PT_UP: "获取支援强化 Pt 提升",
APShopItems.PRODUCE_NOTE_UP: "获取笔记数提升",
APShopItems.RECHALLENGE: "挑战券",
APShopItems.PRODUCE_PT_UP: "支援强化点数提升",
APShopItems.PRODUCE_NOTE_UP: "笔记数提升",
APShopItems.RECHALLENGE: "重新挑战券",
APShopItems.REGENERATE_MEMORY: "回忆再生成券"
}
for item_value in self.current_config.options.purchase.ap_items:
@ -259,7 +270,7 @@ class KotoneBotUI:
inputs=[purchase_enabled],
outputs=[purchase_group]
)
return purchase_enabled, money_enabled, ap_enabled, ap_items
return purchase_enabled, money_enabled, ap_enabled, ap_items, money_items
def _create_work_settings(self) -> Tuple[gr.Checkbox, gr.Checkbox, gr.Dropdown, gr.Checkbox, gr.Dropdown]:
with gr.Column():

Binary file not shown.

After

Width:  |  Height:  |  Size: 861 KiB

View File

@ -0,0 +1 @@
{"definitions":{"0949c622-9067-4f0d-bac2-3f938a1d2ed2":{"name":"Shop.ItemLessonNote","displayName":"レッスンノート","type":"template","annotationId":"0949c622-9067-4f0d-bac2-3f938a1d2ed2","useHintRect":false},"b2af59e9-60e3-4d97-8c72-c7ba092113a3":{"name":"Shop.ItemVeteranNote","displayName":"ベテランノート","type":"template","annotationId":"b2af59e9-60e3-4d97-8c72-c7ba092113a3","useHintRect":false},"835489e2-b29b-426c-b4c9-3bb9f8eb6195":{"name":"Shop.ItemSupportEnhancementPt","displayName":"サポート強化Pt 支援强化Pt","type":"template","annotationId":"835489e2-b29b-426c-b4c9-3bb9f8eb6195","useHintRect":false},"c5b7d67e-7260-4f08-a0e9-4d31ce9bbecf":{"name":"Shop.ItemSenseNoteVocal","displayName":"センスノート(ボーカル)感性笔记(声乐)","type":"template","annotationId":"c5b7d67e-7260-4f08-a0e9-4d31ce9bbecf","useHintRect":false},"0f7d581d-cea3-4039-9205-732e4cd29293":{"name":"Shop.ItemSenseNoteDance","displayName":"センスノート(ダンス)感性笔记(舞蹈)","type":"template","annotationId":"0f7d581d-cea3-4039-9205-732e4cd29293","useHintRect":false},"d3cc3323-51af-4882-ae12-49e7384b746d":{"name":"Shop.ItemSenseNoteVisual","displayName":"センスノート(ビジュアル)感性笔记(形象)","type":"template","annotationId":"d3cc3323-51af-4882-ae12-49e7384b746d","useHintRect":false},"a1df3af1-a3e7-4521-a085-e4dc3cd1cc57":{"name":"Shop.ItemLogicNoteVocal","displayName":"ロジックノート(ボーカル)理性笔记(声乐)","type":"template","annotationId":"a1df3af1-a3e7-4521-a085-e4dc3cd1cc57","useHintRect":false},"a9fcaf04-0c1f-4b0f-bb5b-ede9da96180f":{"name":"Shop.ItemLogicNoteDance","displayName":"ロジックノート(ダンス)理性笔记(舞蹈)","type":"template","annotationId":"a9fcaf04-0c1f-4b0f-bb5b-ede9da96180f","useHintRect":false},"c3f536d6-a04a-4651-b3f9-dd2c22593f7f":{"name":"Shop.ItemLogicNoteVisual","displayName":"ロジックノート(ビジュアル)理性笔记(形象)","type":"template","annotationId":"c3f536d6-a04a-4651-b3f9-dd2c22593f7f","useHintRect":false},"eef25cf9-afd0-43b1-b9c5-fbd997bd5fe0":{"name":"Shop.ItemAnomalyNoteVocal","displayName":"アノマリーノート(ボーカル)非凡笔记(声乐)","type":"template","annotationId":"eef25cf9-afd0-43b1-b9c5-fbd997bd5fe0","useHintRect":false},"df991b42-ed8e-4f2c-bf0c-aa7522f147b6":{"name":"Shop.ItemAnomalyNoteDance","displayName":"アノマリーノート(ダンス)非凡笔记(舞蹈)","type":"template","annotationId":"df991b42-ed8e-4f2c-bf0c-aa7522f147b6","useHintRect":false}},"annotations":[{"id":"0949c622-9067-4f0d-bac2-3f938a1d2ed2","type":"rect","data":{"x1":243,"y1":355,"x2":313,"y2":441}},{"id":"b2af59e9-60e3-4d97-8c72-c7ba092113a3","type":"rect","data":{"x1":414,"y1":355,"x2":484,"y2":441}},{"id":"835489e2-b29b-426c-b4c9-3bb9f8eb6195","type":"rect","data":{"x1":574,"y1":363,"x2":662,"y2":438}},{"id":"c5b7d67e-7260-4f08-a0e9-4d31ce9bbecf","type":"rect","data":{"x1":71,"y1":594,"x2":142,"y2":667}},{"id":"0f7d581d-cea3-4039-9205-732e4cd29293","type":"rect","data":{"x1":241,"y1":593,"x2":309,"y2":667}},{"id":"d3cc3323-51af-4882-ae12-49e7384b746d","type":"rect","data":{"x1":417,"y1":586,"x2":481,"y2":668}},{"id":"a1df3af1-a3e7-4521-a085-e4dc3cd1cc57","type":"rect","data":{"x1":585,"y1":591,"x2":651,"y2":669}},{"id":"a9fcaf04-0c1f-4b0f-bb5b-ede9da96180f","type":"rect","data":{"x1":69,"y1":825,"x2":138,"y2":899}},{"id":"c3f536d6-a04a-4651-b3f9-dd2c22593f7f","type":"rect","data":{"x1":242,"y1":820,"x2":310,"y2":898}},{"id":"eef25cf9-afd0-43b1-b9c5-fbd997bd5fe0","type":"rect","data":{"x1":413,"y1":821,"x2":481,"y2":897}},{"id":"df991b42-ed8e-4f2c-bf0c-aa7522f147b6","type":"rect","data":{"x1":583,"y1":823,"x2":649,"y2":900}}]}

Binary file not shown.

After

Width:  |  Height:  |  Size: 743 KiB

View File

@ -0,0 +1 @@
{"definitions":{"9340b854-025c-40da-9387-385d38433bef":{"name":"Shop.ItemAnomalyNoteVisual","displayName":"アノマリーノート(ビジュアル)非凡笔记(形象)","type":"template","annotationId":"9340b854-025c-40da-9387-385d38433bef","useHintRect":false},"ea1ba124-9cb3-4427-969a-bacd47e7d920":{"name":"Shop.ItemRechallengeTicket","displayName":"再挑戦チケット 重新挑战券","type":"template","annotationId":"ea1ba124-9cb3-4427-969a-bacd47e7d920","useHintRect":false},"1926f2f9-4bd7-48eb-9eba-28ec4efb0606":{"name":"Shop.ItemRecordKey","displayName":"記録の鍵 解锁交流的物品","type":"template","annotationId":"1926f2f9-4bd7-48eb-9eba-28ec4efb0606","useHintRect":false},"6720b6e8-ae80-4cc0-a885-518efe12b707":{"name":"Shop.IdolPiece.倉本千奈_WonderScale","displayName":"倉本千奈 WonderScale 碎片","type":"template","annotationId":"6720b6e8-ae80-4cc0-a885-518efe12b707","useHintRect":false},"afa06fdc-a345-4384-b25d-b16540830256":{"name":"Shop.IdolPiece.篠泽广_光景","displayName":"篠泽广 光景 碎片","type":"template","annotationId":"afa06fdc-a345-4384-b25d-b16540830256","useHintRect":false},"278b7d9c-707e-4392-9677-74574b5cdf42":{"name":"Shop.IdolPiece.紫云清夏_TameLieOneStep","displayName":"紫云清夏 Tame-Lie-One-Step 碎片","type":"template","annotationId":"278b7d9c-707e-4392-9677-74574b5cdf42","useHintRect":false},"e9ee330d-dfca-440e-8b8c-0a3b4e8c8730":{"name":"Daily.IconTitleDailyShop","displayName":"日常商店标题图标","type":"template","annotationId":"e9ee330d-dfca-440e-8b8c-0a3b4e8c8730","useHintRect":false}},"annotations":[{"id":"9340b854-025c-40da-9387-385d38433bef","type":"rect","data":{"x1":72,"y1":611,"x2":138,"y2":693}},{"id":"ea1ba124-9cb3-4427-969a-bacd47e7d920","type":"rect","data":{"x1":227,"y1":639,"x2":316,"y2":674}},{"id":"1926f2f9-4bd7-48eb-9eba-28ec4efb0606","type":"rect","data":{"x1":385,"y1":591,"x2":508,"y2":694}},{"id":"6720b6e8-ae80-4cc0-a885-518efe12b707","type":"rect","data":{"x1":589,"y1":633,"x2":638,"y2":678}},{"id":"afa06fdc-a345-4384-b25d-b16540830256","type":"rect","data":{"x1":83,"y1":867,"x2":134,"y2":912}},{"id":"278b7d9c-707e-4392-9677-74574b5cdf42","type":"rect","data":{"x1":247,"y1":864,"x2":301,"y2":907}},{"id":"e9ee330d-dfca-440e-8b8c-0a3b4e8c8730","type":"rect","data":{"x1":17,"y1":35,"x2":59,"y2":76}}]}

View File

@ -180,7 +180,7 @@ def load_metadata(root_path: str, png_file: str) -> list[Resource]:
uuid=definition.annotationId,
name=definition.name.split('.')[-1],
display_name=definition.displayName,
class_path=to_camel_cases(definition.name.split('.')[:-1]),
class_path=definition.name.split('.')[:-1],
rel_path=png_file,
abs_path=os.path.abspath(clips[definition.annotationId]),
@ -192,7 +192,7 @@ def load_metadata(root_path: str, png_file: str) -> list[Resource]:
hb = HintBox(
name=definition.name.split('.')[-1],
display_name=definition.displayName,
class_path=to_camel_cases(definition.name.split('.')[:-1]),
class_path=definition.name.split('.')[:-1],
x1=annotation.data.x1,
y1=annotation.data.y1,
x2=annotation.data.x2,