gameLaunch.ts 11 KB

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