feat: Better filter implement for OS shops.

This commit is contained in:
SaarChaffee 2023-09-20 14:26:08 +08:00
parent 7d6bb53c4f
commit 3f317c5176
No known key found for this signature in database
GPG Key ID: B20CD458B7DBA455
23 changed files with 265 additions and 114 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View File

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View File

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View File

@ -1617,7 +1617,9 @@
"ServerUpdate": "00:00"
},
"OpsiShop": {
"BuySupply": true
"BuySupply": true,
"PresetFilter": "max_benefit_meta",
"CustomFilter": "Logger"
},
"Storage": {
"Storage": {}

View File

@ -45,7 +45,7 @@ class Filter:
"""
Args:
objs (list): List of objects and strings
func (callable): A function to filter object.
List[func]|func (callable): A function or a list of funciton that to filter object.
Function should receive an object as arguments, and return a bool.
True means add it to output.
@ -68,6 +68,9 @@ class Filter:
for obj in objs:
if isinstance(obj, str):
out.append(obj)
elif isinstance(func, list):
if all(f(obj) for f in func):
out.append(obj)
elif func(obj):
out.append(obj)
else:

View File

@ -8076,6 +8076,21 @@
"BuySupply": {
"type": "checkbox",
"value": true
},
"PresetFilter": {
"type": "select",
"value": "max_benefit_meta",
"option": [
"max_benefit",
"max_benefit_meta",
"no_meta",
"all",
"custom"
]
},
"CustomFilter": {
"type": "textarea",
"value": "Logger"
}
},
"Storage": {

View File

@ -622,6 +622,16 @@ OpsiExplore:
LastZone: 0
OpsiShop:
BuySupply: true
PresetFilter:
value: max_benefit_meta
option:
- max_benefit
- max_benefit_meta
- no_meta
- all
- custom
CustomFilter: |-
Logger
OpsiVoucher:
Filter: |-
LoggerAbyssal > LoggerObscure > Book > Coin > Fragment

View File

@ -381,6 +381,8 @@ class GeneratedConfig:
# Group `OpsiShop`
OpsiShop_BuySupply = True
OpsiShop_PresetFilter = 'max_benefit_meta' # max_benefit, max_benefit_meta, no_meta, all, custom
OpsiShop_CustomFilter = 'Logger'
# Group `OpsiVoucher`
OpsiVoucher_Filter = 'LoggerAbyssal > LoggerObscure > Book > Coin > Fragment'

View File

@ -2215,6 +2215,19 @@
"BuySupply": {
"name": "Buy From Port Shops",
"help": "Buy all items from port shops\nShop inventory consists of a fixed pool that is reset monthly, items not bought during a cycle has the chance of re-appearing and blocking preferable high value items"
},
"PresetFilter": {
"name": "OpSi Shop Filter",
"help": "Generally does not need to be modified. Use \"High value items and META material\"` if Voucher Coins spilled or \"Without META materials\" if you are newcomer.\nHigh value items include ActionPoint, Logger and T4 or higher items; META materials include METABook and META enhance materials.",
"max_benefit": "High value items",
"max_benefit_meta": "High value items and META materials",
"no_meta": "Not to buy META materials",
"all": "All",
"custom": "custom"
},
"CustomFilter": {
"name": "Custom Research Priority",
"help": "To use your own filter, set \"OpSi Shop Filter Select\" to \"custom\". All options have been defined at <https://github.com/LmeSzinc/AzurLaneAutoScript/wiki/filter_string_en>"
}
},
"OpsiVoucher": {

View File

@ -2215,6 +2215,19 @@
"BuySupply": {
"name": "OpsiShop.BuySupply.name",
"help": "OpsiShop.BuySupply.help"
},
"PresetFilter": {
"name": "OpsiShop.PresetFilter.name",
"help": "OpsiShop.PresetFilter.help",
"max_benefit": "max_benefit",
"max_benefit_meta": "max_benefit_meta",
"no_meta": "no_meta",
"all": "all",
"custom": "custom"
},
"CustomFilter": {
"name": "OpsiShop.CustomFilter.name",
"help": "OpsiShop.CustomFilter.help"
}
},
"OpsiVoucher": {

View File

@ -2215,6 +2215,19 @@
"BuySupply": {
"name": "购买港口商店",
"help": "每月港口商店可购买商品是固定的,未购买的物品下次仍会出现,并阻塞高价值物品,因此需要购买全部"
},
"PresetFilter": {
"name": "港口商店过滤器",
"help": "一般默认即可,白票溢出建议选 \"高价值物品与META材料\",萌新建议选 \"不购买META材料\"这会买除了META材料以外的所有东西。\n高价值物品为行动力、坐标和金或者更高级別的商品META材料为META通用战术教材和4种强化材料。",
"max_benefit": "高价值物品",
"max_benefit_meta": "高价值物品与META材料",
"no_meta": "不购买META材料",
"all": "全部",
"custom": "自定义"
},
"CustomFilter": {
"name": "自定义过滤器",
"help": "使用自定义过滤器需将 \"港口商店过滤器\" 设置为 \"自定义\",并阅读 https://github.com/LmeSzinc/AzurLaneAutoScript/wiki/filter_string_cn"
}
},
"OpsiVoucher": {

View File

@ -2215,6 +2215,19 @@
"BuySupply": {
"name": "購買港口商店",
"help": "每月港口商店可購買商品是固定的,未購買的物品下次仍會出現,並阻擋高價值物品,因此需要購買全部"
},
"PresetFilter": {
"name": "港口商店過濾器",
"help": "一般用預設即可,白票溢出建議選 \"高價值物品與META材料\",新手建議選 \"不購買META材料\"這會買除了META材料以外的所有東西。\n高價值物品為行動點、坐標和金或者更高級別的物品META材料為META通用戰術教材和4種強化材料。",
"max_benefit": "高價值物品",
"max_benefit_meta": "高價值物品與META材料",
"no_meta": "不購買META材料",
"all": "全部",
"custom": "自定義過濾器"
},
"CustomFilter": {
"name": "自定義過濾器",
"help": "使用自定義過濾器需將 \"港口商店過濾器\" 設定為 \"自定義\",並閱讀 https://github.com/LmeSzinc/AzurLaneAutoScript/wiki/filter_string_cn"
}
},
"OpsiVoucher": {

View File

@ -0,0 +1,18 @@
OS_SHOP = {
'max_benefit': """
ActionPoint > DevelopmentMaterialT3 > GearDesignPlan > GearPart > Logger > OrdnanceTestingReport > PlateRandom
""",
'max_benefit_meta': """
ActionPoint > DevelopmentMaterialT3 > GearDesignPlan > GearPart > Logger > OrdnanceTestingReport > PlateRandom >
METARedBook > CrystallizedHeatResistantSteel > NanoceramicAlloy > NeuroplasticProstheticArm > SupercavitationGenerator
""",
'no_meta':"""
ActionPoint > DevelopmentMaterial > EnergyStorageDevice > GearDesignPlan > GearPart >
Logger > PurpleCoins > PlateRandom > RepairPack > Turing > TuringSample
""",
'all': """
ActionPoint > CrystallizedHeatResistantSteel > DevelopmentMaterial > EnergyStorageDevice > GearDesignPlan > GearPart >
Logger > METARedBook > NanoceramicAlloy > NeuroplasticProstheticArm > OrdnanceTestingReport > PurpleCoins > PlateRandom >
RepairPack > SupercavitationGenerator > Turing > TuringSample
"""
}

View File

@ -0,0 +1,124 @@
import re
from module.config.config_generated import GeneratedConfig
from module.os_handler.preset import *
from module.base.filter import Filter
FILTER_REGEX = re.compile(
'^(actionpoint|crystallizedheatresistantsteel|developmentmaterial'
'|energystoragedevice|geardesignplan|gearpart|logger|metaredbook'
'|nanoceramicalloy|neuroplasticprostheticarm|ordnancetestingreport'
'|platerandom|purplecoins|repairpack|supercavitationgenerator|turingsample'
'|turings)'
'(20|50|100|prototype|specialized|abyssal|obscure|full2|full|triple2|triple|2'
'|combat|offence|survival)?'
'(t[1-6])?$',
flags=re.IGNORECASE)
FILTER_ATTR = ('group', 'sub_genre', 'tier')
FILTER = Filter(FILTER_REGEX, FILTER_ATTR)
class Selector():
prise_yellow_coin = 0
prise_purple_coin = 0
def clear_prise(self):
self.prise_yellow_coin = 0
self.prise_purple_coin = 0
def pretreatment(self, items) -> list:
"""
Pretreatment items.
Args:
items:
Returns:
list[Item]:
"""
_items = []
for item in items:
item.group, item.sub_genre, item.tier = None, None, None
result = re.search(FILTER_REGEX, item.name.lower())
if result:
item.group, item.sub_genre, item.tier = [group.lower()
if group is not None else None
for group in result.groups()]
_items.append(item)
return _items
def enough_coins(self, item) -> bool:
"""
Check if there are enough coins to buy the item.
Args:
item:
Returns:
bool: True if there are enough coins.
"""
if item.cost == 'YellowCoins' and self.prise_yellow_coin + item.price <= \
self._shop_yellow_coins - (self.config.OS_CL1_YELLOW_COINS_PRESERVE if self.is_cl1_enabled else 0):
self.prise_yellow_coin += item.price
return True
if item.cost == 'PurpleCoins' and self.prise_purple_coin + item.price <= self._shop_purple_coins:
self.prise_purple_coin += item.price
return True
return False
def check_cl1_purple_coins(self, item) -> bool:
"""
Check if cl1 is enable and item name is PurpleCoins.
Args:
item:
Returns:
bool: False if cl1 is enable and item name is PurpleCoins.
"""
return not (self.is_cl1_enabled and item.name == 'PurpleCoins')
def items_filter_in_akashi_shop(self, items) -> list:
"""
Returns items that can be bought.
Args:
items: Irems to be filtered.
Returns:
list[Item]:
"""
self.clear_prise()
items = self.pretreatment(items)
parser = self.config.OpsiGeneral_AkashiShopFilter
if not parser.strip():
parser = GeneratedConfig.OpsiGeneral_AkashiShopFilter
FILTER.load(parser)
return FILTER.apply(items, func=self.enough_coins)
def items_filter_in_os_shop(self, items) -> list:
"""
Returns items that can be bought.
Args:
items: Items to be filtered.
Returns:
list[Item]:
"""
self.clear_prise()
items = self.pretreatment(items)
preset = self.config.OpsiShop_PresetFilter
parser = ''
if preset == 'custom':
parser = self.config.OpsiShop_CustomFilter
if not parser.strip():
parser = OS_SHOP[GeneratedConfig.OpsiShop_PresetFilter]
else:
parser = OS_SHOP[preset]
FILTER.load(parser)
return FILTER.apply(items, func=[self.enough_coins, self.check_cl1_purple_coins])

View File

@ -8,8 +8,9 @@ from module.os_handler.assets import *
from module.os_handler.map_event import MapEventHandler
from module.os_handler.os_status import OSStatus
from module.os_handler.ui import OSShopUI
from module.statistics.item import ItemGrid
from module.os_handler.selector import Selector
from module.base.decorator import Config
from module.statistics.item import ItemGrid
from module.ui.scroll import Scroll
from module.shop.assets import AMOUNT_MAX, AMOUNT_MINUS, AMOUNT_PLUS, SHOP_BUY_CONFIRM_AMOUNT, SHOP_BUY_CONFIRM as OS_SHOP_BUY_CONFIRM
from module.shop.clerk import OCR_SHOP_AMOUNT
@ -37,7 +38,7 @@ class OSShopPrice(DigitYuv):
return result
class OSShopHandler(OSStatus, OSShopUI, MapEventHandler):
class OSShopHandler(OSStatus, OSShopUI, Selector, MapEventHandler):
_shop_yellow_coins = 0
_shop_purple_coins = 0
@ -46,6 +47,13 @@ class OSShopHandler(OSStatus, OSShopUI, MapEventHandler):
self._shop_purple_coins = self.get_purple_coins()
logger.info(f'Yellow coins: {self._shop_yellow_coins}, purple coins: {self._shop_purple_coins}')
@Config.when(SERVER='tw')
def os_shop_get_coins_in_os_shop(self):
self._shop_yellow_coins = self.get_yellow_coins()
self._shop_purple_coins = self.get_purple_coins()
logger.info(f'Yellow coins: {self._shop_yellow_coins}, purple coins: {self._shop_purple_coins}')
@Config.when(SERVER=None)
def os_shop_get_coins_in_os_shop(self):
self._shop_yellow_coins = self.get_yellow_coins()
self._shop_purple_coins = self.get_purple_coins_in_os_shop()
@ -53,7 +61,7 @@ class OSShopHandler(OSStatus, OSShopUI, MapEventHandler):
@cached_property
@Config.when(SERVER='tw')
def os_shop_items(self):
def os_shop_items(self) -> ItemGrid:
"""
Returns:
ItemGrid:
@ -69,7 +77,7 @@ class OSShopHandler(OSStatus, OSShopUI, MapEventHandler):
@cached_property
@Config.when(SERVER='en')
def os_shop_items(self):
def os_shop_items(self) -> ItemGrid:
"""
Returns:
ItemGrid:
@ -85,7 +93,7 @@ class OSShopHandler(OSStatus, OSShopUI, MapEventHandler):
@cached_property
@Config.when(SERVER=None)
def os_shop_items(self):
def os_shop_items(self) -> ItemGrid:
"""
Returns:
ItemGrid:
@ -207,55 +215,27 @@ class OSShopHandler(OSStatus, OSShopUI, MapEventHandler):
"""
costs = self._get_os_shop_cost()
items = []
for cost in costs:
shop_items = self._get_os_shop_items(cost)
if self.config.SHOP_EXTRACT_TEMPLATE:
shop_items.extract_template(self.device.image, './assets/shop/os')
shop_items.predict(self.device.image, name=name, amount=name, cost=True, price=True)
shop_items = shop_items.items
if len(shop_items):
row = [str(item) for item in shop_items]
logger.info(f'Shop items found: {row}')
items += self.items_filter_in_os_shop(shop_items)
items += shop_items
else:
logger.info('No shop items found')
return items
def items_filter_in_os_shop(self, items) -> list:
def os_shop_get_item_to_buy_in_akashi(self) -> list:
"""
Returns items that can be bought.
Args:
items: Items to be filtered.
Returns:
list[Item]:
"""
_items = []
prise_yellow_coin = 0
prise_purple_coin = 0
for item in items:
if self.is_cl1_enabled and item.name == 'PurpleCoins':
continue
if item.cost == 'YellowCoins' and prise_yellow_coin + item.price <= \
self._shop_yellow_coins - (self.config.OS_CL1_YELLOW_COINS_PRESERVE if self.is_cl1_enabled else 0):
prise_yellow_coin += item.price
_items.append(item)
continue
if item.cost == 'PurpleCoins' and prise_purple_coin + item.price <= self._shop_purple_coins:
prise_purple_coin += item.price
_items.append(item)
continue
return _items
def os_shop_get_item_to_buy_in_akashi(self):
"""
Returns:
Item:
"""
self.os_shop_get_coins()
items = self.os_shop_get_items_in_akashi(name=True)
# Shop supplies do not appear immediately, need to confirm if shop is empty.
@ -269,40 +249,8 @@ class OSShopHandler(OSStatus, OSShopUI, MapEventHandler):
else:
break
try:
selection = self.config.OpsiGeneral_AkashiShopFilter.replace(' ', '').replace('\n', '').split('>')
except Exception:
logger.warning(f'Invalid OS akashi buy filter string: {self.config.OpsiGeneral_AkashiShopFilter}')
return None
return self.items_filter_in_os_shop(items)
# TODO: Better filter for Akashi shop.
for select in selection:
for item in items:
if select not in item.name:
continue
if item.cost == 'YellowCoins':
if item.price > self._shop_yellow_coins:
continue
if item.cost == 'PurpleCoins':
if item.price > self._shop_purple_coins:
continue
return item
return None
@Config.when(SERVER='tw')
def os_shop_get_item_to_buy_in_port(self):
"""
Returns:
Item:
"""
self.os_shop_get_coins()
logger.attr('CL1 enabled', self.is_cl1_enabled)
return self.os_shop_get_items(name=True)
@Config.when(SERVER=None)
def os_shop_get_item_to_buy_in_port(self) -> list:
"""
Returns:
@ -320,12 +268,12 @@ class OSShopHandler(OSStatus, OSShopUI, MapEventHandler):
items = self.os_shop_get_items(name=True)
continue
else:
self.os_shop_items.items = items
return items
self.os_shop_items.items = self.items_filter_in_os_shop(items)
return self.os_shop_items.items
return []
def os_shop_buy_execute(self, button, skip_first_screenshot=True):
def os_shop_buy_execute(self, button, skip_first_screenshot=True) -> bool:
"""
Args:
button: Item to buy
@ -378,32 +326,7 @@ class OSShopHandler(OSStatus, OSShopUI, MapEventHandler):
return success
def os_shop_buy(self, select_func):
"""
Args:
select_func:
Returns:
int: Items bought.
Pages:
in: PORT_SUPPLY_CHECK
"""
count = 0
for _ in range(12):
button = select_func()
if button is None:
logger.info('Shop buy finished')
return count
else:
self.os_shop_buy_execute(button)
count += 1
continue
logger.warning('Too many items to buy, stopped')
return count
def os_shop_buy_2(self, select_func) -> int:
def os_shop_buy(self, select_func) -> int:
"""
Args:
select_func:
@ -422,7 +345,7 @@ class OSShopHandler(OSStatus, OSShopUI, MapEventHandler):
logger.info('No items need to be purchased')
continue
for button in buttons:
if count >= 5:
if count >= 10:
logger.info('Shop buy finished')
return count
else:
@ -453,7 +376,7 @@ class OSShopHandler(OSStatus, OSShopUI, MapEventHandler):
if item.cost == 'YellowCoins' else self._shop_purple_coins
if (_currency < item.price):
return False
if self.appear(AMOUNT_MAX, offset=(50, 50)):
limit = None
for _ in range(3):
@ -472,11 +395,11 @@ class OSShopHandler(OSStatus, OSShopUI, MapEventHandler):
diff = limit - total
if diff > 0:
limit = total
self.ui_ensure_index(limit, letter=OCR_SHOP_AMOUNT, prev_button=AMOUNT_MINUS, next_button=AMOUNT_PLUS,
skip_first_screenshot=True)
self.device.click(SHOP_BUY_CONFIRM_AMOUNT)
skip_first_screenshot=True)
self.appear_then_click(OS_SHOP_BUY_CONFIRM, offset=(20, 20))
self.appear_then_click(SHOP_BUY_CONFIRM_AMOUNT, offset=(20, 20))
return True
@ -503,23 +426,25 @@ class OSShopHandler(OSStatus, OSShopUI, MapEventHandler):
Pages:
in: PORT_SUPPLY_CHECK
"""
_count = 0
for i in range(4):
count = 0
self.os_shop_side_navbar_ensure(upper=i + 1)
OS_SHOP_SCROLL.set_top(main=self)
self.os_shop_side_navbar_ensure(bottom=i + 1)
OS_SHOP_SCROLL.set_bottom(main=self)
while True:
count += self.os_shop_buy_2(select_func=self.os_shop_get_item_to_buy_in_port)
if count >= 5:
count += self.os_shop_buy(select_func=self.os_shop_get_item_to_buy_in_port)
if count >= 10:
break
elif OS_SHOP_SCROLL.at_bottom(main=self):
elif OS_SHOP_SCROLL.at_top(main=self):
logger.info('OS shop reach bottom, stop')
break
else:
OS_SHOP_SCROLL.next_page(main=self, page=0.66)
OS_SHOP_SCROLL.prev_page(main=self, page=0.66)
continue
_count += count
return count > 0 or len(self.os_shop_items.items) == 0
return _count > 0 or len(self.os_shop_items.items) == 0
def handle_akashi_supply_buy(self, grid):
"""