fix: 修复 BangDream 活动背景选服

This commit is contained in:
sunlei 2026-06-07 03:46:47 +08:00
parent 7a9d808f8b
commit d429b799da
5 changed files with 141 additions and 24 deletions

View File

@ -312,6 +312,7 @@ export interface TsuguHook {
- 已新增 `models/cutoff-event-top-repository.ts``render-blocks/player-ranking-resource-repository.ts`,把前十榜 eventtop 数据请求与排名徽章素材路径从 `bestdoriUrl + callAPIAndCacheResponse/downloadFileCache` 收口到 provider-backed repository相关 spec 覆盖 eventtop query path、排名徽章路径和 provider 调用,本地 `/ycx 10 100 cn` 图片 smoke 输出非空。
- 已新增 `models/degree-resource-repository.ts`,把 `Degree` 的称号缩略图、称号框、称号图标、动态称号脚本和纹理素材路径从 `bestdoriUrl + downloadFile/downloadFileCache` 收口到 provider-backed repository`degree-resource-repository.spec.ts` 覆盖旧/新缩略图 fallback、动态称号旧纹理白名单和 provider 调用,本地 `/ycx 10 100 cn` 图片 smoke 输出非空。
- 已新增 `render-blocks/deck-rank-resource-repository.ts`把玩家详情“乐队编成等级”Rank 图片从 `list-band-detail.ts` 的本地路径和 Bestdori URL 直拼收口到 repository渲染层只负责 `Buffer -> Image``deck-rank-resource-repository.spec.ts` 覆盖本地素材优先、远端 `/res/icon/*.png` 兜底和 provider 调用,本地 `/查玩家 26591455 jp` 图片 smoke 输出非空。
- `models/event-data-repository.ts` 已改为 provider-injected repository并为活动背景和 topscreen trim 增加路径工厂;活动详情背景现在跟随 `displayedServerList` 选服,不再无条件按全局默认服取底图,避免 `/查活动 ... jp` 这类指定服务器查询出现横幅和背景不一致。
- 固化Windows 下不要用反斜杠测试路径直接调用 Jest pattern容易出现 `Pattern ... - 0 matches`;指定文件测试统一使用 `pnpm exec jest --runInBand --runTestsByPath test/qqbot/plugins/bangDream/tsugu/<file>.spec.ts ...`,路径用正斜杠。
### Phase 4搜索 specification 和 matcher

View File

@ -459,7 +459,9 @@ export async function drawEventDetail(
all.push(gachaImageList[i]);
}
const BGimage = useEasyBG ? undefined : await event.getEventBGImage();
const BGimage = useEasyBG
? undefined
: await event.getEventBGImage(displayedServerList);
return await createOutputFinalImages({
useEasyBG,

View File

@ -1,6 +1,5 @@
import { Canvas, Image, loadImage } from 'skia-canvas';
import { bangDreamBestdoriProvider } from '@/qqbot/plugins/bangDream/tsugu/data-clients/bestdori-provider';
import { bangDreamMainDataRepository } from '@/qqbot/plugins/bangDream/tsugu/models/main-data-repository';
import {
getServerByPriority,
Server,
@ -35,7 +34,11 @@ type RewardWithId = {
rewardType: string;
};
class EventDataRepository {
export class EventDataRepository {
constructor(
private readonly provider = bangDreamBestdoriProvider,
) {}
/**
*
*
@ -44,12 +47,40 @@ class EventDataRepository {
*/
async getDetail(eventId: number, update = true): Promise<Record<string, any>> {
const cacheTime = update ? 0 : 1 / 0;
return await bangDreamBestdoriProvider.getJson<Record<string, any>>(
return await this.provider.getJson<Record<string, any>>(
`/api/events/${eventId}.json`,
{ cacheTime },
);
}
/**
*
*
* @param event -
* @param displayedServerList -
*/
getBackgroundImagePath(
event: EventAssetContext,
displayedServerList: Server[] = globalDefaultServer,
): string {
const server = getServerByPriority(event.startAt, displayedServerList);
return `/assets/${Server[server]}/event/${event.assetBundleName}/topscreen_rip/bg_eventtop.png`;
}
/**
*
*
* @param event -
* @param displayedServerList -
*/
getTopscreenTrimImagePath(
event: EventAssetContext,
displayedServerList: Server[] = globalDefaultServer,
): string {
const server = getServerByPriority(event.startAt, displayedServerList);
return `/assets/${Server[server]}/event/${event.assetBundleName}/topscreen_rip/trim_eventtop.png`;
}
/**
* 退 homebanner
*
@ -62,13 +93,13 @@ class EventDataRepository {
): Promise<Image> {
const server = getServerByPriority(event.startAt, displayedServerList);
try {
const bannerImageBuffer = await bangDreamBestdoriProvider.getAsset(
const bannerImageBuffer = await this.provider.getAsset(
`/assets/${Server[server]}/event/${event.assetBundleName}/images_rip/banner.png`,
{ ignoreError: false },
);
return await loadImage(bannerImageBuffer);
} catch {
const bannerImageBuffer = await bangDreamBestdoriProvider.getAsset(
const bannerImageBuffer = await this.provider.getAsset(
`/assets/jp/homebanner_rip/${event.bannerAssetBundleName}.png`,
);
return await loadImage(bannerImageBuffer);
@ -79,15 +110,21 @@ class EventDataRepository {
*
*
* @param event -
* @param displayedServerList -
*/
async getBackgroundImage(event: EventAssetContext): Promise<Image | Canvas> {
const server = getServerByPriority(event.startAt);
const bgImageBuffer = await bangDreamBestdoriProvider.getAsset(
`/assets/${Server[server]}/event/${event.assetBundleName}/topscreen_rip/bg_eventtop.png`,
async getBackgroundImage(
event: EventAssetContext,
displayedServerList: Server[] = globalDefaultServer,
): Promise<Image | Canvas> {
const bgImageBuffer = await this.provider.getAsset(
this.getBackgroundImagePath(event, displayedServerList),
);
const backgroundImage = await loadImage(bgImageBuffer);
try {
const trimImage = await this.getTopscreenTrimImage(event);
const trimImage = await this.getTopscreenTrimImage(
event,
displayedServerList,
);
return this.mergeTopscreenImages(backgroundImage, trimImage);
} catch {
return backgroundImage;
@ -110,7 +147,7 @@ class EventDataRepository {
let ruleNumber = 1;
while (true) {
try {
const slideImageBuffer = await bangDreamBestdoriProvider.getAsset(
const slideImageBuffer = await this.provider.getAsset(
`${basePath}rule${ruleNumber}.png`,
{ ignoreError: false },
);
@ -127,11 +164,14 @@ class EventDataRepository {
*
*
* @param event -
* @param displayedServerList -
*/
async getTopscreenTrimImage(event: EventAssetContext): Promise<Image> {
const server = getServerByPriority(event.startAt);
const topscreenTrimImageBuffer = await bangDreamBestdoriProvider.getAsset(
`/assets/${Server[server]}/event/${event.assetBundleName}/topscreen_rip/trim_eventtop.png`,
async getTopscreenTrimImage(
event: EventAssetContext,
displayedServerList: Server[] = globalDefaultServer,
): Promise<Image> {
const topscreenTrimImageBuffer = await this.provider.getAsset(
this.getTopscreenTrimImagePath(event, displayedServerList),
);
return await loadImage(topscreenTrimImageBuffer);
}
@ -172,7 +212,7 @@ class EventDataRepository {
tempServer: Server,
): Promise<Image> {
const server = getServerByPriority(event.startAt, [tempServer]);
const logoImageBuffer = await bangDreamBestdoriProvider.getAsset(
const logoImageBuffer = await this.provider.getAsset(
`/assets/${Server[server]}/event/${event.assetBundleName}/images_rip/logo.png`,
);
return await loadImage(logoImageBuffer);
@ -188,7 +228,7 @@ class EventDataRepository {
event: EventRewardContext,
server: Server,
): Promise<Image | undefined> {
const allStamps = await bangDreamBestdoriProvider.getJson<
const allStamps = await this.provider.getJson<
Record<string, any>
>('/api/stamps/all.2.json');
const rewardId = this.pickRewardId(event.pointRewards, 'stamp');
@ -202,7 +242,7 @@ class EventDataRepository {
const serverName = this.pickReleasedServerName(event.startAt, server, 'jp');
try {
const stampBuffer = await bangDreamBestdoriProvider.getAsset(
const stampBuffer = await this.provider.getAsset(
`/assets/${serverName}/stamp/01_rip/${stampAssetName}.png`,
{ ignoreError: false },
);
@ -227,6 +267,9 @@ class EventDataRepository {
const rewardId = this.pickRewardId(event.rankingRewards, 'deco_pins');
if (rewardId === undefined) return undefined;
const { bangDreamMainDataRepository } = await import(
'@/qqbot/plugins/bangDream/tsugu/models/main-data-repository'
);
const allDeco = bangDreamMainDataRepository.getCollection<
Record<string, any>
>('deco');
@ -235,7 +278,7 @@ class EventDataRepository {
const serverName = this.pickReleasedServerName(event.startAt, server, 'cn');
try {
const decoBuffer = await bangDreamBestdoriProvider.getAsset(
const decoBuffer = await this.provider.getAsset(
`/assets/${serverName}/deco/pins_rip/${decoAssetName}.png`,
{ ignoreError: false },
);

View File

@ -214,8 +214,13 @@ export class Event {
*
* @returns
*/
async getEventBGImage(): Promise<Image | Canvas> {
return await eventDataRepository.getBackgroundImage(this);
async getEventBGImage(
displayedServerList: Server[] = globalDefaultServer,
): Promise<Image | Canvas> {
return await eventDataRepository.getBackgroundImage(
this,
displayedServerList,
);
}
//活动规则轮播图
/**
@ -233,8 +238,13 @@ export class Event {
*
* @returns
*/
async getEventTopscreenTrimImage(): Promise<Image> {
return await eventDataRepository.getTopscreenTrimImage(this);
async getEventTopscreenTrimImage(
displayedServerList: Server[] = globalDefaultServer,
): Promise<Image> {
return await eventDataRepository.getTopscreenTrimImage(
this,
displayedServerList,
);
}
/**
* Event Logo图片

View File

@ -0,0 +1,61 @@
import type { BangDreamDataProvider } from '@/qqbot/plugins/bangDream/tsugu/data-clients/data-provider';
import {
EventDataRepository,
type EventAssetContext,
} from '@/qqbot/plugins/bangDream/tsugu/models/event-data-repository';
import { Server } from '@/qqbot/plugins/bangDream/tsugu/models/server';
function createProviderMock(): jest.Mocked<BangDreamDataProvider> {
return {
getAsset: jest.fn(),
getJson: jest.fn(),
getTracker: jest.fn(),
name: 'MockBestdori',
resolveUrl: jest.fn((pathOrUrl) => `https://bestdori.example${pathOrUrl}`),
};
}
function createEventAssetContext(): EventAssetContext {
return {
assetBundleName: 'event_test',
bannerAssetBundleName: 'homebanner_test',
startAt: [1, null, null, 2, null],
};
}
describe('BangDream event data repository', () => {
it('builds event background paths from the requested server priority', () => {
const repository = new EventDataRepository(createProviderMock());
const event = createEventAssetContext();
expect(repository.getBackgroundImagePath(event, [Server.jp])).toBe(
'/assets/jp/event/event_test/topscreen_rip/bg_eventtop.png',
);
expect(repository.getTopscreenTrimImagePath(event, [Server.jp])).toBe(
'/assets/jp/event/event_test/topscreen_rip/trim_eventtop.png',
);
});
it('falls back to released servers when requested server has no event assets', () => {
const repository = new EventDataRepository(createProviderMock());
const event = createEventAssetContext();
expect(repository.getBackgroundImagePath(event, [Server.en])).toBe(
'/assets/cn/event/event_test/topscreen_rip/bg_eventtop.png',
);
});
it('routes event detail requests through the provider cache policy', async () => {
const provider = createProviderMock();
provider.getJson.mockResolvedValue({ eventName: ['test'] });
const repository = new EventDataRepository(provider);
await expect(repository.getDetail(50, false)).resolves.toEqual({
eventName: ['test'],
});
expect(provider.getJson).toHaveBeenCalledWith('/api/events/50.json', {
cacheTime: Infinity,
});
});
});