gameLaunch.ts 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260
  1. export type DemoGamePreset = 'classic' | 'score-o'
  2. export type BusinessLaunchSource = 'demo' | 'competition' | 'direct-event' | 'custom'
  3. export interface GameConfigLaunchRequest {
  4. configUrl: string
  5. configLabel: string
  6. configChecksumSha256?: string | null
  7. releaseId?: string | null
  8. routeCode?: string | null
  9. }
  10. export interface BusinessLaunchContext {
  11. source: BusinessLaunchSource
  12. competitionId?: string | null
  13. eventId?: string | null
  14. launchRequestId?: string | null
  15. participantId?: string | null
  16. sessionId?: string | null
  17. sessionToken?: string | null
  18. sessionTokenExpiresAt?: string | null
  19. realtimeEndpoint?: string | null
  20. realtimeToken?: string | null
  21. }
  22. export interface GameVariantLaunchContext {
  23. variantId?: string | null
  24. variantName?: string | null
  25. routeCode?: string | null
  26. assignmentMode?: string | null
  27. }
  28. export interface GameLaunchEnvelope {
  29. config: GameConfigLaunchRequest
  30. business: BusinessLaunchContext | null
  31. variant?: GameVariantLaunchContext | null
  32. }
  33. export interface MapPageLaunchOptions {
  34. launchId?: string
  35. recoverSession?: string
  36. preset?: string
  37. configUrl?: string
  38. configLabel?: string
  39. configChecksumSha256?: string
  40. releaseId?: string
  41. routeCode?: string
  42. launchSource?: string
  43. competitionId?: string
  44. eventId?: string
  45. launchRequestId?: string
  46. participantId?: string
  47. sessionId?: string
  48. sessionToken?: string
  49. sessionTokenExpiresAt?: string
  50. realtimeEndpoint?: string
  51. realtimeToken?: string
  52. variantId?: string
  53. variantName?: string
  54. assignmentMode?: string
  55. }
  56. type PendingGameLaunchStore = Record<string, GameLaunchEnvelope>
  57. const PENDING_GAME_LAUNCH_STORAGE_KEY = 'cmr.pendingGameLaunch.v1'
  58. const CLASSIC_REMOTE_GAME_CONFIG_URL = 'https://oss-mbh5.colormaprun.com/gotomars/event/classic-sequential.json'
  59. const SCORE_O_REMOTE_GAME_CONFIG_URL = 'https://oss-mbh5.colormaprun.com/gotomars/event/score-o.json'
  60. function normalizeOptionalString(value: unknown): string | null {
  61. if (typeof value !== 'string') {
  62. return null
  63. }
  64. const normalized = decodeURIComponent(value).trim()
  65. return normalized ? normalized : null
  66. }
  67. function resolveDemoPreset(value: string | null): DemoGamePreset {
  68. return value === 'score-o' ? 'score-o' : 'classic'
  69. }
  70. function resolveBusinessLaunchSource(value: string | null): BusinessLaunchSource {
  71. if (value === 'competition' || value === 'direct-event' || value === 'custom') {
  72. return value
  73. }
  74. return 'demo'
  75. }
  76. function buildDemoConfig(preset: DemoGamePreset): GameConfigLaunchRequest {
  77. if (preset === 'score-o') {
  78. return {
  79. configUrl: SCORE_O_REMOTE_GAME_CONFIG_URL,
  80. configLabel: '积分赛配置',
  81. }
  82. }
  83. return {
  84. configUrl: CLASSIC_REMOTE_GAME_CONFIG_URL,
  85. configLabel: '顺序赛配置',
  86. }
  87. }
  88. function hasBusinessFields(context: Omit<BusinessLaunchContext, 'source'>): boolean {
  89. return Object.values(context).some((value) => typeof value === 'string' && value.length > 0)
  90. }
  91. function buildBusinessLaunchContext(options?: MapPageLaunchOptions | null): BusinessLaunchContext | null {
  92. if (!options) {
  93. return null
  94. }
  95. const context = {
  96. competitionId: normalizeOptionalString(options.competitionId),
  97. eventId: normalizeOptionalString(options.eventId),
  98. launchRequestId: normalizeOptionalString(options.launchRequestId),
  99. participantId: normalizeOptionalString(options.participantId),
  100. sessionId: normalizeOptionalString(options.sessionId),
  101. sessionToken: normalizeOptionalString(options.sessionToken),
  102. sessionTokenExpiresAt: normalizeOptionalString(options.sessionTokenExpiresAt),
  103. realtimeEndpoint: normalizeOptionalString(options.realtimeEndpoint),
  104. realtimeToken: normalizeOptionalString(options.realtimeToken),
  105. }
  106. const launchSource = normalizeOptionalString(options.launchSource)
  107. if (!hasBusinessFields(context) && launchSource === null) {
  108. return null
  109. }
  110. return {
  111. source: resolveBusinessLaunchSource(launchSource),
  112. ...context,
  113. }
  114. }
  115. function buildVariantLaunchContext(options?: MapPageLaunchOptions | null): GameVariantLaunchContext | null {
  116. if (!options) {
  117. return null
  118. }
  119. const variantId = normalizeOptionalString(options.variantId)
  120. const variantName = normalizeOptionalString(options.variantName)
  121. const routeCode = normalizeOptionalString(options.routeCode)
  122. const assignmentMode = normalizeOptionalString(options.assignmentMode)
  123. if (!variantId && !variantName && !routeCode && !assignmentMode) {
  124. return null
  125. }
  126. return {
  127. variantId,
  128. variantName,
  129. routeCode,
  130. assignmentMode,
  131. }
  132. }
  133. function loadPendingGameLaunchStore(): PendingGameLaunchStore {
  134. try {
  135. const stored = wx.getStorageSync(PENDING_GAME_LAUNCH_STORAGE_KEY)
  136. if (!stored || typeof stored !== 'object') {
  137. return {}
  138. }
  139. return stored as PendingGameLaunchStore
  140. } catch {
  141. return {}
  142. }
  143. }
  144. function savePendingGameLaunchStore(store: PendingGameLaunchStore): void {
  145. try {
  146. wx.setStorageSync(PENDING_GAME_LAUNCH_STORAGE_KEY, store)
  147. } catch {}
  148. }
  149. export function getDemoGameLaunchEnvelope(preset: DemoGamePreset = 'classic'): GameLaunchEnvelope {
  150. return {
  151. config: buildDemoConfig(preset),
  152. business: {
  153. source: 'demo',
  154. },
  155. variant: null,
  156. }
  157. }
  158. export function stashPendingGameLaunchEnvelope(envelope: GameLaunchEnvelope): string {
  159. const launchId = `launch_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`
  160. const store = loadPendingGameLaunchStore()
  161. store[launchId] = envelope
  162. savePendingGameLaunchStore(store)
  163. return launchId
  164. }
  165. export function consumePendingGameLaunchEnvelope(launchId: string): GameLaunchEnvelope | null {
  166. const normalizedLaunchId = normalizeOptionalString(launchId)
  167. if (!normalizedLaunchId) {
  168. return null
  169. }
  170. const store = loadPendingGameLaunchStore()
  171. const envelope = store[normalizedLaunchId] || null
  172. if (!envelope) {
  173. return null
  174. }
  175. delete store[normalizedLaunchId]
  176. savePendingGameLaunchStore(store)
  177. return envelope
  178. }
  179. export function buildMapPageUrlWithLaunchId(launchId: string): string {
  180. return `/pages/map/map?launchId=${encodeURIComponent(launchId)}`
  181. }
  182. export function prepareMapPageUrlForLaunch(envelope: GameLaunchEnvelope): string {
  183. return buildMapPageUrlWithLaunchId(stashPendingGameLaunchEnvelope(envelope))
  184. }
  185. export function prepareMapPageUrlForRecovery(envelope: GameLaunchEnvelope): string {
  186. return `${buildMapPageUrlWithLaunchId(stashPendingGameLaunchEnvelope(envelope))}&recoverSession=1`
  187. }
  188. export function getBackendSessionContextFromLaunchEnvelope(envelope: GameLaunchEnvelope | null | undefined): { sessionId: string; sessionToken: string } | null {
  189. if (!envelope || !envelope.business || !envelope.business.sessionId || !envelope.business.sessionToken) {
  190. return null
  191. }
  192. return {
  193. sessionId: envelope.business.sessionId,
  194. sessionToken: envelope.business.sessionToken,
  195. }
  196. }
  197. export function resolveGameLaunchEnvelope(options?: MapPageLaunchOptions | null): GameLaunchEnvelope {
  198. const launchId = normalizeOptionalString(options ? options.launchId : undefined)
  199. if (launchId) {
  200. const pendingEnvelope = consumePendingGameLaunchEnvelope(launchId)
  201. if (pendingEnvelope) {
  202. return pendingEnvelope
  203. }
  204. }
  205. const configUrl = normalizeOptionalString(options ? options.configUrl : undefined)
  206. if (configUrl) {
  207. return {
  208. config: {
  209. configUrl,
  210. configLabel: normalizeOptionalString(options ? options.configLabel : undefined) || '线上配置',
  211. configChecksumSha256: normalizeOptionalString(options ? options.configChecksumSha256 : undefined),
  212. releaseId: normalizeOptionalString(options ? options.releaseId : undefined),
  213. routeCode: normalizeOptionalString(options ? options.routeCode : undefined),
  214. },
  215. business: buildBusinessLaunchContext(options),
  216. variant: buildVariantLaunchContext(options),
  217. }
  218. }
  219. const preset = resolveDemoPreset(normalizeOptionalString(options ? options.preset : undefined))
  220. return getDemoGameLaunchEnvelope(preset)
  221. }