77 */
88
99let scriptContext = {
10- version : "1.0 " ,
10+ version : "1.1 " ,
1111} ;
1212
1313// 原本是csv格式,但是为了方便js重用,还是内置在代码中
@@ -81,6 +81,8 @@ const csvText = `物品,刷新机制,背包分类
8181幽光星星,46小时,材料
8282月莲,46小时,材料
8383月落银,46小时,材料
84+ 冬凌草,46小时,材料
85+ 松珀香,46小时,材料
8486云岩裂叶,46小时,材料
8587灼灼彩菊,46小时,材料
8688子探测单元,46小时,材料
@@ -130,6 +132,9 @@ const csvText = `物品,刷新机制,背包分类
130132烛伞蘑菇,每天0点,食物
131133`
132134
135+ const renameMap = { "晶蝶" : "晶核" , "「冷鲜肉」" : "冷鲜肉" , "白铁矿" : "白铁块" , "铁矿" : "铁块" } ;
136+
137+ const supportFile = "native_supported.json" ;
133138const materialMetadata = { } ;
134139
135140function parseCsvTextToDict ( ) {
@@ -178,13 +183,110 @@ function parseCsvTextToDict() {
178183 return resultDict ;
179184}
180185
186+ /**
187+ * 获取需要手动扫描的材料及其对应路径
188+ * @returns {Object } 格式为 { "材料名": "文件完整路径" } 的对象
189+ */
190+ function getManualOcrMaterials ( ) {
191+ const scanDir = "assets/images/CustomScan" ;
192+ const items = file . ReadPathSync ( scanDir ) ;
193+
194+ // 1. 读取本地记录文件,获取已支持列表
195+ let native_supported = { } ;
196+ try {
197+ const content = readTextSync ( supportFile ) ;
198+ if ( content ) {
199+ native_supported = JSON . parse ( content ) ;
200+ }
201+ } catch ( error ) {
202+ log . debug ( `读取记录文件失败: ${ error . toString ( ) } ` ) ;
203+ }
204+
205+ // 2. 核心逻辑:建立材料名与路径的映射关系,并过滤已支持材料
206+ const manualDict = { } ;
207+ for ( const itemPath of items ) {
208+ // 提取材料名(不含扩展名)
209+ const itemName = splitExt ( basename ( itemPath ) ) [ 0 ] ;
210+ // 差集计算:只有当 BetterGI 不支持该材料时,才加入待扫描字典
211+ if ( ! native_supported . hasOwnProperty ( itemName ) ) {
212+ manualDict [ itemName ] = itemPath ;
213+ }
214+ }
215+
216+ return manualDict ;
217+ }
218+
219+ const manualOcrMaterials = getManualOcrMaterials ( ) ;
220+
221+ async function manualOcr ( normalizedItemList , shouldStop , sharedResults ) {
222+ // 1. 预加载:将所有待识别材料的模板初始化并存储,避免在循环中重复读取文件
223+ const templates = { } ;
224+ let remainingItems = [ ] ;
225+
226+ for ( const itemName of normalizedItemList ) {
227+ const mat = file . readImageMatSync ( manualOcrMaterials [ itemName ] ) ;
228+ const ro = RecognitionObject . TemplateMatch ( mat ) ;
229+ ro . threshold = 0.85 ;
230+ templates [ itemName ] = ro ;
231+ remainingItems . push ( itemName ) ; // 加入待扫描清单
232+ }
233+
234+ const _parseInt = ( value , defaultValue = 0 ) => {
235+ const parsed = parseInt ( value . trim ( ) , 10 ) ;
236+ return Number . isNaN ( parsed ) ? defaultValue : parsed ;
237+ } ;
238+ if ( remainingItems . length > 0 ) {
239+ log . info ( `将通过补充OCR识别{0}等{1}类物品的数量` , remainingItems [ 0 ] , remainingItems . length ) ;
240+ }
241+
242+ // 2. 扫描循环:直到所有材料都找到,或者当前画面不再有新目标
243+ while ( remainingItems . length > 0 && ! shouldStop ( ) ) {
244+ // 使用 withCapture 确保每一帧 region 都能正确 dispose
245+ const foundInThisFrame = await withCapture ( async ( region ) => {
246+ const foundList = [ ] ;
247+ for ( const itemName of remainingItems ) {
248+ const result = region . find ( templates [ itemName ] ) ;
249+ if ( result . isExist ( ) ) {
250+ // 计算 OCR 区域坐标, 抄了 吉吉喵 大佬的方法
251+ const tolerance = 1 ;
252+ const rect = {
253+ x : result . x - tolerance ,
254+ y : result . y + 97 - tolerance ,
255+ width : 66 + 2 * tolerance ,
256+ height : 22 + 2 * tolerance ,
257+ } ;
258+ // drawRegion(rect); // 调试用
259+ let ocrResult = region . find ( RecognitionObject . ocr ( rect . x , rect . y , rect . width , rect . height ) ) ;
260+ if ( ocrResult && ocrResult . text ) {
261+ const count = _parseInt ( ocrResult . text , - 2 ) ;
262+ sharedResults [ itemName ] = count ;
263+ log . info ( "{0}: {1}" , itemName , count ) ;
264+ foundList . push ( itemName ) ; // 记录本帧识别到的条目
265+ }
266+ }
267+ }
268+ return foundList ; // 将本帧找到的列表返回给外部
269+ } ) ;
270+
271+ // 3. 更新剩余待扫描列表
272+ if ( foundInThisFrame . length > 0 ) {
273+ remainingItems = remainingItems . filter ( ( item ) => ! foundInThisFrame . includes ( item ) ) ;
274+ }
275+ if ( remainingItems . length > 0 && ! shouldStop ( ) ) {
276+ await sleep ( 100 ) ; // 如果仍有剩余材料,短暂停顿
277+ } else {
278+ break ; // 全部找到,退出循环
279+ }
280+ }
281+
282+ return sharedResults ;
283+ }
284+
181285/**
182286 * 获取背包中物品的数量。
183287 * 如果没有找到,则为-1;如果找到了但数字识别失败,则为-2
184- *
185- * 暂不支持 冷鲜肉, 红果果菇, 奇异的「牙齿」
186288 */
187- async function getItemCount ( itemList = null , retry = true ) {
289+ async function getItemCount ( itemList = null ) {
188290 if ( Object . keys ( materialMetadata ) . length === 0 ) {
189291 Object . assign ( materialMetadata , parseCsvTextToDict ( ) ) ;
190292 }
@@ -194,17 +296,105 @@ async function getItemCount(itemList=null, retry=true) {
194296 } else if ( itemList == null || itemList . length === 0 ) {
195297 itemList = Object . keys ( materialMetadata ) ;
196298 }
197- const renameMap = { "晶蝶" : "晶核" , "「冷鲜肉」" :"冷鲜肉" } ;
299+ const normalizedItemList = itemList . map ( ( itemName ) => {
300+ return renameMap [ itemName ] || itemName ;
301+ } ) ;
302+ const result = await mainScan ( normalizedItemList ) ;
303+ return result ;
304+ }
305+
306+ /**
307+ * 合并扫描结果并实现特征库自动更新(自愈)
308+ * @param {Object } nativeResults - BetterGI 返回的结果 { "鸣草": 10, ... }
309+ * @param {Object } manualResults - 手动 OCR 返回的结果 { "新材料": 5, ... }
310+ * @param {string[] } targetManualList - 本次扫描中,原本判定为需要“手动处理”的材料名清单
311+ * @returns {Object } 合并后的最终清单
312+ */
313+ function mergeAndAutoRepair ( nativeResults , manualResults , manualCandidateList ) {
314+ // 1. 以原生结果为基础,合并手动结果
315+ const finalData = { ...nativeResults } ;
316+ const newlySupported = [ ] ;
317+
318+ for ( const name in manualResults ) {
319+ // 如果 nativeResults 里没有这个材料,采用手动结果
320+ if ( ! finalData [ name ] || finalData [ name ] < 0 ) {
321+ finalData [ name ] = manualResults [ name ] ;
322+ }
323+ }
324+
325+ // 2. 自愈检查:遍历原本以为需要手动的列表
326+ for ( const name of manualCandidateList ) {
327+ // 如果 BetterGI 的返回结果里包含了这个材料说明特征库更新了
328+ if ( nativeResults . hasOwnProperty ( name ) && nativeResults [ name ] > 0 ) {
329+ newlySupported . push ( name ) ;
330+ }
331+ }
332+
333+ // 3. 更新本地记录文件
334+ if ( newlySupported . length > 0 ) {
335+ updateNativeSupportedJson ( newlySupported ) ;
336+ }
337+
338+ return finalData ;
339+ }
340+
341+ /**
342+ * 更新本地受支持列表的持久化存储
343+ */
344+ function updateNativeSupportedJson ( newItems ) {
345+ let registry = { } ;
346+ try {
347+ registry = JSON . parse ( readTextSync ( supportFile ) || "{}" ) ;
348+ } catch ( e ) { }
349+
350+ const today = new Date ( ) . toISOString ( ) . split ( 'T' ) [ 0 ] ;
351+ newItems . forEach ( item => {
352+ registry [ item ] = today ;
353+ log . info ( `发现{0}已被内置接口支持,未来将跳过它的补充OCR` , item ) ;
354+ } ) ;
355+
356+ file . WriteTextSync ( supportFile , JSON . stringify ( registry , null , 2 ) ) ;
357+ }
358+
359+ async function mainScan ( normalizedItemList ) {
360+ const sharedOcrResults = { } ; // 创建共享对象
361+ let isApiFinished = false ;
362+
363+ const manualList = normalizedItemList . filter ( name => manualOcrMaterials . hasOwnProperty ( name ) ) ;
364+
365+ // 并发启动
366+ const apiPromise = getItemCountWithApi ( normalizedItemList , sharedOcrResults )
367+ . finally ( ( ) => isApiFinished = true ) ;
368+ const manualPromise = manualOcr ( manualList , ( ) => isApiFinished , sharedOcrResults ) ;
369+
370+ // 等待全部完成
371+ const [ apiResults , _ ] = await Promise . all ( [ apiPromise , manualPromise ] ) ;
372+
373+ // 最后合并结果(此时 sharedOcrResults 已经包含了所有 OCR 成功识别的内容)
374+ const results = mergeAndAutoRepair ( apiResults , sharedOcrResults , manualList ) ;
375+ const finalResults = { } ;
376+ const reverseRenameMap = Object . entries ( renameMap ) . reduce ( ( acc , [ key , value ] ) => {
377+ acc [ value ] = key ;
378+ return acc ;
379+ } , { } ) ;
380+ for ( const itemName of normalizedItemList ) {
381+ // 如果某个元素没有找到,则不会存在对应的键值,赋值为-1以保持与单个物品查找时一致的行为
382+ const originName = reverseRenameMap [ itemName ] || itemName ;
383+ finalResults [ originName ] = results [ itemName ] ?? - 1 ;
384+ }
385+ return finalResults ;
386+ }
387+
388+ async function getItemCountWithApi ( normalizedItemList , sharedOcrResults ) {
198389 const groupByType = { } ;
199- for ( const itemName of itemList ) {
390+ for ( const itemName of normalizedItemList ) {
200391 const metadata = materialMetadata [ itemName ] ;
201392 const itemType = metadata ?. type ?? "Materials" ;
202393 if ( ! metadata ?. type ) {
203394 log . warn ( "未查找到{0}所属的背包分类,默认它是{1}" , itemName , itemType ) ;
204395 }
205- const normalizedName = renameMap [ itemName ] || itemName ;
206396 groupByType [ itemType ] = groupByType [ itemType ] || [ ] ;
207- groupByType [ itemType ] . push ( normalizedName ) ;
397+ groupByType [ itemType ] . push ( itemName ) ;
208398 }
209399
210400 let results = { } ;
@@ -216,26 +406,38 @@ async function getItemCount(itemList=null, retry=true) {
216406 } ) ) ;
217407 Object . assign ( results , countResult ) ;
218408 }
219- if ( retry && itemList . some ( item => ! ( item in results ) ) ) {
409+
410+ // 逻辑:既不在 API 的结果里,也不在 OCR 已经实时识别到的结果里
411+ const missingItems = normalizedItemList . filter ( name =>
412+ ! ( name in results ) && ! ( name in sharedOcrResults )
413+ ) ;
414+ if ( missingItems . length > 0 ) {
220415 // 即使在白天,大多数情况也能识别成功,因此不作为常态机制,仅在失败时使用
221- log . info ( "部分物品识别失败 ,调整时间和视角后重试" ) ;
416+ log . info ( ` ${ missingItems . length } 个物品识别失败 ,调整时间和视角后重试` ) ;
222417 await genshin . returnMainUi ( ) ;
223418 await genshin . setTime ( 0 , 0 ) ;
224419 await sleep ( 300 ) ;
225420 moveMouseBy ( 0 , 9280 ) ;
226421 await sleep ( 300 ) ;
227- const retryResults = await getItemCount ( itemList , false ) ;
422+
423+ // 只针对缺失的物品进行重试
424+ for ( const type in groupByType ) {
425+ const namesToRetry = groupByType [ type ] . filter ( name => missingItems . includes ( name ) ) ;
426+ if ( namesToRetry . length > 0 ) {
427+ const retryCountResult = await dispatcher . runTask ( new SoloTask ( "CountInventoryItem" , {
428+ "gridScreenName" : type ,
429+ "itemNames" : namesToRetry ,
430+ } ) ) ;
431+ // 将重试结果合并到原始 results 中
432+ Object . assign ( results , retryCountResult ) ;
433+ }
434+ }
435+
436+ // 恢复视角
228437 await genshin . returnMainUi ( ) ;
229438 keyPress ( "MBUTTON" ) ;
230- return retryResults ;
231439 }
232- const finalResults = { } ;
233- for ( const itemName of itemList ) {
234- const normalizedName = renameMap [ itemName ] || itemName ;
235- // 如果某个元素没有找到,则不会存在对应的键值,赋值为-1以保持与单个物品查找时一致的行为
236- finalResults [ itemName ] = results [ normalizedName ] ?? - 1 ;
237- }
238- return finalResults ;
440+ return results ;
239441}
240442
241443function getItemCD ( itemName ) {
0 commit comments