Merge branch 'dev'
2
justfile
|
@ -67,7 +67,7 @@ generate-metadata: env
|
|||
@{{venv}} python -m build -s kotonebot-resource
|
||||
|
||||
# Package KAA
|
||||
@package: package-resource generate-metadata
|
||||
@package: env package-resource generate-metadata
|
||||
{{venv}} python tools/make_resources.py -p # Make R.py in production mode
|
||||
|
||||
Write-Host "Removing old build files..."
|
||||
|
|
|
@ -5,6 +5,12 @@ export interface PropertyRenderBase {
|
|||
required?: boolean,
|
||||
}
|
||||
|
||||
type SelectOption<T extends string> = {
|
||||
value: T,
|
||||
options: Array<{ value: T, label: string }>,
|
||||
onChange: (value: T) => void
|
||||
}
|
||||
|
||||
interface PropertyRenderInputOptions {
|
||||
text: {
|
||||
value: string,
|
||||
|
@ -17,7 +23,8 @@ interface PropertyRenderInputOptions {
|
|||
'long-text': {
|
||||
value: string,
|
||||
onChange: (value: string) => void
|
||||
}
|
||||
},
|
||||
select: SelectOption<string>
|
||||
}
|
||||
type RenderType = keyof PropertyRenderInputOptions;
|
||||
|
||||
|
@ -162,6 +169,21 @@ const PropertyGrid: React.FC<PropertyGridProps> = ({ properties, titleColumnWidt
|
|||
} else if (type === 'long-text') {
|
||||
const propertyLongText = property.render as PropertyRenderInputOptions['long-text'];
|
||||
field = <textarea value={propertyLongText.value} onChange={(e) => propertyLongText.onChange(e.target.value)} />;
|
||||
} else if (type === 'select') {
|
||||
const propertySelect = property.render as PropertyRenderInputOptions['select'];
|
||||
field = (
|
||||
<select
|
||||
value={propertySelect.value}
|
||||
onChange={(e) => propertySelect.onChange(e.target.value as any)}
|
||||
style={{ width: '100%', padding: '4px' }}
|
||||
>
|
||||
{propertySelect.options.map(option => (
|
||||
<option key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
);
|
||||
}
|
||||
} else {
|
||||
console.error('Invalid property render type:', property.render);
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
import { useState } from "react";
|
||||
import { Annotation } from "../components/ImageEditor/types";
|
||||
import { useImmer } from "use-immer";
|
||||
|
||||
export type DefinitionType = 'template' | 'ocr' | 'color' | 'hint-box' | 'hint-point';
|
||||
|
||||
export interface Definition {
|
||||
export interface BaseDefinition {
|
||||
/** 最终出现在 R.py 中的名称 */
|
||||
name: string;
|
||||
/** 显示在调试器与调试输出中的名称 */
|
||||
|
@ -17,7 +16,7 @@ export interface Definition {
|
|||
}
|
||||
|
||||
|
||||
export interface TemplateDefinition extends Definition {
|
||||
export interface TemplateDefinition extends BaseDefinition {
|
||||
type: 'template';
|
||||
/**
|
||||
* 是否将这个模板的矩形范围作为运行时
|
||||
|
@ -29,6 +28,15 @@ export interface TemplateDefinition extends Definition {
|
|||
useHintRect: boolean
|
||||
}
|
||||
|
||||
export interface HintBoxDefinition extends BaseDefinition {
|
||||
type: 'hint-box';
|
||||
}
|
||||
|
||||
export interface HintPointDefinition extends BaseDefinition {
|
||||
type: 'hint-point';
|
||||
}
|
||||
|
||||
export type Definition = TemplateDefinition | HintBoxDefinition | HintPointDefinition;
|
||||
|
||||
export type Definitions = Record<string, Definition>;
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ import PropertyGrid, { Property, PropertyCategory } from '../../components/Prope
|
|||
import ImageEditor, { AnnotationChangedEvent } from '../../components/ImageEditor/ImageEditor';
|
||||
import { Annotation, Tool as EditorTool } from '../../components/ImageEditor/types';
|
||||
import { BsCursor, BsFolder2Open, BsFloppy, BsCardImage, BsQuestionSquare, BsPinMap } from 'react-icons/bs';
|
||||
import useImageMetaData, { DefinitionType, ImageMetaData, TemplateDefinition, Definitions } from '../../hooks/useImageMetaData';
|
||||
import useImageMetaData, { DefinitionType, ImageMetaData, TemplateDefinition, Definitions, Definition } from '../../hooks/useImageMetaData';
|
||||
import { useImageViewerModal } from '../../components/ImageViewerModal';
|
||||
import { useMessageBox } from '../../hooks/useMessageBox';
|
||||
import { useToast } from '../../components/ToastMessage';
|
||||
|
@ -138,7 +138,7 @@ const usePropertyGridData = (
|
|||
definitions: Definitions,
|
||||
image: HTMLImageElement | null,
|
||||
onImageClick: (imageUrl: string) => void,
|
||||
onDefinitionChange?: (id: string, changes: Partial<TemplateDefinition>) => void,
|
||||
onDefinitionChange?: (id: string, changes: Partial<Definition>) => void,
|
||||
imageFileName?: string,
|
||||
annotations?: Annotation[],
|
||||
currentFileResult?: FileResult | null
|
||||
|
@ -246,7 +246,25 @@ const usePropertyGridData = (
|
|||
},
|
||||
{
|
||||
title: '类型',
|
||||
render: () => definition.type,
|
||||
render: {
|
||||
type: 'select',
|
||||
required: true,
|
||||
value: definition.type,
|
||||
options: selectedAnnotation.type === 'rect'
|
||||
? [
|
||||
{ value: 'template', label: '模板' },
|
||||
{ value: 'hint-box', label: 'HintBox' }
|
||||
]
|
||||
: [
|
||||
{ value: 'hint-point', label: 'HintPoint' }
|
||||
],
|
||||
onChange: (value) => {
|
||||
if (value === definition.type) return;
|
||||
onDefinitionChange?.(selectedAnnotation.id, {
|
||||
type: value as "template" | "hint-box" | "hint-point",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
foldable: true
|
||||
|
@ -540,7 +558,7 @@ const ImageAnnotation: React.FC = () => {
|
|||
setSelectedAnnotation(annotation);
|
||||
};
|
||||
|
||||
const handleDefinitionChange = (id: string, changes: Partial<TemplateDefinition>) => {
|
||||
const handleDefinitionChange = (id: string, changes: Partial<Definition>) => {
|
||||
Definitions.update({
|
||||
...changes,
|
||||
annotationId: id
|
||||
|
|
|
@ -9,7 +9,7 @@ import { Splitable } from '../../components/Splitable';
|
|||
import { Tool, Annotation } from '../../components/ImageEditor/types';
|
||||
import { create } from 'zustand';
|
||||
import { css } from '@emotion/react';
|
||||
import useImageMetaData, { Definition, DefinitionType, ImageMetaData, TemplateDefinition } from '../../hooks/useImageMetaData';
|
||||
import useImageMetaData, { BaseDefinition, DefinitionType, ImageMetaData, TemplateDefinition } from '../../hooks/useImageMetaData';
|
||||
import { useDarkMode } from '../../hooks/useDarkMode';
|
||||
import { useDebugClient } from '../../store/debugStore';
|
||||
import useLatestCallback from '../../hooks/useLatestCallback';
|
||||
|
@ -157,25 +157,25 @@ const useScriptRecorderStore = create<ScriptRecorderState>((set) => ({
|
|||
}));
|
||||
|
||||
interface ToolConfigItem {
|
||||
code?: (d: Definition, a: Annotation) => string;
|
||||
code?: (d: BaseDefinition, a: Annotation) => string;
|
||||
}
|
||||
|
||||
const ToolConfig: Record<ScriptRecorderTool, ToolConfigItem> = {
|
||||
'drag': {
|
||||
},
|
||||
'template': {
|
||||
code: (d: Definition) => `image.find(R.${d.name})`,
|
||||
code: (d: BaseDefinition) => `image.find(R.${d.name})`,
|
||||
|
||||
},
|
||||
'template-click': {
|
||||
code: (d: Definition) =>
|
||||
code: (d: BaseDefinition) =>
|
||||
`if image.find(R.${d.name}):\n\tdevice.click()`,
|
||||
},
|
||||
'ocr': {
|
||||
code: (d: Definition) => `ocr.ocr(R.${d.name})`,
|
||||
code: (d: BaseDefinition) => `ocr.ocr(R.${d.name})`,
|
||||
},
|
||||
'ocr-click': {
|
||||
code: (d: Definition) =>
|
||||
code: (d: BaseDefinition) =>
|
||||
`if ocr.ocr(R.${d.name}):\n\tdevice.click()`,
|
||||
},
|
||||
'hint-box': {
|
||||
|
|
Before Width: | Height: | Size: 188 B |
Before Width: | Height: | Size: 474 B After Width: | Height: | Size: 474 B |
Before Width: | Height: | Size: 711 KiB After Width: | Height: | Size: 711 KiB |
|
@ -0,0 +1 @@
|
|||
{"definitions":{"b4081250-d962-46ad-9257-03b68ea99a1e":{"name":"Daily.PointDissmissContestReward","displayName":"跳过竞赛赛季奖励动画","type":"hint-point","annotationId":"b4081250-d962-46ad-9257-03b68ea99a1e","useHintRect":false}},"annotations":[{"id":"b4081250-d962-46ad-9257-03b68ea99a1e","type":"point","data":{"x":604,"y":178}}]}
|
Before Width: | Height: | Size: 924 KiB After Width: | Height: | Size: 924 KiB |
|
@ -0,0 +1 @@
|
|||
{"definitions":{"f1f21925-3e22-4dd1-b53b-bb52bcf26c2b":{"name":"Common.ButtonCommuSkip","displayName":"跳过交流按钮","type":"template","annotationId":"f1f21925-3e22-4dd1-b53b-bb52bcf26c2b","useHintRect":false}},"annotations":[{"id":"f1f21925-3e22-4dd1-b53b-bb52bcf26c2b","type":"rect","data":{"x1":180,"y1":1187,"x2":204,"y2":1215}}]}
|
|
@ -1 +1 @@
|
|||
{"definitions":{"05890a1b-8764-4e9f-9d21-65d292c22e13":{"name":"InPurodyuusu.BoxGoalClearNext","displayName":"培育目标达成 NEXT 文字区域","type":"hint-box","annotationId":"05890a1b-8764-4e9f-9d21-65d292c22e13","useHintRect":false}},"annotations":[{"id":"05890a1b-8764-4e9f-9d21-65d292c22e13","type":"rect","data":{"x1":79,"y1":503,"x2":157,"y2":532}}]}
|
||||
{"definitions":{"05890a1b-8764-4e9f-9d21-65d292c22e13":{"name":"InPurodyuusu.TextGoalClearNext","displayName":"培育目标达成 NEXT 文字","type":"template","annotationId":"05890a1b-8764-4e9f-9d21-65d292c22e13","useHintRect":false}},"annotations":[{"id":"05890a1b-8764-4e9f-9d21-65d292c22e13","type":"rect","data":{"x1":79,"y1":503,"x2":157,"y2":532}}]}
|
|
@ -1 +1 @@
|
|||
{"definitions":{"582d36c0-0916-4706-9833-4fbc026701f5":{"name":"InPurodyuusu.BoxPDrinkMaxConfirmTitle","displayName":"P饮料溢出 不领取弹窗标题","type":"hint-box","annotationId":"582d36c0-0916-4706-9833-4fbc026701f5","useHintRect":false}},"annotations":[{"id":"582d36c0-0916-4706-9833-4fbc026701f5","type":"rect","data":{"x1":46,"y1":829,"x2":270,"y2":876}}]}
|
||||
{"definitions":{"582d36c0-0916-4706-9833-4fbc026701f5":{"name":"InPurodyuusu.TextPDrinkMaxConfirmTitle","displayName":"P饮料溢出 不领取弹窗标题","type":"template","annotationId":"582d36c0-0916-4706-9833-4fbc026701f5","useHintRect":false}},"annotations":[{"id":"582d36c0-0916-4706-9833-4fbc026701f5","type":"rect","data":{"x1":46,"y1":829,"x2":270,"y2":876}}]}
|
After Width: | Height: | Size: 602 KiB |
|
@ -0,0 +1 @@
|
|||
{"definitions":{"bab6c393-692c-4681-ac0d-76c0d9dabea6":{"name":"InPurodyuusu.IconTitleSkillCardRemoval","displayName":"技能卡自选删除 标题图标","type":"template","annotationId":"bab6c393-692c-4681-ac0d-76c0d9dabea6","useHintRect":false},"00551158-fee9-483f-b034-549139a96f58":{"name":"InPurodyuusu.ButtonRemove","displayName":"削除","type":"template","annotationId":"00551158-fee9-483f-b034-549139a96f58","useHintRect":false,"description":"技能卡自选删除对话框上的确认按钮"}},"annotations":[{"id":"bab6c393-692c-4681-ac0d-76c0d9dabea6","type":"rect","data":{"x1":19,"y1":36,"x2":58,"y2":79}},{"id":"00551158-fee9-483f-b034-549139a96f58","type":"rect","data":{"x1":247,"y1":1137,"x2":417,"y2":1185}}]}
|
After Width: | Height: | Size: 465 KiB |
|
@ -0,0 +1 @@
|
|||
{"definitions":{"8c179a21-be6f-4db8-a9b0-9afeb5c36b1c":{"name":"InPurodyuusu.TextPDrink","displayName":"文本「Pドリンク」","type":"template","annotationId":"8c179a21-be6f-4db8-a9b0-9afeb5c36b1c","useHintRect":false,"description":"用于 P 饮料选择对话框"}},"annotations":[{"id":"8c179a21-be6f-4db8-a9b0-9afeb5c36b1c","type":"rect","data":{"x1":244,"y1":615,"x2":357,"y2":641}}]}
|
|
@ -1 +1 @@
|
|||
{"definitions":{"6b58d90d-2e5e-4b7f-bc01-941f2633de89":{"name":"InPurodyuusu.BoxSelectPStuff","displayName":"选择奖励对话框提示语","type":"hint-box","annotationId":"6b58d90d-2e5e-4b7f-bc01-941f2633de89","useHintRect":false},"b3c9e526-de47-44c2-9bda-e9a8a79fdb5a":{"name":"InPurodyuusu.BoxSelectPStuffComfirm","displayName":"选择奖励对话框 领取按钮","type":"hint-box","annotationId":"b3c9e526-de47-44c2-9bda-e9a8a79fdb5a","useHintRect":false}},"annotations":[{"id":"6b58d90d-2e5e-4b7f-bc01-941f2633de89","type":"rect","data":{"x1":62,"y1":558,"x2":655,"y2":716}},{"id":"b3c9e526-de47-44c2-9bda-e9a8a79fdb5a","type":"rect","data":{"x1":256,"y1":1064,"x2":478,"y2":1128}}]}
|
||||
{"definitions":{"b3c9e526-de47-44c2-9bda-e9a8a79fdb5a":{"name":"InPurodyuusu.BoxSelectPStuffComfirm","displayName":"选择奖励对话框 领取按钮","type":"hint-box","annotationId":"b3c9e526-de47-44c2-9bda-e9a8a79fdb5a","useHintRect":false},"c948f136-416f-447e-8152-54a1cd1d1329":{"name":"InPurodyuusu.TextClaim","displayName":"文字「受け取る」","type":"template","annotationId":"c948f136-416f-447e-8152-54a1cd1d1329","useHintRect":false,"description":"用于 P 物品、P 饮料、技能卡选择对话框"},"0c0627be-4a09-4450-a078-1858d3ace532":{"name":"InPurodyuusu.TextPItem","displayName":"文字「Pアイテム」","type":"template","annotationId":"0c0627be-4a09-4450-a078-1858d3ace532","useHintRect":false,"description":"用于 P 物品选择对话框"}},"annotations":[{"id":"b3c9e526-de47-44c2-9bda-e9a8a79fdb5a","type":"rect","data":{"x1":256,"y1":1064,"x2":478,"y2":1128}},{"id":"c948f136-416f-447e-8152-54a1cd1d1329","type":"rect","data":{"x1":144,"y1":608,"x2":245,"y2":647}},{"id":"0c0627be-4a09-4450-a078-1858d3ace532","type":"rect","data":{"x1":247,"y1":613,"x2":354,"y2":645}}]}
|
After Width: | Height: | Size: 737 KiB |
|
@ -0,0 +1 @@
|
|||
{"definitions":{"d271a24f-efe8-424d-8fd5-f6b3756ba4ca":{"name":"InPurodyuusu.TextSkillCard","displayName":"文字「スキルカード」","type":"template","annotationId":"d271a24f-efe8-424d-8fd5-f6b3756ba4ca","useHintRect":false,"description":"用于技能卡选择对话框"}},"annotations":[{"id":"d271a24f-efe8-424d-8fd5-f6b3756ba4ca","type":"rect","data":{"x1":229,"y1":614,"x2":372,"y2":645}}]}
|
|
@ -1 +1 @@
|
|||
{"definitions":{"6b58d90d-2e5e-4b7f-bc01-941f2633de89":{"name":"InPurodyuusu.BoxSkillCardEnhaced","displayName":"技能卡强化文本提示","type":"hint-box","annotationId":"6b58d90d-2e5e-4b7f-bc01-941f2633de89","useHintRect":false}},"annotations":[{"id":"6b58d90d-2e5e-4b7f-bc01-941f2633de89","type":"rect","data":{"x1":49,"y1":948,"x2":676,"y2":1106}}]}
|
||||
{"definitions":{"6b58d90d-2e5e-4b7f-bc01-941f2633de89":{"name":"InPurodyuusu.IconSkillCardEventBubble","displayName":"技能卡事件气泡框图标","type":"template","annotationId":"6b58d90d-2e5e-4b7f-bc01-941f2633de89","useHintRect":false,"description":"背景上左右两边的星星"}},"annotations":[{"id":"6b58d90d-2e5e-4b7f-bc01-941f2633de89","type":"rect","data":{"x1":46,"y1":1007,"x2":79,"y2":1047}}]}
|
After Width: | Height: | Size: 771 KiB |
|
@ -0,0 +1 @@
|
|||
{"definitions":{"e5e84f9e-28da-4cf4-bcba-c9145fe39b07":{"name":"Produce.ButtonSkipLive","displayName":"培育结束跳过演出按钮","type":"template","annotationId":"e5e84f9e-28da-4cf4-bcba-c9145fe39b07","useHintRect":false}},"annotations":[{"id":"e5e84f9e-28da-4cf4-bcba-c9145fe39b07","type":"rect","data":{"x1":1179,"y1":56,"x2":1221,"y2":106}}]}
|
After Width: | Height: | Size: 339 KiB |
|
@ -0,0 +1 @@
|
|||
{"definitions":{"b6b94f21-ef4b-4425-9c7e-ca2b574b0add":{"name":"Produce.TextSkipLiveDialogTitle","displayName":"跳过演出确认对话框标题","type":"template","annotationId":"b6b94f21-ef4b-4425-9c7e-ca2b574b0add","useHintRect":false},"fad5eec2-5fd5-412f-9abb-987a3087dc54":{"name":"Common.IconButtonCheck","displayName":"按钮✓图标","type":"template","annotationId":"fad5eec2-5fd5-412f-9abb-987a3087dc54","useHintRect":false,"description":"通常会显示在对话框按钮上"},"bc7155ac-18c9-4335-9ec2-c8762d37a057":{"name":"Common.IconButtonCross","displayName":"按钮×图标","type":"template","annotationId":"bc7155ac-18c9-4335-9ec2-c8762d37a057","useHintRect":false,"description":"通常会显示在对话框按钮上"}},"annotations":[{"id":"b6b94f21-ef4b-4425-9c7e-ca2b574b0add","type":"rect","data":{"x1":336,"y1":358,"x2":572,"y2":399}},{"id":"fad5eec2-5fd5-412f-9abb-987a3087dc54","type":"rect","data":{"x1":666,"y1":576,"x2":721,"y2":624}},{"id":"bc7155ac-18c9-4335-9ec2-c8762d37a057","type":"rect","data":{"x1":390,"y1":577,"x2":434,"y2":624}}]}
|
|
@ -42,13 +42,16 @@ import kotonebot.backend.color as raw_color
|
|||
from kotonebot.backend.color import (
|
||||
find as color_find, find_all as color_find_all
|
||||
)
|
||||
from kotonebot.backend.ocr import Ocr, OcrResult, OcrResultList, jp, en, StringMatchFunction
|
||||
from kotonebot.backend.ocr import (
|
||||
Ocr, OcrResult, OcrResultList, jp, en, StringMatchFunction
|
||||
)
|
||||
from kotonebot.client.factory import create_device
|
||||
from kotonebot.config.manager import load_config, save_config
|
||||
from kotonebot.config.base_config import UserConfig
|
||||
from kotonebot.backend.core import Image, HintBox
|
||||
from kotonebot.errors import KotonebotWarning
|
||||
from kotonebot.client.factory import DeviceImpl
|
||||
from kotonebot.backend.preprocessor import PreprocessorProtocol
|
||||
|
||||
OcrLanguage = Literal['jp', 'en']
|
||||
ScreenshotMode = Literal['auto', 'manual', 'manual-inherit']
|
||||
|
@ -396,6 +399,7 @@ class ContextImage:
|
|||
*,
|
||||
transparent: bool = False,
|
||||
interval: float = DEFAULT_INTERVAL,
|
||||
preprocessors: list[PreprocessorProtocol] | None = None,
|
||||
) -> TemplateMatchResult | None:
|
||||
"""
|
||||
等待指定图像出现。
|
||||
|
@ -406,7 +410,14 @@ class ContextImage:
|
|||
while True:
|
||||
if is_manual:
|
||||
device.screenshot()
|
||||
ret = self.find(template, mask, transparent=transparent, threshold=threshold, colored=colored)
|
||||
ret = self.find(
|
||||
template,
|
||||
mask,
|
||||
transparent=transparent,
|
||||
threshold=threshold,
|
||||
colored=colored,
|
||||
preprocessors=preprocessors,
|
||||
)
|
||||
if ret is not None:
|
||||
self.context.device.last_find = ret
|
||||
return ret
|
||||
|
@ -423,7 +434,8 @@ class ContextImage:
|
|||
colored: bool = False,
|
||||
*,
|
||||
transparent: bool = False,
|
||||
interval: float = DEFAULT_INTERVAL
|
||||
interval: float = DEFAULT_INTERVAL,
|
||||
preprocessors: list[PreprocessorProtocol] | None = None,
|
||||
):
|
||||
"""
|
||||
等待指定图像中的任意一个出现。
|
||||
|
@ -439,7 +451,14 @@ class ContextImage:
|
|||
if is_manual:
|
||||
device.screenshot()
|
||||
for template, mask in zip(templates, _masks):
|
||||
if self.find(template, mask, transparent=transparent, threshold=threshold, colored=colored):
|
||||
if self.find(
|
||||
template,
|
||||
mask,
|
||||
transparent=transparent,
|
||||
threshold=threshold,
|
||||
colored=colored,
|
||||
preprocessors=preprocessors,
|
||||
):
|
||||
return True
|
||||
if time.time() - start_time > timeout:
|
||||
return False
|
||||
|
@ -454,7 +473,8 @@ class ContextImage:
|
|||
colored: bool = False,
|
||||
*,
|
||||
transparent: bool = False,
|
||||
interval: float = DEFAULT_INTERVAL
|
||||
interval: float = DEFAULT_INTERVAL,
|
||||
preprocessors: list[PreprocessorProtocol] | None = None,
|
||||
) -> TemplateMatchResult:
|
||||
"""
|
||||
等待指定图像出现。
|
||||
|
@ -465,7 +485,14 @@ class ContextImage:
|
|||
while True:
|
||||
if is_manual:
|
||||
device.screenshot()
|
||||
ret = self.find(template, mask, transparent=transparent, threshold=threshold, colored=colored)
|
||||
ret = self.find(
|
||||
template,
|
||||
mask,
|
||||
transparent=transparent,
|
||||
threshold=threshold,
|
||||
colored=colored,
|
||||
preprocessors=preprocessors,
|
||||
)
|
||||
if ret is not None:
|
||||
self.context.device.last_find = ret
|
||||
return ret
|
||||
|
@ -482,7 +509,8 @@ class ContextImage:
|
|||
colored: bool = False,
|
||||
*,
|
||||
transparent: bool = False,
|
||||
interval: float = DEFAULT_INTERVAL
|
||||
interval: float = DEFAULT_INTERVAL,
|
||||
preprocessors: list[PreprocessorProtocol] | None = None,
|
||||
) -> TemplateMatchResult:
|
||||
"""
|
||||
等待指定图像中的任意一个出现。
|
||||
|
@ -498,7 +526,14 @@ class ContextImage:
|
|||
if is_manual:
|
||||
device.screenshot()
|
||||
for template, mask in zip(templates, _masks):
|
||||
ret = self.find(template, mask, transparent=transparent, threshold=threshold, colored=colored)
|
||||
ret = self.find(
|
||||
template,
|
||||
mask,
|
||||
transparent=transparent,
|
||||
threshold=threshold,
|
||||
colored=colored,
|
||||
preprocessors=preprocessors,
|
||||
)
|
||||
if ret is not None:
|
||||
self.context.device.last_find = ret
|
||||
return ret
|
||||
|
|
|
@ -10,6 +10,7 @@ from skimage.metrics import structural_similarity
|
|||
from .core import Image, unify_image
|
||||
from ..util import Rect, Point
|
||||
from .debug import result as debug_result, debug, img
|
||||
from .preprocessor import PreprocessorProtocol
|
||||
|
||||
logger = getLogger(__name__)
|
||||
|
||||
|
@ -124,6 +125,8 @@ def _results2str(results: Sequence[TemplateMatchResult | MultipleTemplateMatchRe
|
|||
return 'None'
|
||||
return ', '.join([_result2str(result) for result in results])
|
||||
|
||||
# TODO: 应该把 template_match 和 find、wait、expect 等函数的公共参数提取出来
|
||||
# TODO: 需要在调试结果中输出 preprocessors 处理后的图像
|
||||
def template_match(
|
||||
template: MatLike | str | Image,
|
||||
image: MatLike | str | Image,
|
||||
|
@ -134,6 +137,7 @@ def template_match(
|
|||
max_results: int = 5,
|
||||
remove_duplicate: bool = True,
|
||||
colored: bool = False,
|
||||
preprocessors: list[PreprocessorProtocol] | None = None,
|
||||
) -> list[TemplateMatchResult]:
|
||||
"""
|
||||
寻找模板在图像中的位置。
|
||||
|
@ -150,6 +154,7 @@ def template_match(
|
|||
:param max_results: 最大结果数,默认为 1。
|
||||
:param remove_duplicate: 是否移除重复结果,默认为 True。
|
||||
:param colored: 是否匹配颜色,默认为 False。
|
||||
:param preprocessors: 预处理列表,默认为 None。
|
||||
"""
|
||||
# 统一参数
|
||||
template = unify_image(template, transparent)
|
||||
|
@ -164,6 +169,13 @@ def template_match(
|
|||
# 从透明图像中提取 alpha 通道作为 mask
|
||||
mask = cv2.threshold(template[:, :, 3], 0, 255, cv2.THRESH_BINARY)[1]
|
||||
template = template[:, :, :3]
|
||||
# 预处理
|
||||
if preprocessors is not None:
|
||||
for preprocessor in preprocessors:
|
||||
image = preprocessor.process(image)
|
||||
template = preprocessor.process(template)
|
||||
if mask is not None:
|
||||
mask = preprocessor.process(mask)
|
||||
# 匹配模板
|
||||
if mask is not None:
|
||||
# https://stackoverflow.com/questions/35642497/python-opencv-cv2-matchtemplate-with-transparency
|
||||
|
@ -302,6 +314,7 @@ def find_all_crop(
|
|||
*,
|
||||
colored: bool = False,
|
||||
remove_duplicate: bool = True,
|
||||
preprocessors: list[PreprocessorProtocol] | None = None,
|
||||
) -> list[CropResult]:
|
||||
"""
|
||||
指定一个模板,在输入图像中寻找其出现的所有位置,并裁剪出结果。
|
||||
|
@ -313,6 +326,7 @@ def find_all_crop(
|
|||
:param threshold: 阈值,默认为 0.8。
|
||||
:param colored: 是否匹配颜色,默认为 False。
|
||||
:param remove_duplicate: 是否移除重复结果,默认为 True。
|
||||
:param preprocessors: 预处理列表,默认为 None。
|
||||
"""
|
||||
matches = template_match(
|
||||
template,
|
||||
|
@ -323,6 +337,7 @@ def find_all_crop(
|
|||
max_results=-1,
|
||||
remove_duplicate=remove_duplicate,
|
||||
colored=colored,
|
||||
preprocessors=preprocessors,
|
||||
)
|
||||
# logger.debug(
|
||||
# f'find_all_crop(): template: {_img2str(template)} image: {_img2str(image)} mask: {_img2str(mask)} '
|
||||
|
@ -345,6 +360,7 @@ def find(
|
|||
debug_output: bool = True,
|
||||
colored: bool = False,
|
||||
remove_duplicate: bool = True,
|
||||
preprocessors: list[PreprocessorProtocol] | None = None,
|
||||
) -> TemplateMatchResult | None:
|
||||
"""
|
||||
指定一个模板,在输入图像中寻找其出现的第一个位置。
|
||||
|
@ -357,6 +373,7 @@ def find(
|
|||
:param debug_output: 是否输出调试信息,默认为 True。
|
||||
:param colored: 是否匹配颜色,默认为 False。
|
||||
:param remove_duplicate: 是否移除重复结果,默认为 True。
|
||||
:param preprocessors: 预处理列表,默认为 None。
|
||||
"""
|
||||
matches = template_match(
|
||||
template,
|
||||
|
@ -367,6 +384,7 @@ def find(
|
|||
max_results=1,
|
||||
remove_duplicate=remove_duplicate,
|
||||
colored=colored,
|
||||
preprocessors=preprocessors,
|
||||
)
|
||||
# logger.debug(
|
||||
# f'find(): template: {_img2str(template)} image: {_img2str(image)} mask: {_img2str(mask)} '
|
||||
|
@ -396,6 +414,7 @@ def find_all(
|
|||
remove_duplicate: bool = True,
|
||||
colored: bool = False,
|
||||
debug_output: bool = True,
|
||||
preprocessors: list[PreprocessorProtocol] | None = None,
|
||||
) -> list[TemplateMatchResult]:
|
||||
"""
|
||||
指定一个模板,在输入图像中寻找其出现的所有位置。
|
||||
|
@ -407,6 +426,7 @@ def find_all(
|
|||
:param threshold: 阈值,默认为 0.8。
|
||||
:param remove_duplicate: 是否移除重复结果,默认为 True。
|
||||
:param colored: 是否匹配颜色,默认为 False。
|
||||
:param preprocessors: 预处理列表,默认为 None。
|
||||
"""
|
||||
results = template_match(
|
||||
template,
|
||||
|
@ -417,6 +437,7 @@ def find_all(
|
|||
max_results=-1,
|
||||
remove_duplicate=remove_duplicate,
|
||||
colored=colored,
|
||||
preprocessors=preprocessors,
|
||||
)
|
||||
# logger.debug(
|
||||
# f'find_all(): template: {_img2str(template)} image: {_img2str(image)} mask: {_img2str(mask)} '
|
||||
|
@ -441,6 +462,7 @@ def find_multi(
|
|||
threshold: float = 0.8,
|
||||
colored: bool = False,
|
||||
remove_duplicate: bool = True,
|
||||
preprocessors: list[PreprocessorProtocol] | None = None,
|
||||
) -> MultipleTemplateMatchResult | None:
|
||||
"""
|
||||
指定多个模板,在输入图像中逐个寻找模板,返回第一个匹配到的结果。
|
||||
|
@ -452,6 +474,7 @@ def find_multi(
|
|||
:param threshold: 阈值,默认为 0.8。
|
||||
:param colored: 是否匹配颜色,默认为 False。
|
||||
:param remove_duplicate: 是否移除重复结果,默认为 True。
|
||||
:param preprocessors: 预处理列表,默认为 None。
|
||||
"""
|
||||
ret = None
|
||||
if masks is None:
|
||||
|
@ -468,6 +491,7 @@ def find_multi(
|
|||
colored=colored,
|
||||
debug_output=False,
|
||||
remove_duplicate=remove_duplicate,
|
||||
preprocessors=preprocessors,
|
||||
)
|
||||
# 调试输出
|
||||
if find_result is not None:
|
||||
|
@ -508,6 +532,7 @@ def find_all_multi(
|
|||
threshold: float = 0.8,
|
||||
colored: bool = False,
|
||||
remove_duplicate: bool = True,
|
||||
preprocessors: list[PreprocessorProtocol] | None = None,
|
||||
) -> list[MultipleTemplateMatchResult]:
|
||||
"""
|
||||
指定多个模板,在输入图像中逐个寻找模板,返回所有匹配到的结果。
|
||||
|
@ -526,6 +551,7 @@ def find_all_multi(
|
|||
:param threshold: 阈值,默认为 0.8。
|
||||
:param colored: 是否匹配颜色,默认为 False。
|
||||
:param remove_duplicate: 是否移除重复结果,默认为 True。
|
||||
:param preprocessors: 预处理列表,默认为 None。
|
||||
:return: 匹配到的一维结果列表。
|
||||
"""
|
||||
ret: list[MultipleTemplateMatchResult] = []
|
||||
|
@ -544,6 +570,7 @@ def find_all_multi(
|
|||
colored=colored,
|
||||
remove_duplicate=remove_duplicate,
|
||||
debug_output=False,
|
||||
preprocessors=preprocessors,
|
||||
)
|
||||
ret.extend([
|
||||
MultipleTemplateMatchResult.from_template_match_result(r, index)
|
||||
|
@ -591,6 +618,7 @@ def count(
|
|||
threshold: float = 0.8,
|
||||
remove_duplicate: bool = True,
|
||||
colored: bool = False,
|
||||
preprocessors: list[PreprocessorProtocol] | None = None,
|
||||
) -> int:
|
||||
"""
|
||||
指定一个模板,统计其出现的次数。
|
||||
|
@ -602,6 +630,7 @@ def count(
|
|||
:param threshold: 阈值,默认为 0.8。
|
||||
:param remove_duplicate: 是否移除重复结果,默认为 True。
|
||||
:param colored: 是否匹配颜色,默认为 False。
|
||||
:param preprocessors: 预处理列表,默认为 None。
|
||||
"""
|
||||
results = template_match(
|
||||
template,
|
||||
|
@ -612,6 +641,7 @@ def count(
|
|||
max_results=-1,
|
||||
remove_duplicate=remove_duplicate,
|
||||
colored=colored,
|
||||
preprocessors=preprocessors,
|
||||
)
|
||||
# logger.debug(
|
||||
# f'count(): template: {_img2str(template)} image: {_img2str(image)} mask: {_img2str(mask)} '
|
||||
|
@ -641,6 +671,7 @@ def expect(
|
|||
threshold: float = 0.8,
|
||||
colored: bool = False,
|
||||
remove_duplicate: bool = True,
|
||||
preprocessors: list[PreprocessorProtocol] | None = None,
|
||||
) -> TemplateMatchResult:
|
||||
"""
|
||||
指定一个模板,寻找其出现的第一个位置。若未找到,则抛出异常。
|
||||
|
@ -652,6 +683,7 @@ def expect(
|
|||
:param threshold: 阈值,默认为 0.8。
|
||||
:param colored: 是否匹配颜色,默认为 False。
|
||||
:param remove_duplicate: 是否移除重复结果,默认为 True。
|
||||
:param preprocessors: 预处理列表,默认为 None。
|
||||
"""
|
||||
ret = find(
|
||||
image,
|
||||
|
@ -662,6 +694,7 @@ def expect(
|
|||
colored=colored,
|
||||
remove_duplicate=remove_duplicate,
|
||||
debug_output=False,
|
||||
preprocessors=preprocessors,
|
||||
)
|
||||
# logger.debug(
|
||||
# f'expect(): template: {_img2str(template)} image: {_img2str(image)} mask: {_img2str(mask)} '
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
|
||||
from typing import Protocol
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
from cv2.typing import MatLike
|
||||
|
||||
class PreprocessorProtocol(Protocol):
|
||||
"""预处理协议。用于 Image 与 Ocr 中的 `preprocessor` 参数。"""
|
||||
def process(self, image: MatLike) -> MatLike:
|
||||
"""
|
||||
预处理图像。
|
||||
|
||||
:param image: 输入图像,格式为 BGR。
|
||||
:return: 预处理后的图像,格式不限。
|
||||
"""
|
||||
...
|
||||
|
||||
class HsvColorFilter(PreprocessorProtocol):
|
||||
"""HSV 颜色过滤器。用于保留指定颜色。"""
|
||||
def __init__(
|
||||
self,
|
||||
lower: tuple[int, int, int],
|
||||
upper: tuple[int, int, int],
|
||||
*,
|
||||
name: str | None = None,
|
||||
):
|
||||
self.lower = np.array(lower)
|
||||
self.upper = np.array(upper)
|
||||
self.name = name
|
||||
|
||||
def process(self, image: MatLike) -> MatLike:
|
||||
hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)
|
||||
mask = cv2.inRange(hsv, self.lower, self.upper)
|
||||
return mask
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f'HsvColorFilter(for color "{self.name}" with range {self.lower} - {self.upper})'
|
|
@ -65,7 +65,8 @@ class Device:
|
|||
"""上次 image 对象或 ocr 对象的寻找结果"""
|
||||
self.orientation: Literal['portrait', 'landscape'] = 'portrait'
|
||||
"""
|
||||
设备当前方向。默认为竖屏。
|
||||
设备当前方向。默认为竖屏。注意此属性并非用于检测设备方向。
|
||||
如果需要检测设备方向,请使用 `self.detect_orientation()` 方法。
|
||||
|
||||
横屏时为 'landscape',竖屏时为 'portrait'。
|
||||
"""
|
||||
|
|
|
@ -72,7 +72,7 @@ class UserConfig(ConfigBaseModel, Generic[T]):
|
|||
|
||||
|
||||
class RootConfig(ConfigBaseModel, Generic[T]):
|
||||
version: int = 1
|
||||
version: int = 2
|
||||
"""配置版本。"""
|
||||
user_configs: list[UserConfig[T]] = []
|
||||
"""用户配置。"""
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
from typing import Literal
|
||||
from logging import getLogger
|
||||
|
||||
from kotonebot.backend.preprocessor import HsvColorFilter
|
||||
from kotonebot.tasks.actions.loading import loading
|
||||
|
||||
from .. import R
|
||||
|
@ -14,7 +15,7 @@ from kotonebot import (
|
|||
sleep,
|
||||
Interval,
|
||||
)
|
||||
from ..game_ui import CommuEventButtonUI
|
||||
from ..game_ui import CommuEventButtonUI, WhiteFilter
|
||||
from .pdorinku import acquire_pdorinku
|
||||
from kotonebot.backend.dispatch import SimpleDispatcher
|
||||
from kotonebot.tasks.actions.commu import handle_unread_commu
|
||||
|
@ -55,7 +56,7 @@ def acquire_skill_card():
|
|||
def select_p_item():
|
||||
"""
|
||||
前置条件:P物品选择对话框(受け取るPアイテムを選んでください;)\n
|
||||
结束条件:P物品获取动画
|
||||
结束状态:P物品获取动画
|
||||
"""
|
||||
# 前置条件 [screenshots/produce/in_produce/select_p_item.png]
|
||||
# 前置条件 [screenshots/produce/in_produce/claim_p_item.png]
|
||||
|
@ -69,16 +70,16 @@ def select_p_item():
|
|||
sleep(0.5)
|
||||
device.click(ocr.expect_wait('受け取る'))
|
||||
|
||||
@action('技能卡强化', screenshot_mode='manual-inherit')
|
||||
@action('技能卡自选强化', screenshot_mode='manual-inherit')
|
||||
def hanlde_skill_card_enhance():
|
||||
"""
|
||||
前置条件:技能卡强化对话框\n
|
||||
结束条件:技能卡强化动画结束后瞬间
|
||||
结束状态:技能卡强化动画结束后瞬间
|
||||
|
||||
:return: 是否成功处理对话框
|
||||
"""
|
||||
# 前置条件 [kotonebot-resource\sprites\jp\in_purodyuusu\screenshot_skill_card_enhane.png]
|
||||
# 结束条件 [screenshots/produce/in_produce/skill_card_enhance.png]
|
||||
# 结束状态 [screenshots/produce/in_produce/skill_card_enhance.png]
|
||||
cards = image.find_multi([
|
||||
R.InPurodyuusu.A,
|
||||
R.InPurodyuusu.M
|
||||
|
@ -93,22 +94,52 @@ def hanlde_skill_card_enhance():
|
|||
device.screenshot()
|
||||
if image.find(R.InPurodyuusu.ButtonEnhance, colored=True):
|
||||
device.click()
|
||||
elif '強化' in ocr.ocr(rect=R.InPurodyuusu.BoxSkillCardEnhaced).squash().text:
|
||||
logger.debug("Enhance button found")
|
||||
elif image.find(R.InPurodyuusu.IconSkillCardEventBubble):
|
||||
device.click_center()
|
||||
logger.debug("Skill card event bubble found")
|
||||
break
|
||||
it.wait()
|
||||
logger.debug("Handle skill card enhance finished.")
|
||||
|
||||
@action('技能卡自选删除', screenshot_mode='manual-inherit')
|
||||
def handle_skill_card_removal():
|
||||
"""
|
||||
前置条件:技能卡删除对话框\n
|
||||
结束状态:技能卡删除动画结束后瞬间
|
||||
"""
|
||||
# 前置条件 [kotonebot-resource\sprites\jp\in_purodyuusu\screenshot_remove_skill_card.png]
|
||||
card = image.find_multi([
|
||||
R.InPurodyuusu.A,
|
||||
R.InPurodyuusu.M
|
||||
])
|
||||
if card is None:
|
||||
logger.info("No skill cards found")
|
||||
return False
|
||||
device.click(card)
|
||||
it = Interval()
|
||||
while True:
|
||||
device.screenshot()
|
||||
if image.find(R.InPurodyuusu.ButtonRemove):
|
||||
device.click()
|
||||
logger.debug("Remove button clicked.")
|
||||
elif image.find(R.InPurodyuusu.IconSkillCardEventBubble):
|
||||
device.click_center()
|
||||
logger.debug("Skill card event bubble found")
|
||||
break
|
||||
it.wait()
|
||||
logger.debug("Handle skill card removal finished.")
|
||||
|
||||
AcquisitionType = Literal[
|
||||
"PDrinkAcquire", # P饮料被动领取
|
||||
"PDrinkSelect", # P饮料主动领取
|
||||
"PDrinkMax", # P饮料到达上限
|
||||
"PSkillCardAcquire", # 技能卡领取
|
||||
"PSkillCardChange", # 技能卡更换
|
||||
"PSkillCardSelect", # 技能卡选择
|
||||
"PSkillCardEnhanced", # 技能卡强化
|
||||
"PSkillCardEnhanceSelect", # 技能卡强化选择
|
||||
"PSkillCardRemove", # 技能卡移除
|
||||
"PSkillCardEnhanceSelect", # 技能卡自选强化
|
||||
"PSkillCardRemoveSelect", # 技能卡自选删除
|
||||
"PSkillCardEvent", # 技能卡事件(随机强化、删除、更换)
|
||||
"PItemClaim", # P物品领取
|
||||
"PItemSelect", # P物品选择
|
||||
"Clear", # 目标达成
|
||||
|
@ -118,7 +149,7 @@ AcquisitionType = Literal[
|
|||
"Loading", # 加载画面
|
||||
]
|
||||
|
||||
@measure_time(file_path='logs/acquisition.time.log')
|
||||
@measure_time()
|
||||
@action('处理培育事件', screenshot_mode='manual')
|
||||
def acquisitions() -> AcquisitionType | None:
|
||||
"""处理行动开始前和结束后可能需要处理的事件,直到到行动页面为止"""
|
||||
|
@ -158,10 +189,10 @@ def acquisitions() -> AcquisitionType | None:
|
|||
logger.info("Leave button found")
|
||||
device.click(leave)
|
||||
return "PDrinkMax"
|
||||
if ocr.find(contains('報酬'), rect=R.InPurodyuusu.BoxPDrinkMaxConfirmTitle):
|
||||
if image.find(R.InPurodyuusu.TextPDrinkMaxConfirmTitle):
|
||||
logger.debug("PDrink max confirm found")
|
||||
device.screenshot()
|
||||
if ocr.find(contains('報酬'), rect=R.InPurodyuusu.BoxPDrinkMaxConfirmTitle):
|
||||
if image.find(R.InPurodyuusu.TextPDrinkMaxConfirmTitle):
|
||||
if confirm := image.find(R.Common.ButtonConfirm):
|
||||
logger.info("Confirm button found")
|
||||
device.click(confirm)
|
||||
|
@ -176,11 +207,16 @@ def acquisitions() -> AcquisitionType | None:
|
|||
device.click_center()
|
||||
return "PSkillCardAcquire"
|
||||
|
||||
# 技能卡强化选择
|
||||
# 技能卡自选强化
|
||||
if image.find(R.InPurodyuusu.IconTitleSkillCardEnhance):
|
||||
if hanlde_skill_card_enhance():
|
||||
return "PSkillCardEnhanceSelect"
|
||||
|
||||
# 技能卡自选删除
|
||||
if image.find(R.InPurodyuusu.IconTitleSkillCardRemoval):
|
||||
if handle_skill_card_removal():
|
||||
return "PSkillCardRemoveSelect"
|
||||
|
||||
# 目标达成
|
||||
logger.debug("Check gloal clear (達成)...")
|
||||
if image.find(R.InPurodyuusu.IconClearBlue):
|
||||
|
@ -190,7 +226,7 @@ def acquisitions() -> AcquisitionType | None:
|
|||
sleep(1)
|
||||
return "Clear"
|
||||
# 目标达成 NEXT
|
||||
if ocr.find(regex('NEXT|next'), rect=R.InPurodyuusu.BoxGoalClearNext):
|
||||
if image.find(R.InPurodyuusu.TextGoalClearNext, preprocessors=[WhiteFilter()]):
|
||||
logger.info("Goal clear (達成) next found")
|
||||
device.click_center()
|
||||
sleep(1)
|
||||
|
@ -218,53 +254,44 @@ def acquisitions() -> AcquisitionType | None:
|
|||
|
||||
# 物品选择对话框
|
||||
logger.debug("Check award select dialog...")
|
||||
if result := ocr.find(contains("受け取る"), rect=R.InPurodyuusu.BoxSelectPStuff):
|
||||
if image.find(R.InPurodyuusu.TextClaim):
|
||||
logger.info("Award select dialog found.")
|
||||
logger.debug(f"Dialog text: {result.text}")
|
||||
|
||||
# P饮料选择
|
||||
logger.debug("Check PDrink select...")
|
||||
if "Pドリンク" in result.text:
|
||||
if image.find(R.InPurodyuusu.TextPDrink):
|
||||
logger.info("PDrink select found")
|
||||
acquire_pdorinku(index=0)
|
||||
return "PDrinkSelect"
|
||||
# 技能卡选择
|
||||
logger.debug("Check skill card select...")
|
||||
if "スキルカード" in result.text:
|
||||
if image.find(R.InPurodyuusu.TextSkillCard):
|
||||
logger.info("Acquire skill card found")
|
||||
acquire_skill_card()
|
||||
return "PSkillCardSelect"
|
||||
# P物品选择
|
||||
logger.debug("Check PItem select...")
|
||||
if "Pアイテム" in result.text:
|
||||
if image.find(R.InPurodyuusu.TextPItem):
|
||||
logger.info("Acquire PItem found")
|
||||
select_p_item()
|
||||
return "PItemSelect"
|
||||
|
||||
# 技能卡变更事件
|
||||
# 包括下面这些:
|
||||
# 1. 技能卡更换
|
||||
# [screenshots/produce/in_produce/support_card_change.png]
|
||||
# 2. 技能卡强化
|
||||
# [screenshots/produce/in_produce/skill_card_enhance.png]
|
||||
# 3. 技能卡移除
|
||||
# [screenshots/produce/in_produce/skill_card_removal.png]
|
||||
logger.debug("Check skill card events...")
|
||||
if result := ocr.ocr(rect=R.InPurodyuusu.BoxSkillCardEnhaced).squash():
|
||||
# 技能卡更换(支援卡效果)
|
||||
# [screenshots/produce/in_produce/support_card_change.png]
|
||||
if "チェンジ" in result.text:
|
||||
logger.info("Change skill card found")
|
||||
device.click(*bottom_pos)
|
||||
return "PSkillCardChange"
|
||||
# 技能卡强化
|
||||
# [screenshots/produce/in_produce/skill_card_enhance.png]
|
||||
if "強化" in result.text:
|
||||
logger.info("Enhance skill card found")
|
||||
device.click(*bottom_pos)
|
||||
return "PSkillCardEnhanced"
|
||||
# 技能卡移除
|
||||
# [screenshots\produce\in_produce\skill_card_removal.png]
|
||||
if "削除" in result.text:
|
||||
logger.info("Remove skill card found")
|
||||
device.click(*bottom_pos)
|
||||
return "PSkillCardRemove"
|
||||
if image.find(R.InPurodyuusu.IconSkillCardEventBubble):
|
||||
device.click() # 不能 click_center,因为中间是技能卡
|
||||
return "PSkillCardEvent"
|
||||
|
||||
# 技能卡获取
|
||||
# [res/sprites/jp/in_purodyuusu/screenshot_skill_card_acquired.png]
|
||||
# [kotonebot-resource\sprites\jp\in_purodyuusu\screenshot_skill_card_acquired.png]
|
||||
# 因为这个文本有缩放动画,因此暂时没法用模板匹配代替
|
||||
if ocr.find("スキルカード獲得", rect=R.InPurodyuusu.BoxSkillCardAcquired):
|
||||
logger.info("Acquire skill card from loot box")
|
||||
device.click_center()
|
||||
|
|
|
@ -5,6 +5,7 @@ from cv2.typing import MatLike
|
|||
|
||||
|
||||
from .. import R
|
||||
from ..game_ui import WhiteFilter
|
||||
from kotonebot.util import Countdown, Interval
|
||||
from kotonebot import device, image, color, user, rect_expand, until, action, sleep, use_screenshot
|
||||
|
||||
|
@ -13,7 +14,7 @@ logger = logging.getLogger(__name__)
|
|||
|
||||
@action('检查是否处于交流')
|
||||
def is_at_commu():
|
||||
return image.find(R.Common.ButtonCommuFastforward) is not None
|
||||
return image.find(R.Common.ButtonCommuSkip, preprocessors=[WhiteFilter()]) is not None
|
||||
|
||||
@action('跳过交流')
|
||||
def skip_commu():
|
||||
|
@ -30,56 +31,32 @@ def handle_unread_commu(img: MatLike | None = None) -> bool:
|
|||
ret = False
|
||||
logger.info('Check and skip commu')
|
||||
img = use_screenshot(img)
|
||||
skip_btn = image.find(R.Common.ButtonCommuFastforward)
|
||||
skip_btn = image.find(R.Common.ButtonCommuFastforward, preprocessors=[WhiteFilter()])
|
||||
if skip_btn is None:
|
||||
logger.info('No fast forward button found. Not at a commu.')
|
||||
return ret
|
||||
|
||||
ret = True
|
||||
logger.debug('Fast forward button found. Check commu')
|
||||
button_bg_rect = rect_expand(skip_btn.rect, 10, 10, 50, 10)
|
||||
def is_fastforwarding():
|
||||
nonlocal img
|
||||
assert img is not None
|
||||
colors = color.raw().dominant_color(img, 2, rect=button_bg_rect)
|
||||
RANGE = ((20, 65, 95), (180, 100, 100))
|
||||
return any(color.raw().in_range(c, RANGE) for c in colors)
|
||||
|
||||
# 防止截图速度过快时,截图到了未加载完全的画面
|
||||
cd = Interval(seconds=0.6)
|
||||
hit = 0
|
||||
HIT_THRESHOLD = 2
|
||||
|
||||
it = Interval()
|
||||
while True:
|
||||
if image.find(R.Common.ButtonCommuFastforward) and not is_fastforwarding():
|
||||
logger.debug("Unread commu hit %d/%d", hit, HIT_THRESHOLD)
|
||||
hit += 1
|
||||
else:
|
||||
hit = 0
|
||||
if not is_at_commu():
|
||||
break
|
||||
if hit >= HIT_THRESHOLD:
|
||||
break
|
||||
cd.wait()
|
||||
img = device.screenshot()
|
||||
should_skip = hit >= HIT_THRESHOLD
|
||||
if not should_skip:
|
||||
logger.info('Fast forwarding. No action needed.')
|
||||
return False
|
||||
|
||||
if should_skip:
|
||||
user.info('发现未读交流', images=[img])
|
||||
logger.debug('Not fast forwarding. Click fast forward button')
|
||||
device.click(skip_btn)
|
||||
sleep(0.7)
|
||||
if image.wait_for(R.Common.ButtonConfirm, timeout=5):
|
||||
logger.debug('Click confirm button')
|
||||
if image.find(R.Common.ButtonCommuSkip, preprocessors=[WhiteFilter()]):
|
||||
device.click()
|
||||
else:
|
||||
logger.info('Fast forwarding. No action needed.')
|
||||
logger.debug('Wait until not at commu')
|
||||
# TODO: 这里改用 while 循环,避免点击未生效的情况
|
||||
until(lambda: not is_at_commu(), interval=0.3)
|
||||
logger.info('Fast forward done')
|
||||
logger.debug('Clicked skip button.')
|
||||
if image.find(R.Common.ButtonConfirm):
|
||||
logger.info('Unread commu found.')
|
||||
device.click()
|
||||
logger.debug('Clicked confirm button.')
|
||||
logger.debug('Pushing notification...')
|
||||
user.info('发现未读交流', images=[img])
|
||||
|
||||
logger.debug('Fast forwarding...')
|
||||
it.wait()
|
||||
|
||||
logger.info('Fast forward done.')
|
||||
return ret
|
||||
|
||||
|
||||
|
@ -87,12 +64,12 @@ 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)
|
||||
from kotonebot.backend.context import manual_context, inject_context
|
||||
from kotonebot.backend.debug.mock import MockDevice
|
||||
manual_context().begin()
|
||||
device = MockDevice()
|
||||
device.load_image(r"D:\current_screenshot.png")
|
||||
inject_context(device=device)
|
||||
print(is_at_commu())
|
||||
# rect = image.expect(R.Common.ButtonCommuFastforward).rect
|
||||
# print(rect)
|
||||
# rect = rect_expand(rect, 10, 10, 50, 10)
|
||||
# print(rect)
|
||||
# img = device.screenshot()
|
||||
# print(color.raw().dominant_color(img, 2, rect=rect))
|
||||
# skip_commu()
|
||||
# check_and_skip_commu()
|
||||
# while True:
|
||||
# print(handle_unread_commu())
|
||||
|
|
|
@ -13,12 +13,13 @@ from .. import R
|
|||
from . import loading
|
||||
from .scenes import at_home
|
||||
from ..util.trace import trace
|
||||
from ..game_ui import WhiteFilter
|
||||
from .commu import handle_unread_commu
|
||||
from ..common import ProduceAction, RecommendCardDetectionMode, conf
|
||||
from kotonebot.errors import UnrecoverableError
|
||||
from kotonebot.backend.context.context import use_screenshot
|
||||
from .common import until_acquisition_clear, acquisitions, commut_event
|
||||
from kotonebot.util import AdaptiveWait, Countdown, crop, cropped
|
||||
from kotonebot.util import AdaptiveWait, Countdown, Interval, crop, cropped
|
||||
from kotonebot.backend.dispatch import DispatcherContext, SimpleDispatcher
|
||||
from kotonebot import ocr, device, contains, image, regex, action, sleep, color, Rect, wait
|
||||
from .non_lesson_actions import (
|
||||
|
@ -677,6 +678,7 @@ def exam(type: Literal['mid', 'final']):
|
|||
while ocr.wait_for(contains("メモリー"), timeout=7):
|
||||
device.click_center()
|
||||
|
||||
# TODO: 将这个函数改为手动截图模式
|
||||
@action('考试结束流程')
|
||||
def produce_end():
|
||||
"""执行考试结束流程"""
|
||||
|
@ -690,12 +692,22 @@ def produce_end():
|
|||
# 等待选择封面画面 [screenshots/produce_end/select_cover.jpg]
|
||||
# 次へ
|
||||
logger.info("Waiting for select cover screen...")
|
||||
aw = AdaptiveWait(timeout=60 * 5, max_interval=20)
|
||||
it = Interval()
|
||||
while not image.find(R.InPurodyuusu.ButtonNextNoIcon):
|
||||
# device.screenshot()
|
||||
# 未读交流
|
||||
if handle_unread_commu():
|
||||
logger.info("Skipping unread commu")
|
||||
continue
|
||||
aw()
|
||||
# 跳过演出
|
||||
# [kotonebot-resource\sprites\jp\produce\screenshot_produce_end.png]
|
||||
elif image.find(R.Produce.ButtonSkipLive, preprocessors=[WhiteFilter()]):
|
||||
logger.info("Skipping live.")
|
||||
device.click()
|
||||
# [kotonebot-resource\sprites\jp\produce\screenshot_produce_end_skip.png]
|
||||
elif image.find(R.Produce.TextSkipLiveDialogTitle):
|
||||
logger.info("Confirming skip live.")
|
||||
device.click(image.expect_wait(R.Common.IconButtonCheck))
|
||||
it.wait()
|
||||
device.click(0, 0)
|
||||
# 选择封面
|
||||
logger.info("Use default cover.")
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
"""领取社团奖励,并尽可能地给其他人送礼物"""
|
||||
import logging
|
||||
|
||||
from kotonebot import task, device, image, sleep, ocr
|
||||
from . import R
|
||||
from .common import conf
|
||||
from .actions.scenes import at_home, goto_home
|
||||
from kotonebot.tasks.game_ui import toolbar_menu
|
||||
from kotonebot import task, device, image, sleep, ocr
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
@ -23,7 +24,7 @@ def club_reward():
|
|||
|
||||
# 进入社团UI
|
||||
logger.info('Entering club UI')
|
||||
device.click(image.expect_wait(R.Daily.IconMenu, timeout=5))
|
||||
device.click(toolbar_menu(True))
|
||||
device.click(image.expect_wait(R.Daily.IconMenuClub, timeout=5))
|
||||
sleep(3)
|
||||
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
from gc import enable
|
||||
import os
|
||||
import json
|
||||
import shutil
|
||||
|
|
|
@ -2,10 +2,9 @@
|
|||
import logging
|
||||
from gettext import gettext as _
|
||||
|
||||
from kotonebot.backend.dispatch import SimpleDispatcher
|
||||
|
||||
from . import R
|
||||
from .common import conf
|
||||
from .game_ui import WhiteFilter
|
||||
from .actions.scenes import at_home, goto_home
|
||||
from .actions.loading import wait_loading_end
|
||||
from kotonebot import device, image, ocr, color, action, task, user, rect_expand, sleep, contains
|
||||
|
@ -28,9 +27,9 @@ def goto_contest() -> bool:
|
|||
device.click(btn_contest)
|
||||
if not has_ongoing_contest:
|
||||
while not image.find(R.Daily.ButtonContestRanking):
|
||||
# [screenshots/contest/acquire1.png]
|
||||
# [kotonebot-resource\sprites\jp\daily\screenshot_contest_season_reward.png]
|
||||
# [screenshots/contest/acquire2.png]
|
||||
device.click(0, 0)
|
||||
device.click(R.Daily.PointDissmissContestReward)
|
||||
sleep(1)
|
||||
# [screenshots/contest/main.png]
|
||||
else:
|
||||
|
@ -93,8 +92,7 @@ def pick_and_contest(has_ongoing_contest: bool = False) -> bool:
|
|||
# 点击 SKIP
|
||||
sleep(3)
|
||||
logger.debug('Clicking on SKIP.')
|
||||
# TODO: 改为二值化图片
|
||||
device.click(image.expect(R.Daily.ButtonIconSkip, colored=True, transparent=True, threshold=0.999))
|
||||
device.click(image.expect(R.Daily.ButtonIconSkip, preprocessors=[WhiteFilter()]))
|
||||
while not image.wait_for(R.Common.ButtonNextNoIcon, timeout=2):
|
||||
device.click_center()
|
||||
logger.debug('Waiting for the result.')
|
||||
|
|
|
@ -1,16 +1,19 @@
|
|||
from dataclasses import dataclass
|
||||
from typing import Literal, NamedTuple
|
||||
from typing import Literal, NamedTuple, overload
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
from cv2.typing import MatLike
|
||||
|
||||
from kotonebot.backend.image import TemplateMatchResult
|
||||
|
||||
from . import R
|
||||
from kotonebot import action, device, color, image, ocr, sleep
|
||||
from kotonebot.backend.color import HsvColor
|
||||
from kotonebot.util import Rect
|
||||
from kotonebot.backend.core import HintBox, Image
|
||||
from kotonebot.backend.preprocessor import HsvColorFilter
|
||||
|
||||
import cv2
|
||||
from cv2.typing import MatLike
|
||||
|
||||
@action('按钮是否禁用', screenshot_mode='manual-inherit')
|
||||
def button_state(*, target: Image | None = None, rect: Rect | None = None) -> bool | None:
|
||||
|
@ -189,20 +192,51 @@ class CommuEventButtonUI:
|
|||
ocr_result = ocr.raw().ocr(img, rect=rects[0])
|
||||
return ocr_result.squash().text
|
||||
|
||||
def filter_white(img: MatLike):
|
||||
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
|
||||
lower_white = np.array([0, 0, 180])
|
||||
upper_white = np.array([180, 50, 255])
|
||||
return cv2.inRange(hsv, lower_white, upper_white)
|
||||
class WhiteFilter(HsvColorFilter):
|
||||
"""
|
||||
匹配时,只匹配图像和模板中的白色部分。
|
||||
|
||||
此类用于识别空心/透明背景的白色图标或文字。
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(WHITE_LOW, WHITE_HIGH)
|
||||
|
||||
@overload
|
||||
def toolbar_home(critical: Literal[False] = False) -> TemplateMatchResult | None:
|
||||
"""寻找工具栏上的首页按钮。"""
|
||||
...
|
||||
|
||||
@overload
|
||||
def toolbar_home(critical: Literal[True]) -> TemplateMatchResult:
|
||||
"""寻找工具栏上的首页按钮。若未找到,则抛出异常。"""
|
||||
...
|
||||
|
||||
# TODO: image 对象加入自定义 hook,处理 post-process 和 pre-process
|
||||
@action('工具栏按钮.寻找首页', screenshot_mode='manual-inherit')
|
||||
def toolbar_home():
|
||||
img = device.screenshot()
|
||||
img = filter_white(img)
|
||||
result = image.raw().find(img, R.Common.ButtonToolbarHomeBinary.binary())
|
||||
return result
|
||||
def toolbar_home(critical: bool = False):
|
||||
device.screenshot()
|
||||
if critical:
|
||||
return image.expect_wait(R.Common.ButtonToolbarHome, preprocessors=[WhiteFilter()])
|
||||
else:
|
||||
return image.find(R.Common.ButtonToolbarHome, preprocessors=[WhiteFilter()])
|
||||
|
||||
@overload
|
||||
def toolbar_menu(critical: Literal[False] = False) -> TemplateMatchResult | None:
|
||||
"""寻找工具栏上的菜单按钮。"""
|
||||
...
|
||||
|
||||
@overload
|
||||
def toolbar_menu(critical: Literal[True]) -> TemplateMatchResult:
|
||||
"""寻找工具栏上的菜单按钮。若未找到,则抛出异常。"""
|
||||
...
|
||||
|
||||
@action('工具栏按钮.寻找菜单', screenshot_mode='manual-inherit')
|
||||
def toolbar_menu(critical: bool = False):
|
||||
device.screenshot()
|
||||
if critical:
|
||||
return image.expect_wait(R.Common.ButtonToolbarMenu, preprocessors=[WhiteFilter()])
|
||||
else:
|
||||
return image.find(R.Common.ButtonToolbarMenu, preprocessors=[WhiteFilter()])
|
||||
|
||||
if __name__ == '__main__':
|
||||
from pprint import pprint as print
|
||||
|
|
|
@ -189,7 +189,7 @@ def do_produce(
|
|||
|
||||
device.screenshot()
|
||||
# 有进行中培育的情况
|
||||
if ocr.find(contains('プロデュース中'), rect=R.Produce.BoxProduceOngoing):
|
||||
if ocr.find(contains('中'), rect=R.Produce.BoxProduceOngoing):
|
||||
logger.info('Ongoing produce found. Try to resume produce.')
|
||||
resume_produce()
|
||||
return True
|
||||
|
|