gameLaunch.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406
  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 GameResolvedReleaseLaunchContext {
  11. launchMode?: string | null
  12. source?: string | null
  13. eventId?: string | null
  14. releaseId?: string | null
  15. configLabel?: string | null
  16. manifestUrl?: string | null
  17. manifestChecksumSha256?: string | null
  18. routeCode?: string | null
  19. }
  20. export interface BusinessLaunchContext {
  21. source: BusinessLaunchSource
  22. competitionId?: string | null
  23. eventId?: string | null
  24. launchRequestId?: string | null
  25. participantId?: string | null
  26. sessionId?: string | null
  27. sessionToken?: string | null
  28. sessionTokenExpiresAt?: string | null
  29. realtimeEndpoint?: string | null
  30. realtimeToken?: string | null
  31. }
  32. export interface GameVariantLaunchContext {
  33. variantId?: string | null
  34. variantName?: string | null
  35. routeCode?: string | null
  36. assignmentMode?: string | null
  37. }
  38. export interface GameRuntimeLaunchContext {
  39. runtimeBindingId?: string | null
  40. placeId?: string | null
  41. placeName?: string | null
  42. mapId?: string | null
  43. mapName?: string | null
  44. tileReleaseId?: string | null
  45. courseSetId?: string | null
  46. courseVariantId?: string | null
  47. routeCode?: string | null
  48. }
  49. export interface GamePresentationLaunchContext {
  50. presentationId?: string | null
  51. templateKey?: string | null
  52. version?: string | null
  53. }
  54. export interface GameContentBundleLaunchContext {
  55. bundleId?: string | null
  56. bundleType?: string | null
  57. version?: string | null
  58. }
  59. export interface GameLaunchEnvelope {
  60. config: GameConfigLaunchRequest
  61. business: BusinessLaunchContext | null
  62. resolvedRelease?: GameResolvedReleaseLaunchContext | null
  63. variant?: GameVariantLaunchContext | null
  64. runtime?: GameRuntimeLaunchContext | null
  65. presentation?: GamePresentationLaunchContext | null
  66. contentBundle?: GameContentBundleLaunchContext | null
  67. }
  68. export interface MapPageLaunchOptions {
  69. launchId?: string
  70. recoverSession?: string
  71. autoStartOnEnter?: string
  72. preset?: string
  73. configUrl?: string
  74. configLabel?: string
  75. configChecksumSha256?: string
  76. releaseId?: string
  77. routeCode?: string
  78. launchSource?: string
  79. competitionId?: string
  80. eventId?: string
  81. launchRequestId?: string
  82. participantId?: string
  83. sessionId?: string
  84. sessionToken?: string
  85. sessionTokenExpiresAt?: string
  86. realtimeEndpoint?: string
  87. realtimeToken?: string
  88. variantId?: string
  89. variantName?: string
  90. assignmentMode?: string
  91. runtimeBindingId?: string
  92. placeId?: string
  93. placeName?: string
  94. mapId?: string
  95. mapName?: string
  96. tileReleaseId?: string
  97. courseSetId?: string
  98. courseVariantId?: string
  99. presentationId?: string
  100. presentationTemplateKey?: string
  101. presentationVersion?: string
  102. contentBundleId?: string
  103. contentBundleType?: string
  104. contentBundleVersion?: string
  105. }
  106. type PendingGameLaunchStore = Record<string, GameLaunchEnvelope>
  107. const PENDING_GAME_LAUNCH_STORAGE_KEY = 'cmr.pendingGameLaunch.v1'
  108. const CLASSIC_REMOTE_GAME_CONFIG_URL = 'https://oss-mbh5.colormaprun.com/gotomars/event/classic-sequential.json'
  109. const SCORE_O_REMOTE_GAME_CONFIG_URL = 'https://oss-mbh5.colormaprun.com/gotomars/event/score-o.json'
  110. function normalizeOptionalString(value: unknown): string | null {
  111. if (typeof value !== 'string') {
  112. return null
  113. }
  114. const normalized = decodeURIComponent(value).trim()
  115. return normalized ? normalized : null
  116. }
  117. function resolveDemoPreset(value: string | null): DemoGamePreset {
  118. return value === 'score-o' ? 'score-o' : 'classic'
  119. }
  120. function resolveBusinessLaunchSource(value: string | null): BusinessLaunchSource {
  121. if (value === 'competition' || value === 'direct-event' || value === 'custom') {
  122. return value
  123. }
  124. return 'demo'
  125. }
  126. function buildDemoConfig(preset: DemoGamePreset): GameConfigLaunchRequest {
  127. if (preset === 'score-o') {
  128. return {
  129. configUrl: SCORE_O_REMOTE_GAME_CONFIG_URL,
  130. configLabel: '积分赛配置',
  131. }
  132. }
  133. return {
  134. configUrl: CLASSIC_REMOTE_GAME_CONFIG_URL,
  135. configLabel: '顺序赛配置',
  136. }
  137. }
  138. function hasBusinessFields(context: Omit<BusinessLaunchContext, 'source'>): boolean {
  139. return Object.values(context).some((value) => typeof value === 'string' && value.length > 0)
  140. }
  141. function buildBusinessLaunchContext(options?: MapPageLaunchOptions | null): BusinessLaunchContext | null {
  142. if (!options) {
  143. return null
  144. }
  145. const context = {
  146. competitionId: normalizeOptionalString(options.competitionId),
  147. eventId: normalizeOptionalString(options.eventId),
  148. launchRequestId: normalizeOptionalString(options.launchRequestId),
  149. participantId: normalizeOptionalString(options.participantId),
  150. sessionId: normalizeOptionalString(options.sessionId),
  151. sessionToken: normalizeOptionalString(options.sessionToken),
  152. sessionTokenExpiresAt: normalizeOptionalString(options.sessionTokenExpiresAt),
  153. realtimeEndpoint: normalizeOptionalString(options.realtimeEndpoint),
  154. realtimeToken: normalizeOptionalString(options.realtimeToken),
  155. }
  156. const launchSource = normalizeOptionalString(options.launchSource)
  157. if (!hasBusinessFields(context) && launchSource === null) {
  158. return null
  159. }
  160. return {
  161. source: resolveBusinessLaunchSource(launchSource),
  162. ...context,
  163. }
  164. }
  165. function buildVariantLaunchContext(options?: MapPageLaunchOptions | null): GameVariantLaunchContext | null {
  166. if (!options) {
  167. return null
  168. }
  169. const variantId = normalizeOptionalString(options.variantId)
  170. const variantName = normalizeOptionalString(options.variantName)
  171. const routeCode = normalizeOptionalString(options.routeCode)
  172. const assignmentMode = normalizeOptionalString(options.assignmentMode)
  173. if (!variantId && !variantName && !routeCode && !assignmentMode) {
  174. return null
  175. }
  176. return {
  177. variantId,
  178. variantName,
  179. routeCode,
  180. assignmentMode,
  181. }
  182. }
  183. function buildRuntimeLaunchContext(options?: MapPageLaunchOptions | null): GameRuntimeLaunchContext | null {
  184. if (!options) {
  185. return null
  186. }
  187. const runtimeBindingId = normalizeOptionalString(options.runtimeBindingId)
  188. const placeId = normalizeOptionalString(options.placeId)
  189. const placeName = normalizeOptionalString(options.placeName)
  190. const mapId = normalizeOptionalString(options.mapId)
  191. const mapName = normalizeOptionalString(options.mapName)
  192. const tileReleaseId = normalizeOptionalString(options.tileReleaseId)
  193. const courseSetId = normalizeOptionalString(options.courseSetId)
  194. const courseVariantId = normalizeOptionalString(options.courseVariantId)
  195. const routeCode = normalizeOptionalString(options.routeCode)
  196. if (!runtimeBindingId && !placeId && !placeName && !mapId && !mapName && !tileReleaseId && !courseSetId && !courseVariantId && !routeCode) {
  197. return null
  198. }
  199. return {
  200. runtimeBindingId,
  201. placeId,
  202. placeName,
  203. mapId,
  204. mapName,
  205. tileReleaseId,
  206. courseSetId,
  207. courseVariantId,
  208. routeCode,
  209. }
  210. }
  211. function buildPresentationLaunchContext(options?: MapPageLaunchOptions | null): GamePresentationLaunchContext | null {
  212. if (!options) {
  213. return null
  214. }
  215. const presentationId = normalizeOptionalString(options.presentationId)
  216. const templateKey = normalizeOptionalString(options.presentationTemplateKey)
  217. const version = normalizeOptionalString(options.presentationVersion)
  218. if (!presentationId && !templateKey && !version) {
  219. return null
  220. }
  221. return {
  222. presentationId,
  223. templateKey,
  224. version,
  225. }
  226. }
  227. function buildContentBundleLaunchContext(options?: MapPageLaunchOptions | null): GameContentBundleLaunchContext | null {
  228. if (!options) {
  229. return null
  230. }
  231. const bundleId = normalizeOptionalString(options.contentBundleId)
  232. const bundleType = normalizeOptionalString(options.contentBundleType)
  233. const version = normalizeOptionalString(options.contentBundleVersion)
  234. if (!bundleId && !bundleType && !version) {
  235. return null
  236. }
  237. return {
  238. bundleId,
  239. bundleType,
  240. version,
  241. }
  242. }
  243. function loadPendingGameLaunchStore(): PendingGameLaunchStore {
  244. try {
  245. const stored = wx.getStorageSync(PENDING_GAME_LAUNCH_STORAGE_KEY)
  246. if (!stored || typeof stored !== 'object') {
  247. return {}
  248. }
  249. return stored as PendingGameLaunchStore
  250. } catch {
  251. return {}
  252. }
  253. }
  254. function savePendingGameLaunchStore(store: PendingGameLaunchStore): void {
  255. try {
  256. wx.setStorageSync(PENDING_GAME_LAUNCH_STORAGE_KEY, store)
  257. } catch {}
  258. }
  259. export function getDemoGameLaunchEnvelope(preset: DemoGamePreset = 'classic'): GameLaunchEnvelope {
  260. return {
  261. config: buildDemoConfig(preset),
  262. business: {
  263. source: 'demo',
  264. },
  265. resolvedRelease: null,
  266. variant: null,
  267. runtime: null,
  268. presentation: null,
  269. contentBundle: null,
  270. }
  271. }
  272. export function stashPendingGameLaunchEnvelope(envelope: GameLaunchEnvelope): string {
  273. const launchId = `launch_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`
  274. const store = loadPendingGameLaunchStore()
  275. store[launchId] = envelope
  276. savePendingGameLaunchStore(store)
  277. return launchId
  278. }
  279. export function consumePendingGameLaunchEnvelope(launchId: string): GameLaunchEnvelope | null {
  280. const normalizedLaunchId = normalizeOptionalString(launchId)
  281. if (!normalizedLaunchId) {
  282. return null
  283. }
  284. const store = loadPendingGameLaunchStore()
  285. const envelope = store[normalizedLaunchId] || null
  286. if (!envelope) {
  287. return null
  288. }
  289. delete store[normalizedLaunchId]
  290. savePendingGameLaunchStore(store)
  291. return envelope
  292. }
  293. export function buildMapPageUrlWithLaunchId(launchId: string, extraQuery?: Record<string, string>): string {
  294. const queryParts = [`launchId=${encodeURIComponent(launchId)}`]
  295. if (extraQuery) {
  296. Object.keys(extraQuery).forEach((key) => {
  297. const value = extraQuery[key]
  298. if (typeof value === 'string' && value) {
  299. queryParts.push(`${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
  300. }
  301. })
  302. }
  303. return `/pages/map/map?${queryParts.join('&')}`
  304. }
  305. export function prepareMapPageUrlForLaunch(envelope: GameLaunchEnvelope): string {
  306. return buildMapPageUrlWithLaunchId(
  307. stashPendingGameLaunchEnvelope(envelope),
  308. { autoStartOnEnter: '1' },
  309. )
  310. }
  311. export function prepareMapPageUrlForRecovery(envelope: GameLaunchEnvelope): string {
  312. return `${buildMapPageUrlWithLaunchId(stashPendingGameLaunchEnvelope(envelope))}&recoverSession=1`
  313. }
  314. export function getBackendSessionContextFromLaunchEnvelope(envelope: GameLaunchEnvelope | null | undefined): { sessionId: string; sessionToken: string } | null {
  315. if (!envelope || !envelope.business || !envelope.business.sessionId || !envelope.business.sessionToken) {
  316. return null
  317. }
  318. return {
  319. sessionId: envelope.business.sessionId,
  320. sessionToken: envelope.business.sessionToken,
  321. }
  322. }
  323. export function resolveGameLaunchEnvelope(options?: MapPageLaunchOptions | null): GameLaunchEnvelope {
  324. const launchId = normalizeOptionalString(options ? options.launchId : undefined)
  325. if (launchId) {
  326. const pendingEnvelope = consumePendingGameLaunchEnvelope(launchId)
  327. if (pendingEnvelope) {
  328. return pendingEnvelope
  329. }
  330. }
  331. const configUrl = normalizeOptionalString(options ? options.configUrl : undefined)
  332. if (configUrl) {
  333. return {
  334. config: {
  335. configUrl,
  336. configLabel: normalizeOptionalString(options ? options.configLabel : undefined) || '线上配置',
  337. configChecksumSha256: normalizeOptionalString(options ? options.configChecksumSha256 : undefined),
  338. releaseId: normalizeOptionalString(options ? options.releaseId : undefined),
  339. routeCode: normalizeOptionalString(options ? options.routeCode : undefined),
  340. },
  341. business: buildBusinessLaunchContext(options),
  342. resolvedRelease: null,
  343. variant: buildVariantLaunchContext(options),
  344. runtime: buildRuntimeLaunchContext(options),
  345. presentation: buildPresentationLaunchContext(options),
  346. contentBundle: buildContentBundleLaunchContext(options),
  347. }
  348. }
  349. const preset = resolveDemoPreset(normalizeOptionalString(options ? options.preset : undefined))
  350. return getDemoGameLaunchEnvelope(preset)
  351. }