feat: 收敛玩法运行时配置并加入故障恢复

This commit is contained in:
2026-04-01 13:04:26 +08:00
parent 1635a11780
commit 3ef841ecc7
73 changed files with 8820 additions and 2122 deletions

View File

@@ -0,0 +1,209 @@
export type DemoGamePreset = 'classic' | 'score-o'
export type BusinessLaunchSource = 'demo' | 'competition' | 'direct-event' | 'custom'
export interface GameConfigLaunchRequest {
configUrl: string
configLabel: string
configChecksumSha256?: string | null
releaseId?: string | null
routeCode?: string | null
}
export interface BusinessLaunchContext {
source: BusinessLaunchSource
competitionId?: string | null
eventId?: string | null
launchRequestId?: string | null
participantId?: string | null
sessionId?: string | null
sessionToken?: string | null
sessionTokenExpiresAt?: string | null
realtimeEndpoint?: string | null
realtimeToken?: string | null
}
export interface GameLaunchEnvelope {
config: GameConfigLaunchRequest
business: BusinessLaunchContext | null
}
export interface MapPageLaunchOptions {
launchId?: string
preset?: string
configUrl?: string
configLabel?: string
configChecksumSha256?: string
releaseId?: string
routeCode?: string
launchSource?: string
competitionId?: string
eventId?: string
launchRequestId?: string
participantId?: string
sessionId?: string
sessionToken?: string
sessionTokenExpiresAt?: string
realtimeEndpoint?: string
realtimeToken?: string
}
type PendingGameLaunchStore = Record<string, GameLaunchEnvelope>
const PENDING_GAME_LAUNCH_STORAGE_KEY = 'cmr.pendingGameLaunch.v1'
const CLASSIC_REMOTE_GAME_CONFIG_URL = 'https://oss-mbh5.colormaprun.com/gotomars/event/classic-sequential.json'
const SCORE_O_REMOTE_GAME_CONFIG_URL = 'https://oss-mbh5.colormaprun.com/gotomars/event/score-o.json'
function normalizeOptionalString(value: unknown): string | null {
if (typeof value !== 'string') {
return null
}
const normalized = decodeURIComponent(value).trim()
return normalized ? normalized : null
}
function resolveDemoPreset(value: string | null): DemoGamePreset {
return value === 'score-o' ? 'score-o' : 'classic'
}
function resolveBusinessLaunchSource(value: string | null): BusinessLaunchSource {
if (value === 'competition' || value === 'direct-event' || value === 'custom') {
return value
}
return 'demo'
}
function buildDemoConfig(preset: DemoGamePreset): GameConfigLaunchRequest {
if (preset === 'score-o') {
return {
configUrl: SCORE_O_REMOTE_GAME_CONFIG_URL,
configLabel: '积分赛配置',
}
}
return {
configUrl: CLASSIC_REMOTE_GAME_CONFIG_URL,
configLabel: '顺序赛配置',
}
}
function hasBusinessFields(context: Omit<BusinessLaunchContext, 'source'>): boolean {
return Object.values(context).some((value) => typeof value === 'string' && value.length > 0)
}
function buildBusinessLaunchContext(options?: MapPageLaunchOptions | null): BusinessLaunchContext | null {
if (!options) {
return null
}
const context = {
competitionId: normalizeOptionalString(options.competitionId),
eventId: normalizeOptionalString(options.eventId),
launchRequestId: normalizeOptionalString(options.launchRequestId),
participantId: normalizeOptionalString(options.participantId),
sessionId: normalizeOptionalString(options.sessionId),
sessionToken: normalizeOptionalString(options.sessionToken),
sessionTokenExpiresAt: normalizeOptionalString(options.sessionTokenExpiresAt),
realtimeEndpoint: normalizeOptionalString(options.realtimeEndpoint),
realtimeToken: normalizeOptionalString(options.realtimeToken),
}
const launchSource = normalizeOptionalString(options.launchSource)
if (!hasBusinessFields(context) && launchSource === null) {
return null
}
return {
source: resolveBusinessLaunchSource(launchSource),
...context,
}
}
function loadPendingGameLaunchStore(): PendingGameLaunchStore {
try {
const stored = wx.getStorageSync(PENDING_GAME_LAUNCH_STORAGE_KEY)
if (!stored || typeof stored !== 'object') {
return {}
}
return stored as PendingGameLaunchStore
} catch {
return {}
}
}
function savePendingGameLaunchStore(store: PendingGameLaunchStore): void {
try {
wx.setStorageSync(PENDING_GAME_LAUNCH_STORAGE_KEY, store)
} catch {}
}
export function getDemoGameLaunchEnvelope(preset: DemoGamePreset = 'classic'): GameLaunchEnvelope {
return {
config: buildDemoConfig(preset),
business: {
source: 'demo',
},
}
}
export function stashPendingGameLaunchEnvelope(envelope: GameLaunchEnvelope): string {
const launchId = `launch_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`
const store = loadPendingGameLaunchStore()
store[launchId] = envelope
savePendingGameLaunchStore(store)
return launchId
}
export function consumePendingGameLaunchEnvelope(launchId: string): GameLaunchEnvelope | null {
const normalizedLaunchId = normalizeOptionalString(launchId)
if (!normalizedLaunchId) {
return null
}
const store = loadPendingGameLaunchStore()
const envelope = store[normalizedLaunchId] || null
if (!envelope) {
return null
}
delete store[normalizedLaunchId]
savePendingGameLaunchStore(store)
return envelope
}
export function buildMapPageUrlWithLaunchId(launchId: string): string {
return `/pages/map/map?launchId=${encodeURIComponent(launchId)}`
}
export function prepareMapPageUrlForLaunch(envelope: GameLaunchEnvelope): string {
return buildMapPageUrlWithLaunchId(stashPendingGameLaunchEnvelope(envelope))
}
export function resolveGameLaunchEnvelope(options?: MapPageLaunchOptions | null): GameLaunchEnvelope {
const launchId = normalizeOptionalString(options ? options.launchId : undefined)
if (launchId) {
const pendingEnvelope = consumePendingGameLaunchEnvelope(launchId)
if (pendingEnvelope) {
return pendingEnvelope
}
}
const configUrl = normalizeOptionalString(options ? options.configUrl : undefined)
if (configUrl) {
return {
config: {
configUrl,
configLabel: normalizeOptionalString(options ? options.configLabel : undefined) || '线上配置',
configChecksumSha256: normalizeOptionalString(options ? options.configChecksumSha256 : undefined),
releaseId: normalizeOptionalString(options ? options.releaseId : undefined),
routeCode: normalizeOptionalString(options ? options.routeCode : undefined),
},
business: buildBusinessLaunchContext(options),
}
}
const preset = resolveDemoPreset(normalizeOptionalString(options ? options.preset : undefined))
return getDemoGameLaunchEnvelope(preset)
}