feat:完善游戏房状态维护

This commit is contained in:
max
2024-05-30 19:18:05 +08:00
parent 2396eb9089
commit 5be8ebde3a
51 changed files with 7346 additions and 426 deletions

View File

@@ -274,8 +274,6 @@ dependencies {
if (!isolationMode && file("../modules/module_google/build.gradle").exists()) {
implementation project(':modules:module_google')
}
implementation 'tech.sud.mgp:SudMGP-static:1.3.3.1158'
}
channel {

View File

@@ -18,6 +18,8 @@ import com.chwl.core.room.game.bean.GameCfg
import com.chwl.core.room.game.GameModel
import com.chwl.core.room.game.GameStatus
import com.chwl.core.room.model.HomePartyModel
import com.chwl.core.sud.state.SudMGPAPPState
import com.chwl.core.sud.state.SudMGPMGState
import com.chwl.core.user.UserModel
import com.chwl.core.utils.LogUtils
import com.chwl.core.utils.net.RxHelper
@@ -386,7 +388,7 @@ class GameDelegate(val activity: Activity, val container: FrameLayout, var mgId:
}
}
}
SudMGPMGState.APP_COMMON_SELF_CLICK_JOIN_BTN -> {
SudMGPMGState.MG_COMMON_SELF_CLICK_JOIN_BTN -> {
try {
val jsonObject = JSONObject(dataJson)
@@ -397,7 +399,7 @@ class GameDelegate(val activity: Activity, val container: FrameLayout, var mgId:
}
}
SudMGPMGState.APP_COMMON_SELF_CLICK_START_BTN -> {
SudMGPMGState.MG_COMMON_SELF_CLICK_START_BTN -> {
notifySelfPlayingState(true)
}
SudMGPMGState.MG_COMMON_GAME_STATE -> handleGameState(dataJson)

View File

@@ -1,61 +0,0 @@
/*
Copyright © Sud.Tech
https://sud.tech
*/
package com.chwl.app.avroom.game;
/**
* Time:2021/10/19
* Description: APP to MG 的状态定义
*/
public class SudMGPAPPState {
// region 通用状态
/**
* 加入状态
* 最低版本: v1.1.30.xx
*/
public static final String APP_COMMON_SELF_IN = "app_common_self_in";
/**
* 准备状态
* 最低版本: v1.1.30.xx
*/
public static final String APP_COMMON_SELF_READY = "app_common_self_ready";
/**
* 游戏状态
* 最低版本: v1.1.30.xx
*/
public static final String APP_COMMON_SELF_PLAYING = "app_common_self_playing";
/**
* 队长状态
* 最低版本: v1.1.30.xx
*/
public static final String APP_COMMON_SELF_CAPTAIN = "app_common_self_captain";
/**
* 踢人
* v1.1.30.xx
*/
public static final String APP_COMMON_SELF_KICK = "app_common_self_kick";
/**
* 结束游戏
* v1.1.30.xx
*/
public static final String APP_COMMON_SELF_END = "app_common_self_end";
/**
* 麦克风状态
*/
public static final String APP_COMMON_SELF_MICROPHONE = "app_common_self_microphone";
/**
* 文字命中状态
*/
public static final String APP_COMMON_SELF_TEXT_HIT = "app_common_self_text_hit";
}

View File

@@ -1,125 +0,0 @@
/*
Copyright © Sud.Tech
https://sud.tech
*/
package com.chwl.app.avroom.game;
/**
* Time:2021/10/19
* Description: MG to APP 的状态定义
*/
public class SudMGPMGState {
// region 通用状态-游戏
/**
* 公屏消息 (已修改)
* 最低版本: v1.1.30.xx
*/
public static final String MG_COMMON_PUBLIC_MESSAGE = "mg_common_public_message";
/**
* 关键词状态
*/
public static final String MG_COMMON_KEY_WORD_TO_HIT = "mg_common_key_word_to_hit";
// endregion 通用状态-游戏
// region 通用状态-玩家
/**
* 加入状态 (已修改)
* 最低版本: v1.1.30.xx
*/
public static final String MG_COMMON_PLAYER_IN = "mg_common_player_in";
/**
* 准备状态 (已修改)
* 最低版本: v1.1.30.xx
*/
public static final String MG_COMMON_PLAYER_READY = "mg_common_player_ready";
/**
* 队长状态 (已修改)
* 最低版本: v1.1.30.xx
*/
public static final String MG_COMMON_PLAYER_CAPTAIN = "mg_common_player_captain";
/**
* 游戏状态 (已修改)
* 最低版本: v1.1.30.xx
*/
public static final String MG_COMMON_PLAYER_PLAYING = "mg_common_player_playing";
/**
* 游戏状态(已修改)
* 最低版本: v1.1.30.xx
*/
public static final String MG_COMMON_GAME_STATE = "mg_common_game_state";
// endregion 通用状态-玩家
// region 碰碰我最强
// endregion 碰碰我最强
// region 飞刀达人
// endregion 飞刀达人
// region 你画我猜
/**
* 选词中
*/
public static final String MG_DG_SELECTING = "mg_dg_selecting";
/**
* 作画中
*/
public static final String MG_DG_PAINTING = "mg_dg_painting";
/**
* 错误答案
*/
public static final String MG_DG_ERRORANSWER = "mg_dg_erroranswer";
/**
* 总积分
*/
public static final String MG_DG_TOTALSCORE = "mg_dg_totalscore";
/**
* 本次积分
*/
public static final String MG_DG_SCORE = "mg_dg_score";
// endregion 你画我猜
/**
* 加入游戏按钮点击状态
*/
public static final String APP_COMMON_SELF_CLICK_JOIN_BTN = "mg_common_self_click_join_btn";
/**
* 取消加入游戏按钮点击状态
*/
public static final String APP_COMMON_SELF_CLICK_CANCEL_JOIN_BTN = "mg_common_self_click_cancel_join_btn";
/**
* 准备按钮点击状态
*/
public static final String APP_COMMON_SELF_CLICK_READY_BTN = "mg_common_self_click_ready_btn";
/**
* 取消准备按钮点击状态
*/
public static final String APP_COMMON_SELF_CLICK_CANCEL_READY_BTN = "mg_common_self_click_cancel_ready_btn";
/**
* 开始游戏按钮点击状态
*/
public static final String APP_COMMON_SELF_CLICK_START_BTN = "mg_common_self_click_start_btn";
/**
* 分享按钮点击状态
*/
public static final String APP_COMMON_SELF_CLICK_SHARE_BTN = "mg_common_self_click_share_btn";
}

View File

@@ -1,6 +1,7 @@
package com.chwl.app.support
import androidx.lifecycle.viewModelScope
import com.chwl.app.BuildConfig
import com.chwl.app.base.BaseViewModel
import com.chwl.core.helper.PathHelper
import com.chwl.core.home.model.HomeModel
@@ -27,6 +28,10 @@ class PreloadResourceViewModel : BaseViewModel(), DownloadListener, ILog {
private var isStarted = false
fun start() {
if (BuildConfig.DEBUG) {
// 太多请求了,影响查看控制台日志
return
}
if (isStarted) {
return
}

View File

@@ -728,4 +728,5 @@
<color name="color_10ECD6">#10ECD6</color>
<color name="color_4D415E">#4D415E</color>
<color name="color_DE3446">#DE3446</color>
<color name="color_FF6629">#FF6629</color>
</resources>

View File

@@ -1,18 +1,27 @@
package com.chwl.app.game.core
import android.app.Activity
import android.view.View
import android.widget.FrameLayout
import androidx.activity.viewModels
import androidx.fragment.app.FragmentManager
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LiveData
import androidx.lifecycle.lifecycleScope
import androidx.viewbinding.ViewBinding
import com.chwl.app.R
import com.chwl.app.base.BaseViewBindingActivity
import com.chwl.app.common.widget.dialog.DialogManager
import com.chwl.app.game.core.engine.GameEngineViewModel
import com.chwl.app.game.data.bean.GameInfoUiState
import com.chwl.app.game.ui.game.GameViewModel
import com.chwl.core.support.room.RoomContext
import com.chwl.core.support.room.RoomView
import com.chwl.core.support.room.RoomWidget
import com.netease.nim.uikit.StatusBarUtil
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.zip
import kotlinx.coroutines.launch
abstract class BaseGameActivity<T : ViewBinding> : BaseViewBindingActivity<T>(), RoomView {
@@ -23,25 +32,110 @@ abstract class BaseGameActivity<T : ViewBinding> : BaseViewBindingActivity<T>(),
protected val stateAbility get() = gameContext?.findAbility<GameStateAbility>(GameStateAbility::class.java.simpleName)
private var gameInfoDialogManager: DialogManager? = null
protected var loadingDialogManager: DialogManager? = null
override fun init() {
initView()
initEvent()
initObserver()
initWidgets()
gameEngineViewModel.init(lifecycle)
}
protected open fun initView() {
gameInfoDialogManager = DialogManager(this)
loadingDialogManager = DialogManager(this)
}
protected open fun initEvent() {
}
protected open fun initObserver() {
viewModel.gameContextLiveData.observe(this) {
onGameContext(it)
}
gameEngineViewModel.gameViewLiveData.observe(this) {
onGameViewChanged(it)
}
}
protected open fun initWidgets() {
}
protected open fun onGameContext(gameContext: GameContext) {
gameEngineViewModel.setGameContext(gameContext)
stateAbility?.let {
observeGameInfoUiState(it)
updateGameEngine(it)
}
}
private fun observeGameInfoUiState(stateAbility: GameStateAbility) {
lifecycleScope.launch {
stateAbility.gameInfoUiStateFlow.collectLatest {
when (it) {
is GameInfoUiState.Loading -> {
gameInfoDialogManager?.showProgressDialog(context)
}
is GameInfoUiState.Success -> {
gameInfoDialogManager?.dismissDialog()
}
is GameInfoUiState.Failed -> {
gameInfoDialogManager?.dismissDialog()
it.throwable.message?.let { msg ->
toast(msg)
}
}
is GameInfoUiState.Empty -> {
gameInfoDialogManager?.dismissDialog()
toast(R.string.empty_data)
finish()
}
}
}
}
}
private fun updateGameEngine(stateAbility: GameStateAbility) {
lifecycleScope.launch {
stateAbility.gameIdFlow.zip(stateAbility.roomIdFlow) { gameId, roomId ->
if (gameId?.toLongOrNull() != null && roomId != null) {
Pair(gameId.toLong(), roomId)
} else {
null
}
}.collectLatest {
if (it != null) {
gameEngineViewModel.loadGame(
this@BaseGameActivity,
it.second.toString(),
it.first
)
}
}
}
}
protected open fun onGameViewChanged(view: View?) {
if (view == null) {
getGameViewGroup().removeAllViews()
} else {
getGameViewGroup().addView(
view,
FrameLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT,
FrameLayout.LayoutParams.MATCH_PARENT
)
)
}
}
abstract fun getGameViewGroup(): FrameLayout
/**
* 注册组件
*/
@@ -93,4 +187,11 @@ abstract class BaseGameActivity<T : ViewBinding> : BaseViewBindingActivity<T>(),
return widgets[name]
}
override fun onDestroy() {
super.onDestroy()
gameInfoDialogManager?.dismissDialog()
gameInfoDialogManager = null
loadingDialogManager?.dismissDialog()
loadingDialogManager = null
}
}

View File

@@ -11,6 +11,7 @@ import com.netease.nimlib.sdk.StatusCode
import com.netease.nimlib.sdk.chatroom.ChatRoomMessageBuilder
import com.netease.nimlib.sdk.chatroom.model.ChatRoomMessage
import io.reactivex.Single
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.collectLatest
import java.lang.IllegalStateException
@@ -22,6 +23,8 @@ class GameIMEngineAbility(private val listenerOwner: ListenerOwner<GameIMEngineA
private var chatRoomClient: ChatRoomClient? = null
val receiveMessageFlow = MutableSharedFlow<List<ChatRoomMessage>>()
private val stateAbility: GameStateAbility?
get() = roomContext?.findAbility<GameStateAbility>(
GameStateAbility::class.java.simpleName

View File

@@ -1,7 +1,19 @@
package com.chwl.app.game.core
import com.chwl.core.bean.room.RoomMicBean
import com.chwl.core.support.room.RoomAbility
import com.chwl.core.support.room.RoomContext
import kotlinx.coroutines.flow.MutableStateFlow
class GameQueueAbility : RoomAbility() {
val queueFlow = MutableStateFlow<List<RoomMicBean>?>(null)
override fun onStart(context: RoomContext) {
super.onStart(context)
}
suspend fun updateQueue(data: List<RoomMicBean>?) {
queueFlow.value = data
}
}

View File

@@ -1,12 +1,18 @@
package com.chwl.app.game.core
import com.chwl.app.game.data.GameModel2
import com.chwl.app.game.data.bean.GameRoomInfo
import com.chwl.app.game.data.bean.GameInfoUiState
import com.chwl.core.bean.game.GameRoomInfo
import com.chwl.core.im.custom.bean.CustomAttachment
import com.chwl.core.im.custom.bean.GameQueueChangedAttachment
import com.chwl.core.support.room.RoomAbility
import com.chwl.core.support.room.RoomContext
import com.netease.nimlib.sdk.chatroom.model.ChatRoomMessage
import com.netease.nimlib.sdk.msg.constant.MsgTypeEnum
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
class GameStateAbility : RoomAbility() {
class GameStateAbility : RoomAbility(), GameIMEngineAbility.Listener {
/**
* 游戏状态: 本地定义状态+匹配状态
@@ -25,32 +31,98 @@ class GameStateAbility : RoomAbility() {
val scoresFlow = MutableStateFlow<List<Double>?>(null)
@Deprecated("里面的属性有可能不是最新的建议通过具体属性flow获取数据")
val gameInfoFlow = MutableStateFlow<GameRoomInfo?>(null)
val gameInfoUiStateFlow = MutableStateFlow<GameInfoUiState>(GameInfoUiState.Loading)
val matchFailedFlow = MutableSharedFlow<Boolean>()
private val queueAbility get() = roomContext?.findAbility<GameQueueAbility>(GameQueueAbility::class.java.simpleName)
override fun onStart(context: RoomContext) {
super.onStart(context)
requestRoomInfo()
val imEngineAbility =
context.findAbility<GameIMEngineAbility>(GameIMEngineAbility::class.java.simpleName)
imEngineAbility?.addListener(this)
}
private fun requestRoomInfo() {
safeLaunch {
gameInfoUiStateFlow.value = GameInfoUiState.Loading
safeLaunch(onError = {
gameInfoUiStateFlow.value = GameInfoUiState.Failed(it, gameInfoFlow.value)
}) {
val info = GameModel2.getGameRoomInfo()
if (info != null) {
gameInfoUiStateFlow.value = GameInfoUiState.Success
syncRoomInfo(info)
} else {
gameInfoUiStateFlow.value = GameInfoUiState.Empty(gameInfoFlow.value)
syncGameState(null)
}
}
}
private fun syncRoomInfo(info: GameRoomInfo) {
private suspend fun syncRoomInfo(info: GameRoomInfo) {
roomContext?.roomId = info.chatRoomId ?: 0
gameInfoFlow.value = info
roomIdFlow.value = info.chatRoomId
imIdFlow.value = info.roomId
gameIdFlow.value = info.data?.mgId
gameIconFlow.value = info.data?.gameRoomIcon
scoresFlow.value = info.data?.scores
syncGameState(info.data?.matchStatus)
syncQueue(info)
}
private fun syncGameState(matchStatus: Int?) {
gameStateFlow.value = matchStatus
}
private suspend fun syncQueue(info: GameRoomInfo) {
queueAbility?.updateQueue(info.roomMics)
syncGameState(info.data?.matchStatus)
}
override fun onReceiveMessage(messages: List<ChatRoomMessage>) {
messages.forEach {
onReceiveMessage(it)
}
}
private fun onReceiveMessage(message: ChatRoomMessage) {
if (message.msgType == MsgTypeEnum.custom) {
val attachment: CustomAttachment = (message.attachment as? CustomAttachment) ?: return
when (attachment.first) {
CustomAttachment.CUSTOM_MSG_MINI_GAME -> {
when (attachment.second) {
// 麦位变更
CustomAttachment.CUSTOM_MSG_MINI_GAME_QUEUE_CHANGED -> {
val gameInfo =
(attachment as? GameQueueChangedAttachment)?.gameInfo ?: return
safeLaunch {
syncQueue(gameInfo)
}
}
// 匹配失败
CustomAttachment.CUSTOM_MSG_MINI_GAME_MATCH_FAILED -> {
safeLaunch {
gameStateFlow.value = 3
matchFailedFlow.emit(true)
}
}
// 提前结束
CustomAttachment.CUSTOM_MSG_MINI_GAME_FORCED_END -> {
safeLaunch {
gameStateFlow.value = 2
}
}
}
}
}
}
}
}

View File

@@ -8,45 +8,60 @@ import android.widget.FrameLayout
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.MutableLiveData
import com.chwl.app.R
import com.chwl.app.avroom.game.AppConfig
import com.chwl.app.base.BaseViewModel
import com.chwl.app.game.core.engine.model.GameStateResponse
import com.chwl.app.game.core.engine.model.GameViewInfoModel
import com.chwl.app.game.core.engine.model.GameViewInfoModel.GameViewRectModel
import com.chwl.app.game.core.GameContext
import com.chwl.app.game.core.GameQueueAbility
import com.chwl.app.game.core.GameStateAbility
import com.chwl.core.auth.AuthModel
import com.chwl.core.room.game.GameModel
import com.chwl.core.room.game.bean.GameCfg
import com.chwl.core.sud.decorator.SudFSMMGDecorator
import com.chwl.core.sud.decorator.SudFSMMGListener
import com.chwl.core.sud.decorator.SudFSTAPPDecorator
import com.chwl.core.sud.model.GameViewInfoModel
import com.chwl.core.sud.state.MGStateResponse
import com.chwl.core.sud.state.SudMGPMGState
import com.chwl.library.language.LanguageHelper
import com.chwl.library.utils.SingleToastUtil
import com.chwl.library.utils.json.JsonUtils
import com.example.lib_utils.log.ILog
import kotlinx.coroutines.flow.MutableStateFlow
import tech.sud.mgp.core.ISudFSMStateHandle
import tech.sud.mgp.core.ISudListenerInitSDK
import tech.sud.mgp.core.SudMGP
import java.lang.IllegalStateException
@SuppressLint("StaticFieldLeak")
open class GameEngineViewModel : BaseViewModel(), ILog, LifecycleEventObserver {
open class GameEngineViewModel : BaseViewModel(), SudFSMMGListener, ILog,
LifecycleEventObserver {
val gameViewLiveData = MutableLiveData<View>(null)
private var isRunning = true
private val gameFSMMG = GameSudFSMMG(this)
private val gameFSTAPP = GameSudFSTAPP(this)
private val sudFSMMG = SudFSMMGDecorator().apply {
setSudFSMMGListener(this@GameEngineViewModel)
}
private val sudFSTAPP = SudFSTAPPDecorator()
private var lifecycle: Lifecycle? = null
private var gameLayout: FrameLayout? = null
private var gameViewRect: GameViewRectModel = GameViewRectModel()
private var gameViewRect: GameViewInfoModel.GameViewRectModel =
GameViewInfoModel.GameViewRectModel()
private var roomId: String = ""
fun init(lifecycle: Lifecycle, gameLayout: FrameLayout) {
private var gameContext: GameContext? = null
fun init(lifecycle: Lifecycle) {
this.lifecycle = lifecycle
this.gameLayout = gameLayout
lifecycle.addObserver(this)
}
fun setGameContext(gameContext: GameContext) {
this.gameContext = gameContext
}
fun loadGame(activity: Activity, roomId: String, gameId: Long) {
logD("loadGame() roomId:$roomId gameId:$gameId")
if (!this.isRunning) {
return
}
@@ -85,21 +100,14 @@ open class GameEngineViewModel : BaseViewModel(), ILog, LifecycleEventObserver {
return
}
val userId = AuthModel.get().currentUid.toString()
gameFSTAPP.destroyMG()
sudFSTAPP.destroyMG()
val iSudFSTAPP =
SudMGP.loadMG(activity, userId, getRoomId(), code, gameId, getGameLanguage(), gameFSMMG)
gameFSTAPP.updateSudFSTAPP(iSudFSTAPP)
updateGameView(iSudFSTAPP.gameView)
SudMGP.loadMG(activity, userId, getRoomId(), code, gameId, getGameLanguage(), sudFSMMG)
sudFSTAPP.setISudFSTAPP(iSudFSTAPP)
gameViewLiveData.value = iSudFSTAPP.gameView
}
private fun updateGameView(view: View?) {
gameLayout?.removeAllViews()
if (view != null) {
gameLayout?.addView(view)
}
}
fun setGameViewRect(rect: GameViewRectModel) {
fun setGameViewRect(rect: GameViewInfoModel.GameViewRectModel) {
this.gameViewRect = rect
}
@@ -120,40 +128,6 @@ open class GameEngineViewModel : BaseViewModel(), ILog, LifecycleEventObserver {
}
}
fun onExpireCode(handle: ISudFSMStateHandle) {
getGameCode({
handle.success(GameStateResponse.success().toJson())
gameFSTAPP.updateCode(it, null)
}, {
logE(it)
})
}
fun onGetGameCfg(handle: ISudFSMStateHandle) {
handle.success(JsonUtils.toJson(GameCfg()))
}
fun onGetGameViewInfo(handle: ISudFSMStateHandle) {
val gameLayout = gameLayout
if (gameLayout == null) {
handle.failure("gameLayout is NULL")
return
}
val gameViewWidth = gameLayout.measuredWidth
val gameViewHeight = gameLayout.measuredHeight
if (gameViewWidth > 0 && gameViewHeight > 0) {
notifyGameViewInfo(handle, gameViewWidth, gameViewHeight)
return
}
gameLayout.viewTreeObserver.addOnGlobalLayoutListener(object :
ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() {
gameLayout.viewTreeObserver.removeOnGlobalLayoutListener(this)
notifyGameViewInfo(handle, gameLayout.measuredWidth, gameLayout.measuredHeight)
}
})
}
private fun notifyGameViewInfo(handle: ISudFSMStateHandle, width: Int, height: Int) {
val response = GameViewInfoModel()
response.ret_code = 0
@@ -203,4 +177,76 @@ open class GameEngineViewModel : BaseViewModel(), ILog, LifecycleEventObserver {
lifecycle?.removeObserver(this)
lifecycle = null
}
override fun onGameStarted() {
autoJoinGame()
}
override fun onGameDestroyed() {
}
private fun autoJoinGame() {
sudFSTAPP.notifyAPPCommonSelfIn(true, -1, true, 1)
sudFSTAPP.notifyAPPCommonSelfReady(true)
}
override fun onExpireCode(handle: ISudFSMStateHandle, p1: String?) {
getGameCode({
handle.success(MGStateResponse.success().toJson())
sudFSTAPP.updateCode(it, null)
}, {
logE(it)
})
}
override fun onGetGameViewInfo(handle: ISudFSMStateHandle, p1: String?) {
val gameLayout = gameViewLiveData.value
if (gameLayout == null) {
handle.failure("gameLayout is NULL")
return
}
val gameViewWidth = gameLayout.measuredWidth
val gameViewHeight = gameLayout.measuredHeight
if (gameViewWidth > 0 && gameViewHeight > 0) {
notifyGameViewInfo(handle, gameViewWidth, gameViewHeight)
return
}
gameLayout.viewTreeObserver.addOnGlobalLayoutListener(object :
ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() {
gameLayout.viewTreeObserver.removeOnGlobalLayoutListener(this)
notifyGameViewInfo(handle, gameLayout.measuredWidth, gameLayout.measuredHeight)
}
})
}
override fun onGetGameCfg(handle: ISudFSMStateHandle, p1: String?) {
handle.success(JsonUtils.toJson(GameCfg()))
}
override fun onGameMGCommonSelfClickJoinBtn(
handle: ISudFSMStateHandle?,
model: SudMGPMGState.MGCommonSelfClickJoinBtn?
) {
sudFSTAPP.notifyAPPCommonSelfIn(true, -1, true, 1)
}
override fun onPlayerMGCommonPlayerReady(
handle: ISudFSMStateHandle?,
userId: String?,
model: SudMGPMGState.MGCommonPlayerReady?
) {
super.onPlayerMGCommonPlayerReady(handle, userId, model)
val queueAbility =
gameContext?.findAbility<GameQueueAbility>(GameQueueAbility::class.java.simpleName)
?: return
val queueSize = queueAbility.queueFlow.value?.size ?: 0
if (queueSize == 0) {
return
}
logD("onPlayerMGCommonPlayerReady queueSize:$queueSize size:${sudFSMMG.sudFSMMGCache.playerReadySet.size}")
if (sudFSMMG.sudFSMMGCache.playerReadySet.size >= queueSize) {
sudFSTAPP.notifyAPPCommonSelfPlaying(true, null, null)
}
}
}

View File

@@ -1,48 +0,0 @@
package com.chwl.app.game.core.engine
import com.chwl.app.game.core.engine.model.GameStateResponse
import com.example.lib_utils.log.ILog
import tech.sud.mgp.core.ISudFSMMG
import tech.sud.mgp.core.ISudFSMStateHandle
class GameSudFSMMG(private val engineViewModel: GameEngineViewModel) : ISudFSMMG, ILog {
override fun onGameLog(p0: String?) {
if (p0 != null) {
logD(p0)
}
}
override fun onGameLoadingProgress(p0: Int, p1: Int, p2: Int) {
}
override fun onGameStarted() {
}
override fun onGameDestroyed() {
}
override fun onExpireCode(p0: ISudFSMStateHandle, p1: String?) {
engineViewModel.onExpireCode(p0)
}
override fun onGetGameViewInfo(p0: ISudFSMStateHandle, p1: String?) {
engineViewModel.onGetGameViewInfo(p0)
}
override fun onGetGameCfg(p0: ISudFSMStateHandle, p1: String?) {
engineViewModel.onGetGameCfg(p0)
}
override fun onGameStateChange(p0: ISudFSMStateHandle, p1: String?, p2: String?) {
p0.success(GameStateResponse.success().toJson())
}
override fun onPlayerStateChange(
p0: ISudFSMStateHandle,
p1: String?,
p2: String?,
p3: String?
) {
p0.success(GameStateResponse.success().toJson())
}
}

View File

@@ -1,37 +0,0 @@
package com.chwl.app.game.core.engine
import tech.sud.mgp.core.ISudFSTAPP
import tech.sud.mgp.core.ISudListenerNotifyStateChange
class GameSudFSTAPP(private val engineViewModel: GameEngineViewModel) {
private var sudFSTAPP: ISudFSTAPP? = null
fun updateSudFSTAPP(iSudFSTAPP: ISudFSTAPP) {
this.sudFSTAPP = iSudFSTAPP
}
fun updateCode(code: String, listener: ISudListenerNotifyStateChange?) {
this.sudFSTAPP?.updateCode(code, listener)
}
fun startMG() {
this.sudFSTAPP?.startMG()
}
fun playMG() {
this.sudFSTAPP?.playMG()
}
fun pauseMG() {
this.sudFSTAPP?.pauseMG()
}
fun stopMG() {
this.sudFSTAPP?.stopMG()
}
fun destroyMG() {
sudFSTAPP?.destroyMG()
sudFSTAPP = null
}
}

View File

@@ -1,25 +0,0 @@
package com.chwl.app.game.core.engine.model
import androidx.annotation.Keep
import com.chwl.library.utils.json.JsonUtils
import java.io.Serializable
@Keep
data class GameStateResponse(
var ret_code: Int = CODE_SUCCESS, // 返回码
var ret_msg: String? = null // 返回消息
) : Serializable {
fun toJson(): String {
return JsonUtils.toJson(this)
}
// 返回码,成功
companion object {
val CODE_SUCCESS = 0
fun success(): GameStateResponse {
return GameStateResponse(CODE_SUCCESS, "success")
}
}
}

View File

@@ -1,47 +0,0 @@
/*
* Copyright © Sud.Tech
* https://sud.tech
*/
package com.chwl.app.game.core.engine.model;
import androidx.annotation.Keep;
/**
* 游戏视图
* 参考文档https://docs.sud.tech/zh-CN/app/Client/API/ISudFSMMG/onGetGameViewInfo.html
*/
@Keep
public class GameViewInfoModel {
// 返回码
public int ret_code;
// 返回消息
public String ret_msg;
// 游戏View的大小
public GameViewSizeModel view_size = new GameViewSizeModel();
// 游戏安全操作区域
public GameViewRectModel view_game_rect = new GameViewRectModel();
@Keep
public static class GameViewSizeModel {
// 游戏View的宽 (单位像素)
public int width;
// 游戏View的高 (单位像素)
public int height;
}
@Keep
public static class GameViewRectModel {
// 相对于view_size左边框偏移单位像素
public int left;
// 相对于view_size上边框偏移单位像素
public int top;
// 相对于view_size右边框偏移单位像素
public int right;
// 相对于view_size下边框偏移单位像素
public int bottom;
}
}

View File

@@ -1,7 +1,7 @@
package com.chwl.app.game.data
import com.chwl.app.game.data.bean.GameConfigBean
import com.chwl.app.game.data.bean.GameRoomInfo
import com.chwl.core.bean.game.GameConfigBean
import com.chwl.core.bean.game.GameRoomInfo
import com.chwl.core.base.BaseModel
import com.chwl.core.bean.response.ServiceResult
import com.chwl.core.bean.room.BaseRoomInfo
@@ -33,11 +33,16 @@ object GameModel2 : BaseModel() {
api.startGame(gameId, gameMode)
}
suspend fun closeGame(roomId: Long): String? =
launchRequest {
api.closeGame(roomId)
}
private interface Api {
/**
* 房间信息
*/
@GET("chatRoom/get")
@GET("chatRoom/getByType")
suspend fun getChatRoomInfo(@Query("roomType") roomType: Int): ServiceResult<GameRoomInfo>
/**
@@ -55,6 +60,12 @@ object GameModel2 : BaseModel() {
@Field("mgId") mgId: String,
@Field("gameMode") gameMode: Int
): ServiceResult<Long>
/**
* 关闭游戏
*/
@GET("miniGame/nav/close")
suspend fun closeGame(@Query("roomId") roomId: Long): ServiceResult<String>
}
}

View File

@@ -1,12 +0,0 @@
package com.chwl.app.game.data.bean
import androidx.annotation.Keep
@Keep
class GameConfigBean {
val mgId: Long? = null
val mgIdStr: String? = null
val name: String? = null
val pic: String? = null
val gameModes: List<GameModeBean>? = null
}

View File

@@ -0,0 +1,15 @@
package com.chwl.app.game.data.bean
import com.chwl.core.bean.game.GameRoomInfo
sealed class GameInfoUiState {
object Loading : GameInfoUiState()
object Success : GameInfoUiState()
data class Failed(val throwable: Throwable, val gameInfo: GameRoomInfo?) : GameInfoUiState()
data class Empty(val gameInfo: GameRoomInfo?) : GameInfoUiState()
}

View File

@@ -1,14 +0,0 @@
package com.chwl.app.game.data.bean
import androidx.annotation.Keep
import java.io.Serializable
@Keep
class GameModeBean : Serializable {
val ticket: Long? = null
val modeIcon: String? = null
val gameMode: Int? = null
val scores: List<Int>? = null
val ruleUrl: String? = null
val modeName: String? = null
}

View File

@@ -1,18 +0,0 @@
package com.chwl.app.game.data.bean
import androidx.annotation.Keep
import java.io.Serializable
@Keep
class GameRoomData : Serializable {
val mgId: String? = null
val gameRoomIcon: String? = null
val configJson: String? = null
val scores: MutableList<Double>? = null
// 匹配状态0:匹配中、1:匹配成功、2:游戏结束、3:匹配失败)
val matchStatus: Int? = null
// 轮次状态0:进行中、1:结束)
val roundStatus: Int? = null
}

View File

@@ -1,8 +0,0 @@
package com.chwl.app.game.data.bean
import androidx.annotation.Keep
import com.chwl.core.bean.room.BaseRoomInfo
@Keep
class GameRoomInfo : BaseRoomInfo<GameRoomData>() {
}

View File

@@ -12,8 +12,8 @@ import androidx.lifecycle.repeatOnLifecycle
import com.chwl.app.R
import com.chwl.app.common.widget.dialog.DialogManager
import com.chwl.app.databinding.GameBuyDialogBinding
import com.chwl.app.game.data.bean.GameConfigBean
import com.chwl.app.game.data.bean.GameModeBean
import com.chwl.core.bean.game.GameConfigBean
import com.chwl.core.bean.game.GameModeBean
import com.chwl.app.game.ui.game.GameActivity
import com.chwl.app.game.ui.game.GameIntent
import com.chwl.app.game.ui.home.GameHomeViewModel

View File

@@ -2,6 +2,8 @@ package com.chwl.app.game.ui.game
import android.content.Context
import android.content.Intent
import android.widget.FrameLayout
import androidx.core.view.isVisible
import androidx.lifecycle.lifecycleScope
import com.chwl.app.R
import com.chwl.app.common.widget.dialog.DialogManager.OkCancelDialogListener
@@ -9,18 +11,17 @@ import com.chwl.app.databinding.GameActivityBinding
import com.chwl.app.game.core.BaseGameActivity
import com.chwl.app.game.core.GameContext
import com.chwl.app.game.core.GameStateAbility
import com.chwl.app.game.core.engine.model.GameViewInfoModel.GameViewRectModel
import com.chwl.app.game.ui.game.widgets.bottom.GameBottomWidget
import com.chwl.app.game.ui.game.widgets.message.GameMessageWidget
import com.chwl.app.game.ui.game.widgets.queue.GameQueueWidget
import com.chwl.app.ui.utils.ImageLoadUtilsV2
import com.chwl.app.ui.utils.load
import com.chwl.app.ui.utils.loadImage
import com.chwl.core.sud.model.GameViewInfoModel
import com.chwl.core.support.room.RoomView
import com.example.lib_utils.ktx.getColorById
import com.example.lib_utils.ktx.singleClick
import com.example.lib_utils.log.ILog
import com.example.lib_utils.spannable.SpannableTextBuilder
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.zip
import kotlinx.coroutines.launch
class GameActivity : BaseGameActivity<GameActivityBinding>(), RoomView, ILog {
@@ -41,14 +42,13 @@ class GameActivity : BaseGameActivity<GameActivityBinding>(), RoomView, ILog {
return
}
super.init()
initGameEngine()
viewModel.init(intentData)
}
private fun initGameEngine() {
gameEngineViewModel.init(lifecycle, binding.layoutGame)
override fun initView() {
super.initView()
binding.spaceGameRect.post {
val rect = GameViewRectModel().apply {
val rect = GameViewInfoModel.GameViewRectModel().apply {
top = binding.spaceGameRect.top
bottom = binding.root.height - binding.spaceGameRect.bottom
}
@@ -65,14 +65,8 @@ class GameActivity : BaseGameActivity<GameActivityBinding>(), RoomView, ILog {
}
}
override fun initObserver() {
super.initObserver()
viewModel.gameContextLiveData.observe(this) {
updateGameContext(it)
}
}
private fun updateGameContext(gameContext: GameContext) {
override fun onGameContext(gameContext: GameContext) {
super.onGameContext(gameContext)
val stateAbility =
gameContext.findAbility<GameStateAbility>(GameStateAbility::class.java.simpleName)
?: return
@@ -80,30 +74,49 @@ class GameActivity : BaseGameActivity<GameActivityBinding>(), RoomView, ILog {
stateAbility.gameIconFlow.collectLatest {
binding.ivLogo.loadImage(it)
}
}
lifecycleScope.launch {
stateAbility.scoresFlow.collectLatest {
val number = it?.first()?.toString() ?: ""
binding.tvAwardValue.text = number
binding.tvAwardTips.text = getString(R.string.game_award_tips_format, number)
updateAward(it?.first())
}
}
lifecycleScope.launch {
stateAbility.matchFailedFlow.collectLatest {
showMatchFailed()
}
updateGameEngine(stateAbility)
}
}
private suspend fun updateGameEngine(stateAbility: GameStateAbility) {
stateAbility.imIdFlow.zip(stateAbility.roomIdFlow) { imId, roomId ->
if (imId != null && roomId != null) {
Pair(imId, roomId)
} else {
null
private fun updateAward(number: Double?) {
val numberStr = number?.toInt()?.toString() ?: ""
if (numberStr.isEmpty()) {
binding.layoutAward.isVisible = false
binding.tvAwardTips.isVisible = false
} else {
binding.layoutAward.isVisible = true
binding.tvAwardTips.isVisible = true
binding.tvAwardValue.text = numberStr
val awardTips = getString(R.string.game_award_tips_format, numberStr)
SpannableTextBuilder(binding.tvAwardTips).appendText(awardTips)
.setTextStyle(numberStr, textColor = getColorById(R.color.color_FF6629)).apply()
}
}
override fun initObserver() {
super.initObserver()
lifecycleScope.launch {
viewModel.closeRoomFlow.collectLatest {
loadingDialogManager?.dismissDialog()
finish()
}
}.collectLatest {
if (it != null) {
gameEngineViewModel.loadGame(
this@GameActivity,
it.second.toString(),
it.first
)
}
lifecycleScope.launch {
viewModel.restartFlow.collectLatest {
loadingDialogManager?.dismissDialog()
// TODO 待完善
}
}
}
@@ -120,17 +133,41 @@ class GameActivity : BaseGameActivity<GameActivityBinding>(), RoomView, ILog {
showExitTips()
}
private fun showExitTips() {
dialogManager.showOkCancelDialog(
getString(R.string.game_exit_tips),
getString(R.string.layout_dialog_game_exit_04),
getString(R.string.exit_text), object : OkCancelDialogListener {
override fun getGameViewGroup(): FrameLayout {
return binding.layoutGame
}
private fun showMatchFailed() {
dialogManager.showOkCancelDialog(null,
getString(R.string.game_match_failed),
getString(R.string.game_rematch),
getString(R.string.exit_text), false, false, object : OkCancelDialogListener {
override fun onOk() {
loadingDialogManager?.showProgressDialog(this@GameActivity)
viewModel.restart()
}
override fun onCancel() {
super.onCancel()
this@GameActivity.finish()
loadingDialogManager?.showProgressDialog(this@GameActivity)
viewModel.closeRoom()
}
})
}
private fun showExitTips() {
dialogManager.showOkCancelDialog(null,
getString(R.string.game_exit_tips),
getString(R.string.layout_dialog_game_exit_04),
getString(R.string.exit_text), false, false, object : OkCancelDialogListener {
override fun onOk() {
dialogManager.dismissDialog()
}
override fun onCancel() {
super.onCancel()
loadingDialogManager?.showProgressDialog(this@GameActivity)
viewModel.closeRoom()
}
})
}

View File

@@ -1,14 +1,26 @@
package com.chwl.app.game.ui.game
import androidx.lifecycle.MutableLiveData
import com.chwl.app.R
import com.chwl.app.base.BaseViewModel
import com.chwl.app.game.core.GameContext
import com.chwl.app.game.data.GameModel2
import com.chwl.core.bean.response.BeanResult
import com.chwl.library.utils.ResUtil
import kotlinx.coroutines.flow.MutableSharedFlow
class GameViewModel : BaseViewModel() {
val gameContextLiveData = MutableLiveData<GameContext>()
val closeRoomFlow = MutableSharedFlow<BeanResult<String?>>()
val restartFlow = MutableSharedFlow<BeanResult<String?>>()
private var gameIntent: GameIntent? = null
fun init(intent: GameIntent) {
this.gameIntent = intent
val gameContext = GameContext(intent.roomId)
gameContext.performStart()
gameContextLiveData.value = gameContext
@@ -18,4 +30,34 @@ class GameViewModel : BaseViewModel() {
super.onCleared()
gameContextLiveData.value?.performStop()
}
fun restart() {
val intent = gameIntent
if (intent == null) {
safeLaunch {
restartFlow.emit(BeanResult.failed(NullPointerException(ResUtil.getString(R.string.utils_net_beanobserver_05))))
}
return
}
safeLaunch {
val result = GameModel2.startGame(intent.gameId.toString(), intent.gameMode)
// TODO 待完善
}
}
fun closeRoom() {
val roomId = gameContextLiveData.value?.roomId
if (roomId != null) {
safeLaunch(onError = {
closeRoomFlow.emit(BeanResult.failed(it))
}) {
GameModel2.closeGame(roomId)
closeRoomFlow.emit(BeanResult.success(null))
}
} else {
safeLaunch {
closeRoomFlow.emit(BeanResult.success(null))
}
}
}
}

View File

@@ -6,7 +6,15 @@ import android.view.LayoutInflater
import androidx.databinding.DataBindingUtil
import com.chwl.app.R
import com.chwl.app.databinding.GameQueueWidgetBinding
import com.chwl.app.game.core.GameQueueAbility
import com.chwl.app.game.core.GameStateAbility
import com.chwl.app.ui.utils.loadAvatar
import com.chwl.core.auth.AuthModel
import com.chwl.core.bean.room.RoomMicBean
import com.chwl.core.support.room.FrameLayoutRoomWidget
import com.chwl.core.support.room.RoomContext
import com.chwl.core.support.room.RoomView
import kotlinx.coroutines.flow.collectLatest
class GameQueueWidget : FrameLayoutRoomWidget {
@@ -32,11 +40,52 @@ class GameQueueWidget : FrameLayoutRoomWidget {
defStyleRes: Int
) : super(context, attrs, defStyleAttr, defStyleRes)
init {
binding.tvState.setText("AAAA")
override fun onInitialize(roomView: RoomView, roomContext: RoomContext) {
super.onInitialize(roomView, roomContext)
val stateAbility =
roomContext.findAbility<GameStateAbility>(GameStateAbility::class.java.simpleName)
val queueAbility =
roomContext.findAbility<GameQueueAbility>(GameQueueAbility::class.java.simpleName)
queueAbility?.let {
safeLaunch {
it.queueFlow.collectLatest {
updateQueue(it)
}
}
}
stateAbility?.let {
safeLaunch {
it.gameStateFlow.collectLatest {
updateState(it)
}
}
}
}
private fun updateState() {
private fun updateQueue(list: List<RoomMicBean>?) {
val selfInfo = list?.firstOrNull {
it.micUser?.uid == AuthModel.get().currentUid
}
val targetInfo = list?.firstOrNull {
it.micUser?.uid != AuthModel.get().currentUid
}
binding.ivQueue1.loadAvatar(selfInfo?.micUser?.avatar)
binding.ivQueue2.loadAvatar(targetInfo?.micUser?.avatar)
}
private fun updateState(gameState: Int?) {
when (gameState) {
1, 2 -> {
binding.tvState.setText(R.string.match_successfully)
binding.ivState.setImageResource(R.drawable.game_ic_vs)
}
else -> {
binding.tvState.setText(R.string.matchmaking)
binding.ivState.setImageResource(R.drawable.game_ic_link)
}
}
}
}

View File

@@ -4,7 +4,7 @@ import android.widget.ImageView
import com.chad.library.adapter.base.BaseQuickAdapter
import com.chad.library.adapter.base.BaseViewHolder
import com.chwl.app.R
import com.chwl.app.game.data.bean.GameModeBean
import com.chwl.core.bean.game.GameModeBean
import com.chwl.app.ui.utils.load
class GameHomeAdapter : BaseQuickAdapter<GameModeBean, BaseViewHolder>(R.layout.game_home_item) {

View File

@@ -4,7 +4,7 @@ import androidx.fragment.app.activityViewModels
import com.chwl.app.MainTabContentView
import com.chwl.app.base.BaseViewBindingFragment
import com.chwl.app.databinding.GameHomeFragmentBinding
import com.chwl.app.game.data.bean.GameConfigBean
import com.chwl.core.bean.game.GameConfigBean
import com.chwl.app.game.ui.buy.GameBuyDialog
import com.chwl.app.support.FragmentVisibleStateHelper
import com.chwl.app.ui.pay.ChargeActivity

View File

@@ -3,7 +3,7 @@ package com.chwl.app.game.ui.home
import androidx.lifecycle.MutableLiveData
import com.chwl.app.base.BaseViewModel
import com.chwl.app.game.data.GameModel2
import com.chwl.app.game.data.bean.GameConfigBean
import com.chwl.core.bean.game.GameConfigBean
import com.chwl.core.bean.response.BeanResult
import com.chwl.core.pay.PayModel
import kotlinx.coroutines.flow.MutableSharedFlow

View File

@@ -60,10 +60,12 @@
android:layout_marginTop="@dimen/dp_9"
android:textColor="@color/white"
android:textSize="@dimen/dp_14"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/queue_widget"
tools:text="@string/game_award_tips_format" />
tools:text="@string/game_award_tips_format"
tools:visibility="visible" />
<FrameLayout
android:id="@+id/layout_award"
@@ -71,8 +73,10 @@
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/dp_21"
android:layout_marginTop="@dimen/dp_22"
android:visibility="gone"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_award_tips">
app:layout_constraintTop_toBottomOf="@id/tv_award_tips"
tools:visibility="visible">
<FrameLayout
android:layout_width="wrap_content"
@@ -89,7 +93,6 @@
android:minWidth="@dimen/dp_56"
android:paddingStart="@dimen/dp_10"
android:paddingEnd="@dimen/dp_10"
android:text="0"
android:textColor="@color/white"
android:textSize="@dimen/dp_17" />
</FrameLayout>

View File

@@ -1,8 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="matchmaking">匹配中</string>
<string name="match_successfully">匹配成功</string>
<string name="game_award_tips_format">獲勝獎勵%s金幣</string>
<string name="start">开始</string>
<string name="game_ticket_format">入场费(%s)</string>
<string name="game_exit_tips">遊戲已經開始,退出房間將默認遊戲失敗,確認退出房間?</string>
<string name="game_match_failed">匹配失敗,是否重新開始?</string>
<string name="game_rematch">重新匹配</string>
</resources>

View File

@@ -2,8 +2,11 @@
<resources>
<string name="matchmaking">匹配中</string>
<string name="match_successfully">匹配成功</string>
<string name="game_award_tips_format">獲勝獎勵%s金幣</string>
<string name="start">開始</string>
<string name="game_ticket_format">入场费(%s)</string>
<string name="game_exit_tips">遊戲已經開始,退出房間將默認遊戲失敗,確認退出房間?</string>
<string name="game_match_failed">匹配失敗,是否重新開始?</string>
<string name="game_rematch">重新匹配</string>
</resources>

View File

@@ -1,8 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="matchmaking">匹配中</string>
<string name="match_successfully">匹配成功</string>
<string name="game_award_tips_format">獲勝獎勵%s金幣</string>
<string name="start">开始</string>
<string name="game_ticket_format">入场费(%s)</string>
<string name="game_exit_tips">遊戲已經開始,退出房間將默認遊戲失敗,確認退出房間?</string>
<string name="game_match_failed">匹配失敗,是否重新開始?</string>
<string name="game_rematch">重新匹配</string>
</resources>