Skip to content

Conversation

@guoh064
Copy link
Contributor

@guoh064 guoh064 commented Sep 25, 2025

尝试适配活动商店。希望这次review别烂尾了。

@nEEtdo0d
Copy link
Collaborator

seems overkill, almost all shop UIs are the same. I think can almost use MedalShop_date wholesale since its got that special scroll bar setup, overload a few functions, and a dedicated event shop folder.

think back then event shop were a hassle, but maybe a little bit easier now

@guoh064
Copy link
Contributor Author

guoh064 commented Sep 28, 2025

seems overkill, almost all shop UIs are the same. I think can almost use MedalShop_date wholesale since its got that special scroll bar setup, overload a few functions, and a dedicated event shop folder.

The dedicated buying logic for event shop is to deal with the problem that event pt is precious (meaning one cannot buy all the items easily). Current logic is to buy and stop whenever we cannot afford all. This makes it more possible to successfully buy all the things you want, and make it possible to change EventPtLimit in the future if wanted. However this also make it necessary to scan the whole shop, record all items and then judge.
The other parts are really a combination of MedalShop and OpsiPortShop I think, but the absence of pt template and the low resolution means cost_template matching mechanism may not work well in event shop.

think back then event shop were a hassle, but maybe a little bit easier now

I don’t think the difficulty drops that much, but of course you can have your logic and implementation. I am not sure how you will implement it but have fun!

@nEEtdo0d
Copy link
Collaborator

nEEtdo0d commented Sep 28, 2025

The dedicated buying logic for event shop is to deal with the problem that event pt is precious (meaning one cannot buy all the items easily). Current logic is to buy and stop whenever we cannot afford all.

Ah good catch, you are right. I overlooked that. Especially with UR trade in, sometimes you don't need all 300 but that interface just uses the hit max button. to do that haha But that might be exception case where user has to do it themselves.

I am not sure how you will implement it but have fun!

Haha actually have no idea how the OpSi port shop does its thing and able to memorize positions especially with scroll bar movement in mind. I did implement a version that just relied on buying everything in the last week right before the port shop expired, that has its own flaws too but worked pretty well. I would think the same principle could work for events too just maybe using a custom date to configure then increment refresh up until the end of event.

Cool none the less, this is definitely a large feature. Maybe making this its own task might be less confusing and that way its attached to event category and can be enabled or disabled automatically only when the event task is enabled. Or can just rely on user to do so manually.

@guoh064
Copy link
Contributor Author

guoh064 commented Sep 28, 2025

Maybe making this its own task might be less confusing and that way its attached to event category and can be enabled or disabled automatically only when the event task is enabled.

Good idea! But let's wait until this pull request makes it way into Alas or Lme asked for it. It is usually the opposite case (at least for me) where he (she) loses interest in the halfway and never reviews the PR any more.

Comment on lines 553 to 561
CustomFilter: |-
EquipUR > EquipSSR > GachaTicket
> DR > PR > Array > Chip > CatT3
> Meta > SkinBox
> Oil > Coin > FoodT1
> AugmentCore > AugmentEnhanceT2 > AugmentChangeT2 > AugmentChangeT1
> Cube > Medal > ExpBookT1
> CatT2 > CatT1 > PlateGeneralT3 > PlateT3 > BoxT4
> ShipSSR
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ShipSSR 在未获取的情况下应该是第一个兑换的

Copy link
Contributor Author

@guoh064 guoh064 Sep 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这是自选过滤器,如果不满意可以修改。
另见下面对购买部分逻辑的说明。
这里也没有写ShipUR,因为这些都是特殊优先处理的。

Comment on lines +73 to +83
while ships_to_buy:
urpt_needed = sum([item.price for item in ships_to_buy])
if current_urpt >= urpt_needed:
for item in ships_to_buy:
self.event_shop_buy_item(item)
logger.info(f"Successfully bought ship items: {[str(item) for item in ships_to_buy]}")
break
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

筛选部分的代码和购买部分的耦合太严重了。参考大世界商店的实现,先进行一轮完整的物品扫描,然后计算出准备购买的指导列表,记住物品的相对顺序,最后遍历指导列表尝试购买

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

185行进行了完整的物品扫描。

参照191行至206行,这里主要进行准备购买的列表的计算。
有几个问题导致不能直接一次得出所有物品的相对顺序,而是需要动态调整:

  1. 对于用金pt购买的彩pt,相对顺序就比较难处理:比如说不知道该不该买,应该买多少个,会不会买多了;
  2. 对于用彩pt购买的金币,只靠过滤器很容易放在中间位置,而实际上一般不应该优先购买,需要特殊处理。
  3. 对于未解锁的金pt金船,购买金船之后金pt金船的优先级就降低了,所以应当置后;

综上,目前采用了三步走的方式,首先处理彩pt对应的物品(handle_items_related_with_urpt,处理彩船彩pt和金币),其次处理未解锁的金船(handle_unobtained_items,会将买剩下的金船扔回过滤器),最后再按照过滤器过滤剩余商品。

209行开始对物品的指导列表进行遍历和购买。

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. 中间代币应该是一个内部状态,不应该存在于过滤器中,脚本的作用是抹平这个故意恶心人的中间代币。进行首轮扫描后就可以计算需要换多少中间代币,然后插入到指导列表中的彩船之前,如果需要的pt不足以兑换指导的中间代币数量,就认为是pt不足,跳过列表中后续所有需要pt的项目
  2. 角色应该也有variant,类似 ShipUR-unobtained, ShipUR-obtained, ShipUR, ShipSSR也有类似变体,就可以在过滤器中筛选属性了

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

中间代币应该是一个内部状态,不应该存在于过滤器中,脚本的作用是抹平这个故意恶心人的中间代币。

同意,脚本应当抹平中间代币的作用。但目前中间代币并不参与最后的过滤器的筛选。

角色应该也有variant,类似 ShipUR-unobtained, ShipUR-obtained, ShipUR, ShipSSR也有类似变体,就可以在过滤器中筛选属性了

带变体的话那就需要对过滤器做防呆处理了,比如说同种舰船的obtained变体不能出现在unobtained的前面,做这些处理方式的代价我觉得比目前处理的方式要高,不做的话则会导致购买结果和玩家意愿不符。目前采用的方式是类似通用商店购买外观装备箱的方式,在处理unobtained变体时如果当前已经解锁该舰船则直接交给过滤器进行过滤。

进行首轮扫描后就可以计算需要换多少中间代币,然后插入到指导列表中的彩船之前

过滤器无法判断中间代币购买的金币和普通代币购买的金币之间的区别,不做特殊判断的话会出现提前购买中间代币购买的金币的问题。做特殊判断的话那就不如直接利用目前的逻辑进行判断了。

总的来说,目前计算出的指导列表的效果等价于你所设想的全部用过滤器表示的结果:(URpt >) ShipUR > ShipSSR-unobtained > ... > (URpt >) Coin_URpt,这里省略了现有过滤器里面的全部内容。
这种处理方式的优点在于鲁棒性强(都依靠逻辑判断),能减少用户的学习成本。

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

不一样,购买物品必须只能有一次循环,每买一个物品都需要重新识别当前页面的物品、重新识别货币数量、重新生成指导列表,然后购买列表中的只第一个物品,这个逻辑已经非常固定了,参考ShopClerk.shop_buy。
不能持有一个固定的列表然后挨个pop然后购买,不能认为物品列表在购买之后是不变的,不能认为货币数量必然会减少预期的数量。

    def shop_buy(self):
        for _ in range(12):
            logger.hr('Shop buy', level=2)
            items = self.shop_get_items()
            self.shop_currency()
            if self._currency <= 0:
                logger.warning(f'Current funds: {self._currency}, stopped')
                return False

            item = self.shop_get_item_to_buy(items)
            if item is None:
                logger.info('Shop buy finished')
                return True
            else:
                self.shop_buy_execute(item)
                continue

        logger.warning('Too many items to buy, stopped')
        return True

第一次扫描会得到一个全局列表,主要用来确定物品的顺序,然后每次购买物品前局部更新物品信息,如果不在当前页面那么滑动,如果发送了滑动重新更新局部物品信息。
ShipUR-obtained 不能在 ShipUR-unobtained,否则打印warning然后忽略,这不需要多少代码

Copy link
Contributor Author

@guoh064 guoh064 Oct 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

购买物品必须只能有一次循环

购买的时候只有一个循环,绝大多数物品也只会被遍历一次。

每买一个物品都需要重新识别当前页面的物品、重新识别货币数量、重新生成指导列表,然后购买列表中的只第一个物品,这个逻辑已经非常固定了,参考ShopClerk.shop_buy。

这是勋章商店的逻辑,只在单页面里面排序和生成指导列表,相当于只看局部,而只看局部相当于假定高价值物品都在第一行,这与实际可能的过滤器不符。目前按照大世界港口商店的逻辑,扫描多页内容后生成完整的指导列表再购买。

不赞同使用勋章商店及现有小卖部其他商店的逻辑。活动商店不是勋章商店,不能假定玩家拥有足够代币来搬空所有需要的物品

不能持有一个固定的列表然后挨个pop然后购买

我寻思夏克菲当年写港口商店就是这么写的:

def handle_port_supply_buy(self) -> bool:
"""
Returns:
bool: True if success to buy any or no items found.
False if not enough coins to buy any.
Pages:
in: PORT_SUPPLY_CHECK
"""
items = self.scan_all()
if not len(items):
logger.warning('Empty OS shop.')
return False
items = self.items_filter_in_os_shop(items)
if not len(items):
logger.warning('Nothing to buy.')
return False
self.os_shop_get_coins()
skip_get_coins = True
items.reverse()
count = 0
while len(items):
logger.hr('OpsiShop buy', level=2)
item = items.pop()
if not skip_get_coins:
self.os_shop_get_coins()
if item.price > self.get_currency_coins(item):
logger.info(f'Not enough coins to buy item: {item.name}, skip.')
if self.is_coins_both_not_enough():
logger.info('Not enough coins to buy any items, stop.')
break
continue
logger.info(f'Buying item: {item.name}. In shop {item.shop_index + 1}. At pos {item.scroll_pos:.2f}.')
self.os_shop_side_navbar_ensure(upper=item.shop_index + 1)
OS_SHOP_SCROLL.set(item.scroll_pos, main=self, skip_first_screenshot=False)
_item = self.os_shop_get_items_to_buy(name=item.name, price=item.price)
if _item is None:
logger.warning(f'Item {item.name} not found in shop {item.shop_index + 1} at pos {item.scroll_pos:.2f}, skip.')
continue
if not self.check_item_count(_item):
logger.warning(f'Get {_item.name} count error, skip.')
continue
if self.os_shop_buy_execute(_item):
logger.info(f'Bought item: {_item.name}.')
skip_get_coins = False
count += 1
else:
logger.warning(f'Item {_item.name} cant be bought, skip.')
self.device.click_record.clear()
logger.info(f'Bought {f"{count} items" if count else "nothing"} in port.')
return True

我是这么写的:

self.get_current_pts()
logger.attr("Pt_preserved", self.pt_preserved)
for item in items:
logger.hr(f"Attempting to buy item: {str(item)}", level=3)
affordable_amount = self.calculate_affordable_amount(item)
if affordable_amount <= 0:
logger.warning(f"Cannot afford to buy any of item: {str(item)}.")
if self.is_event_ended:
logger.info("Event is ended, skip this item and continue to try buying other items.")
continue
else:
logger.info("Event is not ended, stopping further purchases to avoid overspending.")
break
elif affordable_amount < item.count:
logger.warning(f"Can only afford to buy {affordable_amount} of item: {str(item)}.")
self.event_shop_buy_item(item, amount=affordable_amount)
if self.is_event_ended:
logger.info("Event is ended, continue to try buying other items.")
self.get_current_pts()
continue
else:
logger.info("Event is not ended, stopping further purchases to avoid overspending.")
break
else:
self.event_shop_buy_item(item)
logger.info(f"Successfully bought item: {str(item)}")
self.get_current_pts()

不能认为物品列表在购买之后是不变的

局部物品列表的变化只会发生在获取船只的前后,其余情况都是留在原地的,而且也通过ItemNotFoundError的方式判断是否发生变化了,仅在发生变化的时候重新扫描全局物品列表(重新开始任务)

不能认为货币数量必然会减少预期的数量

没有这么认为。每次购买完后(并且还能买下一个时)都会重新检查代币数量。

self.event_shop_buy_item(item)
logger.info(f"Successfully bought item: {str(item)}")
self.get_current_pts()

然后每次购买物品前局部更新物品信息,如果不在当前页面那么滑动,如果发送了滑动重新更新局部物品信息。

没有理解你的意图。目前根据EventShopItem.scroll这一属性决定需要在哪个位置检查物品局部信息,并发送对应的滑动。

ShipUR-obtained 不能在 ShipUR-unobtained,否则打印warning然后忽略,这不需要多少代码

这个写法需要Filter支持tag的处理,而且没办法处理不同代币的物资的区分。

@LmeSzinc LmeSzinc added the feature request / 功能请求 New feature or requests label Sep 28, 2025
@guoh064

This comment was marked as outdated.

@guoh064 guoh064 marked this pull request as draft October 9, 2025 09:21
@guoh064 guoh064 force-pushed the shop_event branch 2 times, most recently from 1bd330f to 005c3f0 Compare October 17, 2025 09:11
@guoh064 guoh064 marked this pull request as ready for review October 17, 2025 09:13
@guoh064
Copy link
Contributor Author

guoh064 commented Oct 17, 2025

旧方法已经适配新界面了。
TODO:台服的NAV_EVENT

@guoh064 guoh064 force-pushed the shop_event branch 2 times, most recently from 288335d to 78a3dad Compare October 27, 2025 14:24
@guoh064 guoh064 force-pushed the shop_event branch 3 times, most recently from a5bf783 to 3047fa1 Compare November 1, 2025 16:04
@guoh064
Copy link
Contributor Author

guoh064 commented Nov 8, 2025

看上去review已经烂尾了,目前仅供有需要的人自行拉取,选项位于活动-活动商店。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature request / 功能请求 New feature or requests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants