diff --git a/battle_srv/models/room.go b/battle_srv/models/room.go index ea71837..6a8a718 100644 --- a/battle_srv/models/room.go +++ b/battle_srv/models/room.go @@ -26,10 +26,10 @@ const ( UPSYNC_MSG_ACT_PLAYER_CMD = int32(2) UPSYNC_MSG_ACT_PLAYER_COLLIDER_ACK = int32(3) - DOWNSYNC_MSG_ACT_HB_REQ = int32(1) - DOWNSYNC_MSG_ACT_INPUT_BATCH = int32(2) - DOWNSYNC_MSG_ACT_ROOM_FRAME = int32(3) - DOWNSYNC_MSG_ACT_FORCED_RESYNC = int32(4) + DOWNSYNC_MSG_ACT_HB_REQ = int32(1) + DOWNSYNC_MSG_ACT_INPUT_BATCH = int32(2) + DOWNSYNC_MSG_ACT_BATTLE_STOPPED = int32(3) + DOWNSYNC_MSG_ACT_FORCED_RESYNC = int32(4) DOWNSYNC_MSG_ACT_BATTLE_READY_TO_START = int32(-1) DOWNSYNC_MSG_ACT_BATTLE_START = int32(0) @@ -334,6 +334,10 @@ func (pR *Room) ConvertToInputFrameId(renderFrameId int32, inputDelayFrames int3 return ((renderFrameId - inputDelayFrames) >> pR.InputScaleFrames) } +func (pR *Room) ConvertToGeneratingRenderFrameId(inputFrameId int32) int32 { + return (inputFrameId << pR.InputScaleFrames) +} + func (pR *Room) ConvertToFirstUsedRenderFrameId(inputFrameId int32, inputDelayFrames int32) int32 { return ((inputFrameId << pR.InputScaleFrames) + inputDelayFrames) } @@ -349,6 +353,10 @@ func (pR *Room) EncodeUpsyncCmd(upsyncCmd *pb.InputFrameUpsync) uint64 { return ret } +func (pR *Room) RenderFrameBufferString() string { + return fmt.Sprintf("{renderFrameId: %d, stRenderFrameId: %d, edRenderFrameId: %d, lastAllConfirmedRenderFrameId: %d}", pR.RenderFrameId, pR.RenderFrameBuffer.StFrameId, pR.RenderFrameBuffer.EdFrameId, pR.CurDynamicsRenderFrameId) +} + func (pR *Room) AllPlayerInputsBufferString(allDetails bool) string { if allDetails { // Appending of the array of strings can be very SLOW due to on-demand heap allocation! Use this printing with caution. @@ -366,7 +374,7 @@ func (pR *Room) AllPlayerInputsBufferString(allDetails bool) string { s = append(s, fmt.Sprintf("{inputFrameId: %v, inputList: %v, confirmedList: %v}", f.InputFrameId, f.InputList, f.ConfirmedList)) } - return strings.Join(s, "\n") + return strings.Join(s, "; ") } else { return fmt.Sprintf("{renderFrameId: %d, stInputFrameId: %d, edInputFrameId: %d, lastAllConfirmedInputFrameIdWithChange: %d, lastAllConfirmedInputFrameId: %d}", pR.RenderFrameId, pR.AllPlayerInputsBuffer.StFrameId, pR.AllPlayerInputsBuffer.EdFrameId, pR.LastAllConfirmedInputFrameIdWithChange, pR.LastAllConfirmedInputFrameId) } @@ -443,7 +451,7 @@ func (pR *Room) StartBattle() { dynamicsDuration = utils.UnixtimeNano() - dynamicsStartedAt } - lastAllConfirmedInputFrameIdWithChange := atomic.LoadInt32(&(pR.LastAllConfirmedInputFrameIdWithChange)) + upperToSendInputFrameId := atomic.LoadInt32(&(pR.LastAllConfirmedInputFrameId)) for playerId, player := range pR.Players { if swapped := atomic.CompareAndSwapInt32(&player.BattleState, PlayerBattleStateIns.ACTIVE, PlayerBattleStateIns.ACTIVE); !swapped { // [WARNING] DON'T send anything if the player is disconnected, because it could jam the channel and cause significant delay upon "battle recovery for reconnected player". @@ -463,46 +471,65 @@ func (pR *Room) StartBattle() { } // [WARNING] EDGE CASE HERE: Upon initialization, all of "lastAllConfirmedInputFrameId", "lastAllConfirmedInputFrameIdWithChange" and "anchorInputFrameId" are "-1", thus "candidateToSendInputFrameId" starts with "0", however "inputFrameId: 0" might not have been all confirmed! - debugSendingInputFrameId := int32(-1) - for candidateToSendInputFrameId <= lastAllConfirmedInputFrameIdWithChange { + for candidateToSendInputFrameId <= upperToSendInputFrameId { tmp := pR.AllPlayerInputsBuffer.GetByFrameId(candidateToSendInputFrameId) if nil == tmp { panic(fmt.Sprintf("Required inputFrameId=%v for roomId=%v, playerId=%v doesn't exist! AllPlayerInputsBuffer=%v", candidateToSendInputFrameId, pR.Id, playerId, pR.AllPlayerInputsBufferString(false))) } f := tmp.(*pb.InputFrameDownsync) if pR.inputFrameIdDebuggable(candidateToSendInputFrameId) { - debugSendingInputFrameId = candidateToSendInputFrameId Logger.Debug("inputFrame lifecycle#3[sending]:", zap.Any("roomId", pR.Id), zap.Any("playerId", playerId), zap.Any("playerAckingInputFrameId", player.AckingInputFrameId), zap.Any("inputFrameId", candidateToSendInputFrameId), zap.Any("inputFrameId-doublecheck", f.InputFrameId), zap.Any("AllPlayerInputsBuffer", pR.AllPlayerInputsBufferString(false)), zap.Any("ConfirmedList", f.ConfirmedList)) } toSendInputFrames = append(toSendInputFrames, f) candidateToSendInputFrameId++ } + if 0 >= len(toSendInputFrames) { + // [WARNING] When sending DOWNSYNC_MSG_ACT_FORCED_RESYNC, there MUST BE accompanying "toSendInputFrames" for calculating "refRenderFrameId"! + continue + } + indiceInJoinIndexBooleanArr := uint32(player.JoinIndex - 1) var joinMask uint64 = (1 << indiceInJoinIndexBooleanArr) if 0 < (unconfirmedMask & joinMask) { - refRenderFrameId := pR.CurDynamicsRenderFrameId + /* + [WARNING] + "refRenderFrameId" MUST BE CAPPED somehow by "candidateToSendInputFrameId-1", if frontend resyncs itself to a more advanced value than given below, upon the next renderFrame tick on the frontend it might generate non-consecutive "nextInputFrameId > frontend.recentInputCache.edFrameId+1". + + If "NstDelayFrames" becomes larger, "pR.RenderFrameId - refRenderFrameId" possibly becomes larger because the force confirmation is delayed more. + + Hence even upon resync, it's still possible that "refRenderFrameId < frontend.chaserRenderFrameId". + */ + refRenderFrameId := pR.ConvertToGeneratingRenderFrameId(upperToSendInputFrameId) + (1 << pR.InputScaleFrames) - 1 + // [WARNING] The following inequalities are seldom true, but just to avoid that in good network condition the frontend resyncs itself to a "too advanced frontend.renderFrameId", and then starts upsyncing "too advanced inputFrameId". if refRenderFrameId > pR.RenderFrameId { - // [WARNING] To avoid that in good network condition the frontend resyncs itself to a "too advanced frontend.renderFrameId", and then starts upsyncing "too advanced inputFrameId". refRenderFrameId = pR.RenderFrameId } - refRenderFrame := pR.RenderFrameBuffer.GetByFrameId(refRenderFrameId).(*pb.RoomDownsyncFrame) + if refRenderFrameId > pR.CurDynamicsRenderFrameId { + refRenderFrameId = pR.CurDynamicsRenderFrameId + } + tmp := pR.RenderFrameBuffer.GetByFrameId(refRenderFrameId) + if nil == tmp { + panic(fmt.Sprintf("Required refRenderFrameId=%v for roomId=%v, playerId=%v, candidateToSendInputFrameId=%v doesn't exist! AllPlayerInputsBuffer=%v, RenderFrameBuffer=%v", refRenderFrameId, pR.Id, playerId, candidateToSendInputFrameId, pR.AllPlayerInputsBufferString(false), pR.RenderFrameBufferString())) + } + refRenderFrame := tmp.(*pb.RoomDownsyncFrame) pR.sendSafely(refRenderFrame, toSendInputFrames, DOWNSYNC_MSG_ACT_FORCED_RESYNC, playerId) } else { - if 0 >= len(toSendInputFrames) { - continue - } pR.sendSafely(nil, toSendInputFrames, DOWNSYNC_MSG_ACT_INPUT_BATCH, playerId) } - if -1 != debugSendingInputFrameId { - Logger.Info("inputFrame lifecycle#4[sent]:", zap.Any("roomId", pR.Id), zap.Any("playerId", playerId), zap.Any("playerAckingInputFrameId", player.AckingInputFrameId), zap.Any("inputFrameId", debugSendingInputFrameId), zap.Any("AllPlayerInputsBuffer", pR.AllPlayerInputsBufferString(false))) - } atomic.StoreInt32(&(pR.Players[playerId].LastSentInputFrameId), candidateToSendInputFrameId-1) } } - for 0 < pR.RenderFrameBuffer.Cnt && pR.RenderFrameBuffer.StFrameId < pR.CurDynamicsRenderFrameId { + renderFrameBuffLowerBound := pR.ConvertToGeneratingRenderFrameId(upperToSendInputFrameId) + (1 << pR.InputScaleFrames) - 1 + if renderFrameBuffLowerBound > pR.RenderFrameId { + renderFrameBuffLowerBound = pR.RenderFrameId + } + if renderFrameBuffLowerBound > pR.CurDynamicsRenderFrameId { + renderFrameBuffLowerBound = pR.CurDynamicsRenderFrameId + } + for 0 < pR.RenderFrameBuffer.Cnt && pR.RenderFrameBuffer.StFrameId < renderFrameBuffLowerBound { _ = pR.RenderFrameBuffer.Pop() } @@ -511,7 +538,7 @@ func (pR *Room) StartBattle() { f := pR.AllPlayerInputsBuffer.Pop().(*pb.InputFrameDownsync) if pR.inputFrameIdDebuggable(f.InputFrameId) { // Popping of an "inputFrame" would be AFTER its being all being confirmed, because it requires the "inputFrame" to be all acked - Logger.Debug("inputFrame lifecycle#5[popped]:", zap.Any("roomId", pR.Id), zap.Any("inputFrameId", f.InputFrameId), zap.Any("AllPlayerInputsBuffer", pR.AllPlayerInputsBufferString(false))) + Logger.Debug("inputFrame lifecycle#4[popped]:", zap.Any("roomId", pR.Id), zap.Any("inputFrameId", f.InputFrameId), zap.Any("AllPlayerInputsBuffer", pR.AllPlayerInputsBufferString(false))) } } @@ -585,13 +612,13 @@ func (pR *Room) OnBattleCmdReceived(pReq *pb.WsReq) { // In Golang 1.12, there's no "compare-and-swap primitive" on a custom struct (or it's pointer, unless it's an unsafe pointer https://pkg.go.dev/sync/atomic@go1.12#CompareAndSwapPointer). Although CAS on custom struct is possible in Golang 1.19 https://pkg.go.dev/sync/atomic@go1.19.1#Value.CompareAndSwap, using a single word is still faster whenever possible. - // [WARNING] No need to use CAS for updating "inputFrameDownsync.InputList[indiceInJoinIndexBooleanArr]", the upsync from frontend takes top priority. - atomic.StoreUint64(&inputFrameDownsync.InputList[indiceInJoinIndexBooleanArr], encodedInput); + // [WARNING] No need to use CAS for updating "inputFrameDownsync.InputList[indiceInJoinIndexBooleanArr]", the upsync from frontend takes top priority. + atomic.StoreUint64(&inputFrameDownsync.InputList[indiceInJoinIndexBooleanArr], encodedInput) newConfirmedList := (oldConfirmedList | joinMask) if swapped := atomic.CompareAndSwapUint64(&(inputFrameDownsync.ConfirmedList), oldConfirmedList, newConfirmedList); !swapped { // [WARNING] Upon this error, the actual input has already been updated, which is an expected result if it caused by the force confirmation from "battleMainLoop". - Logger.Warn(fmt.Sprintf("Failed confirm CAS: roomId=%v, playerId=%v, clientInputFrameId=%v", pR.Id, playerId, clientInputFrameId)) + Logger.Warn(fmt.Sprintf("Failed confirm CAS, might've been forced to all-confirmed: roomId=%v, playerId=%v, clientInputFrameId=%v", pR.Id, playerId, clientInputFrameId)) return } @@ -606,7 +633,11 @@ func (pR *Room) OnBattleCmdReceived(pReq *pb.WsReq) { func (pR *Room) onInputFrameDownsyncAllConfirmed(inputFrameDownsync *pb.InputFrameDownsync, playerId int32) { inputFrameId := inputFrameDownsync.InputFrameId if -1 == pR.LastAllConfirmedInputFrameIdWithChange || false == pR.equalInputLists(inputFrameDownsync.InputList, pR.LastAllConfirmedInputList) { - Logger.Info(fmt.Sprintf("Key inputFrame change: roomId=%v, playerId=%v, newInputFrameId=%v, lastInputFrameId=%v, newInputList=%v, lastInputList=%v, AllPlayerInputsBuffer=%v", pR.Id, playerId, inputFrameId, pR.LastAllConfirmedInputFrameId, inputFrameDownsync.InputList, pR.LastAllConfirmedInputList, pR.AllPlayerInputsBufferString(false))) + if -1 == playerId { + Logger.Info(fmt.Sprintf("Key inputFrame change: roomId=%v, newInputFrameId=%v, lastInputFrameId=%v, newInputList=%v, lastInputList=%v, AllPlayerInputsBuffer=%v", pR.Id, inputFrameId, pR.LastAllConfirmedInputFrameId, inputFrameDownsync.InputList, pR.LastAllConfirmedInputList, pR.AllPlayerInputsBufferString(false))) + } else { + Logger.Info(fmt.Sprintf("Key inputFrame change: roomId=%v, playerId=%v, newInputFrameId=%v, lastInputFrameId=%v, newInputList=%v, lastInputList=%v, AllPlayerInputsBuffer=%v", pR.Id, playerId, inputFrameId, pR.LastAllConfirmedInputFrameId, inputFrameDownsync.InputList, pR.LastAllConfirmedInputList, pR.AllPlayerInputsBufferString(false))) + } atomic.StoreInt32(&(pR.LastAllConfirmedInputFrameIdWithChange), inputFrameId) } atomic.StoreInt32(&(pR.LastAllConfirmedInputFrameId), inputFrameId) // [WARNING] It's IMPORTANT that "pR.LastAllConfirmedInputFrameId" is NOT NECESSARILY CONSECUTIVE, i.e. if one of the players disconnects and reconnects within a considerable amount of frame delays! @@ -614,12 +645,10 @@ func (pR *Room) onInputFrameDownsyncAllConfirmed(inputFrameDownsync *pb.InputFra // To avoid potential misuse of pointers pR.LastAllConfirmedInputList[i] = v } - if pR.inputFrameIdDebuggable(inputFrameId) { - if -1 == playerId { - Logger.Info(fmt.Sprintf("inputFrame lifecycle#2[forced-allconfirmed]: roomId=%v, inputFrameId=%v, lastAllConfirmedInputFrameId=%v, AllPlayerInputsBuffer=%v", pR.Id, inputFrameId, pR.LastAllConfirmedInputFrameId, pR.AllPlayerInputsBufferString(false))) - } else { - Logger.Info(fmt.Sprintf("inputFrame lifecycle#2[allconfirmed]: roomId=%v, playerId=%v, inputFrameId=%v, lastAllConfirmedInputFrameId=%v, AllPlayerInputsBuffer=%v", pR.Id, playerId, inputFrameId, pR.LastAllConfirmedInputFrameId, pR.AllPlayerInputsBufferString(false))) - } + if -1 == playerId { + Logger.Debug(fmt.Sprintf("inputFrame lifecycle#2[forced-allconfirmed]: roomId=%v, AllPlayerInputsBuffer=%v", pR.Id, pR.AllPlayerInputsBufferString(false))) + } else { + Logger.Info(fmt.Sprintf("inputFrame lifecycle#2[allconfirmed]: roomId=%v, playerId=%v, AllPlayerInputsBuffer=%v", pR.Id, playerId, pR.AllPlayerInputsBufferString(false))) } } @@ -648,7 +677,7 @@ func (pR *Room) StopBattleForSettlement() { Players: toPbPlayers(pR.Players), CountdownNanos: -1, // TODO: Replace this magic constant! } - pR.sendSafely(&assembledFrame, nil, DOWNSYNC_MSG_ACT_ROOM_FRAME, playerId) + pR.sendSafely(&assembledFrame, nil, DOWNSYNC_MSG_ACT_BATTLE_STOPPED, playerId) } // Note that `pR.onBattleStoppedForSettlement` will be called by `battleMainLoop`. } @@ -1000,7 +1029,7 @@ func (pR *Room) sendSafely(roomDownsyncFrame *pb.RoomDownsyncFrame, toSendFrames } if err := pR.PlayerDownsyncSessionDict[playerId].WriteMessage(websocket.BinaryMessage, theBytes); nil != err { - panic(fmt.Sprintf("Error sending downsync message: roomId=%v, playerId=%v, roomState=%v, roomEffectivePlayerCount=%v", pR.Id, playerId, pR.State, pR.EffectivePlayerCount)) + panic(fmt.Sprintf("Error sending downsync message: roomId=%v, playerId=%v, roomState=%v, roomEffectivePlayerCount=%v, err=%v", pR.Id, playerId, pR.State, pR.EffectivePlayerCount, err)) } } @@ -1053,11 +1082,11 @@ func (pR *Room) forceConfirmationIfApplicable() uint64 { } inputFrameId2 := pR.ConvertToInputFrameId(renderFrameId1, 0) // The inputFrame to force confirmation (if necessary) - if inputFrameId2 < pR.LastAllConfirmedInputFrameId { - // No need to force confirmation, the inputFrames already arrived + if inputFrameId2 < pR.LastAllConfirmedInputFrameId { + // No need to force confirmation, the inputFrames already arrived Logger.Debug(fmt.Sprintf("inputFrameId2=%v is already all-confirmed for roomId=%v[type#1], no need to force confirmation of it", inputFrameId2, pR.Id)) - return 0 - } + return 0 + } tmp := pR.AllPlayerInputsBuffer.GetByFrameId(inputFrameId2) if nil == tmp { panic(fmt.Sprintf("inputFrameId2=%v doesn't exist for roomId=%v, this is abnormal because the server should prefab inputFrameDownsync in a most advanced pace, check the prefab logic! AllPlayerInputsBuffer=%v", inputFrameId2, pR.Id, pR.AllPlayerInputsBufferString(false))) @@ -1067,7 +1096,7 @@ func (pR *Room) forceConfirmationIfApplicable() uint64 { totPlayerCnt := uint32(pR.Capacity) allConfirmedMask := uint64((1 << totPlayerCnt) - 1) if swapped := atomic.CompareAndSwapUint64(&(inputFrame2.ConfirmedList), allConfirmedMask, allConfirmedMask); swapped { - // This could happen if the frontend upsync command arrived between type#1 and type#2 checks. + // This could happen if the frontend upsync command arrived between type#1 and type#2 checks. Logger.Debug(fmt.Sprintf("inputFrameId2=%v is already all-confirmed for roomId=%v[type#2], no need to force confirmation of it", inputFrameId2, pR.Id)) return 0 } @@ -1087,21 +1116,22 @@ func (pR *Room) applyInputFrameDownsyncDynamics(fromRenderFrameId int32, toRende } Logger.Debug(fmt.Sprintf("Applying inputFrame dynamics: roomId=%v, room.RenderFrameId=%v, fromRenderFrameId=%v, toRenderFrameId=%v", pR.Id, pR.RenderFrameId, fromRenderFrameId, toRenderFrameId)) - totPlayerCnt := uint32(pR.Capacity) allConfirmedMask := uint64((1 << totPlayerCnt) - 1) for collisionSysRenderFrameId := fromRenderFrameId; collisionSysRenderFrameId < toRenderFrameId; collisionSysRenderFrameId++ { delayedInputFrameId := pR.ConvertToInputFrameId(collisionSysRenderFrameId, pR.InputDelayFrames) if 0 <= delayedInputFrameId { + if delayedInputFrameId > pR.LastAllConfirmedInputFrameId { + panic(fmt.Sprintf("delayedInputFrameId=%v is not yet all-confirmed for roomId=%v, this is abnormal because it's to be used for applying dynamics to [fromRenderFrameId:%v, toRenderFrameId:%v) @ collisionSysRenderFrameId=%v! AllPlayerInputsBuffer=%v", delayedInputFrameId, pR.Id, fromRenderFrameId, toRenderFrameId, collisionSysRenderFrameId, pR.AllPlayerInputsBufferString(false))) + } tmp := pR.AllPlayerInputsBuffer.GetByFrameId(delayedInputFrameId) if nil == tmp { panic(fmt.Sprintf("delayedInputFrameId=%v doesn't exist for roomId=%v, this is abnormal because it's to be used for applying dynamics to [fromRenderFrameId:%v, toRenderFrameId:%v) @ collisionSysRenderFrameId=%v! AllPlayerInputsBuffer=%v", delayedInputFrameId, pR.Id, fromRenderFrameId, toRenderFrameId, collisionSysRenderFrameId, pR.AllPlayerInputsBufferString(false))) } delayedInputFrame := tmp.(*pb.InputFrameDownsync) - if swapped := atomic.CompareAndSwapUint64(&(delayedInputFrame.ConfirmedList), allConfirmedMask, allConfirmedMask); !swapped { - panic(fmt.Sprintf("delayedInputFrameId=%v is not yet all-confirmed for roomId=%v, this is abnormal because it's to be used for applying dynamics to [fromRenderFrameId:%v, toRenderFrameId:%v) @ collisionSysRenderFrameId=%v! AllPlayerInputsBuffer=%v", delayedInputFrameId, pR.Id, fromRenderFrameId, toRenderFrameId, collisionSysRenderFrameId, pR.AllPlayerInputsBufferString(false))) - } + // [WARNING] It's possible that by now "allConfirmedMask != delayedInputFrame.ConfirmedList && delayedInputFrameId <= pR.LastAllConfirmedInputFrameId", we trust "pR.LastAllConfirmedInputFrameId" as the TOP AUTHORITY. + atomic.StoreUint64(&(delayedInputFrame.ConfirmedList), allConfirmedMask) inputList := delayedInputFrame.InputList // Ordered by joinIndex to guarantee determinism diff --git a/frontend/assets/scenes/login.fire b/frontend/assets/scenes/login.fire index cee1e6c..15d9cdf 100644 --- a/frontend/assets/scenes/login.fire +++ b/frontend/assets/scenes/login.fire @@ -440,7 +440,7 @@ "array": [ 0, 0, - 209.63606633899616, + 216.6425019058577, 0, 0, 0, diff --git a/frontend/assets/scripts/Map.js b/frontend/assets/scripts/Map.js index d719ab1..84ba905 100644 --- a/frontend/assets/scripts/Map.js +++ b/frontend/assets/scripts/Map.js @@ -98,52 +98,35 @@ cc.Class({ default: null }, }, - + _inputFrameIdDebuggable(inputFrameId) { - return (0 == inputFrameId%10); + return (0 == inputFrameId % 10); }, - _dumpToRenderCache: function(roomDownsyncFrame) { + dumpToRenderCache: function(roomDownsyncFrame) { const self = this; - const minToKeepRenderFrameId = self.lastAllConfirmedRenderFrameId; + const minToKeepRenderFrameId = self.lastAllConfirmedRenderFrameId; while (0 < self.recentRenderCache.cnt && self.recentRenderCache.stFrameId < minToKeepRenderFrameId) { self.recentRenderCache.pop(); } - if (self.recentRenderCache.stFrameId < minToKeepRenderFrameId) { - console.warn("Weird dumping of RENDER frame: self.renderFrame=", self.renderFrame, ", self.recentInputCache=", self._stringifyRecentInputCache(false), ", self.recentRenderCache=", self._stringifyRecentRenderCache(false), ", self.lastAllConfirmedRenderFrameId=", self.lastAllConfirmedRenderFrameId, ", self.lastAllConfirmedInputFrameId=", self.lastAllConfirmedInputFrameId); - } - const existing = self.recentRenderCache.getByFrameId(roomDownsyncFrame.id); - if (null != existing) { - existing.players = roomDownsyncFrame.players; - existing.sentAt = roomDownsyncFrame.sentAt; - existing.countdownNanos = roomDownsyncFrame.countdownNanos; - existing.treasures = roomDownsyncFrame.treasures; - existing.bullets = roomDownsyncFrame.bullets; - existing.speedShoes = roomDownsyncFrame.speedShoes; - existing.guardTowers = roomDownsyncFrame.guardTowers; - existing.playerMetas = roomDownsyncFrame.playerMetas; - } else { - self.recentRenderCache.put(roomDownsyncFrame); - } + const ret = self.recentRenderCache.setByFrameId(roomDownsyncFrame, roomDownsyncFrame.id); + return ret; }, - _dumpToInputCache: function(inputFrameDownsync) { + dumpToInputCache: function(inputFrameDownsync) { const self = this; let minToKeepInputFrameId = self._convertToInputFrameId(self.lastAllConfirmedRenderFrameId, self.inputDelayFrames); // [WARNING] This could be different from "self.lastAllConfirmedInputFrameId". We'd like to keep the corresponding inputFrame for "self.lastAllConfirmedRenderFrameId" such that a rollback could place "self.chaserRenderFrameId = self.lastAllConfirmedRenderFrameId" for the worst case incorrect prediction. - if (minToKeepInputFrameId > self.lastAllConfirmedInputFrameId) minToKeepInputFrameId = self.lastAllConfirmedInputFrameId; + if (minToKeepInputFrameId > self.lastAllConfirmedInputFrameId) { + minToKeepInputFrameId = self.lastAllConfirmedInputFrameId; + } while (0 < self.recentInputCache.cnt && self.recentInputCache.stFrameId < minToKeepInputFrameId) { self.recentInputCache.pop(); } - if (self.recentInputCache.stFrameId < minToKeepInputFrameId) { - console.warn("Weird dumping of INPUT frame: self.renderFrame=", self.renderFrame, ", self.recentInputCache=", self._stringifyRecentInputCache(false), ", self.recentRenderCache=", self._stringifyRecentRenderCache(false), ", self.lastAllConfirmedRenderFrameId=", self.lastAllConfirmedRenderFrameId, ", self.lastAllConfirmedInputFrameId=", self.lastAllConfirmedInputFrameId); - } - const existing = self.recentInputCache.getByFrameId(inputFrameDownsync.inputFrameId); - if (null != existing) { - existing.inputList = inputFrameDownsync.inputList; - existing.confirmedList = inputFrameDownsync.confirmedList; - } else { - self.recentInputCache.put(inputFrameDownsync); + const ret = self.recentInputCache.setByFrameId(inputFrameDownsync, inputFrameDownsync.inputFrameId); + if (-1 < self.lastAllConfirmedInputFrameId && self.recentInputCache.stFrameId > self.lastAllConfirmedInputFrameId) { + console.error("Invalid input cache dumped! lastAllConfirmedRenderFrameId=", self.lastAllConfirmedRenderFrameId, ", lastAllConfirmedInputFrameId=", self.lastAllConfirmedInputFrameId, ", recentRenderCache=", self._stringifyRecentRenderCache(false), ", recentInputCache=", self._stringifyRecentInputCache(false)); } + return ret; }, _convertToInputFrameId(renderFrameId, inputDelayFrames) { @@ -156,12 +139,12 @@ cc.Class({ }, shouldGenerateInputFrameUpsync(renderFrameId) { - return ((renderFrameId & ((1 << this.inputScaleFrames)-1)) == 0); + return ((renderFrameId & ((1 << this.inputScaleFrames) - 1)) == 0); }, _allConfirmed(confirmedList) { - return (confirmedList+1) == (1 << this.playerRichInfoDict.size); - }, + return (confirmedList + 1) == (1 << this.playerRichInfoDict.size); + }, _generateInputFrameUpsync(inputFrameId) { const self = this; @@ -174,21 +157,21 @@ cc.Class({ const joinIndex = self.selfPlayerInfo.joinIndex; const discreteDir = self.ctrl.getDiscretizedDirection(); - const previousInputFrameDownsyncWithPrediction = self.getCachedInputFrameDownsyncWithPrediction(inputFrameId); - const prefabbedInputList = (null == previousInputFrameDownsyncWithPrediction ? new Array(self.playerRichInfoDict.size).fill(0) : previousInputFrameDownsyncWithPrediction.inputList.slice()); - prefabbedInputList[(joinIndex-1)] = discreteDir.encodedIdx; - const prefabbedInputFrameDownsync = { + const previousInputFrameDownsyncWithPrediction = self.getCachedInputFrameDownsyncWithPrediction(inputFrameId); + const prefabbedInputList = (null == previousInputFrameDownsyncWithPrediction ? new Array(self.playerRichInfoDict.size).fill(0) : previousInputFrameDownsyncWithPrediction.inputList.slice()); + prefabbedInputList[(joinIndex - 1)] = discreteDir.encodedIdx; + const prefabbedInputFrameDownsync = { inputFrameId: inputFrameId, - inputList: prefabbedInputList, - confirmedList: (1 << (self.selfPlayerInfo.joinIndex-1)) + inputList: prefabbedInputList, + confirmedList: (1 << (self.selfPlayerInfo.joinIndex - 1)) }; - self._dumpToInputCache(prefabbedInputFrameDownsync); // A prefabbed inputFrame, would certainly be adding a new inputFrame to the cache, because server only downsyncs "all-confirmed inputFrames" + self.dumpToInputCache(prefabbedInputFrameDownsync); // A prefabbed inputFrame, would certainly be adding a new inputFrame to the cache, because server only downsyncs "all-confirmed inputFrames" - const previousSelfInput = (null == previousInputFrameDownsyncWithPrediction ? null : previousInputFrameDownsyncWithPrediction.inputList[joinIndex-1]); + const previousSelfInput = (null == previousInputFrameDownsyncWithPrediction ? null : previousInputFrameDownsyncWithPrediction.inputList[joinIndex - 1]); return [previousSelfInput, discreteDir.encodedIdx]; }, - + shouldSendInputFrameUpsyncBatch(prevSelfInput, currSelfInput, lastUpsyncInputFrameId, currInputFrameId) { /* For a 2-player-battle, this "shouldUpsyncForEarlyAllConfirmedOnBackend" can be omitted, however for more players in a same battle, to avoid a "long time non-moving player" jamming the downsync of other moving players, we should use this flag. @@ -197,26 +180,31 @@ cc.Class({ */ if (null == currSelfInput) return false; - const shouldUpsyncForEarlyAllConfirmedOnBackend = (currInputFrameId - lastUpsyncInputFrameId >= this.inputFrameUpsyncDelayTolerance); + const shouldUpsyncForEarlyAllConfirmedOnBackend = (currInputFrameId - lastUpsyncInputFrameId >= this.inputFrameUpsyncDelayTolerance); return shouldUpsyncForEarlyAllConfirmedOnBackend || (prevSelfInput != currSelfInput); - }, + }, - sendInputFrameUpsyncBatch(inputFrameId) { - // [WARNING] Why not just send the latest input? Because different player would have a different "inputFrameId" of changing its last input, and that could make the server not recognizing any "all-confirmed inputFrame"! + sendInputFrameUpsyncBatch(latestLocalInputFrameId) { + // [WARNING] Why not just send the latest input? Because different player would have a different "latestLocalInputFrameId" of changing its last input, and that could make the server not recognizing any "all-confirmed inputFrame"! const self = this; let inputFrameUpsyncBatch = []; - for (let i = self.lastUpsyncInputFrameId+1; i <= inputFrameId; ++i) { + let batchInputFrameIdSt = self.lastUpsyncInputFrameId + 1; + if (batchInputFrameIdSt < self.recentInputCache.stFrameId) { + // Upon resync, "self.lastUpsyncInputFrameId" might not have been updated properly. + batchInputFrameIdSt = self.recentInputCache.stFrameId; + } + for (let i = batchInputFrameIdSt; i <= latestLocalInputFrameId; ++i) { const inputFrameDownsync = self.recentInputCache.getByFrameId(i); if (null == inputFrameDownsync) { - console.warn("sendInputFrameUpsyncBatch: recentInputCache is NOT having inputFrameId=", i, "; recentInputCache=", self._stringifyRecentInputCache(false)); + console.error("sendInputFrameUpsyncBatch: recentInputCache is NOT having inputFrameId=", i, ": latestLocalInputFrameId=", latestLocalInputFrameId, ", recentInputCache=", self._stringifyRecentInputCache(false)); } else { const inputFrameUpsync = { inputFrameId: i, - encodedDir: inputFrameDownsync.inputList[self.selfPlayerInfo.joinIndex-1], + encodedDir: inputFrameDownsync.inputList[self.selfPlayerInfo.joinIndex - 1], }; inputFrameUpsyncBatch.push(inputFrameUpsync); } - } + } const reqData = window.WsReq.encode({ msgId: Date.now(), playerId: self.selfPlayerInfo.id, @@ -227,7 +215,7 @@ cc.Class({ inputFrameUpsyncBatch: inputFrameUpsyncBatch, }).finish(); window.sendSafely(reqData); - self.lastUpsyncInputFrameId = inputFrameId; + self.lastUpsyncInputFrameId = latestLocalInputFrameId; }, onEnable() { @@ -244,12 +232,6 @@ cc.Class({ if (null == self.battleState || ALL_BATTLE_STATES.WAITING == self.battleState) { window.clearBoundRoomIdInBothVolatileAndPersistentStorage(); } - if (null != window.handleRoomDownsyncFrame) { - window.handleRoomDownsyncFrame = null; - } - if (null != window.handleInputFrameDownsyncBatch) { - window.handleInputFrameDownsyncBatch = null; - } if (null != window.handleBattleColliderInfo) { window.handleBattleColliderInfo = null; } @@ -309,7 +291,7 @@ cc.Class({ playerRichInfo.node.parent.removeChild(playerRichInfo.node); } }); - } + } self.playerRichInfoDict = new Map(); // Clearing previous info of all players. [ENDS] @@ -317,12 +299,12 @@ cc.Class({ self.lastAllConfirmedRenderFrameId = -1; self.lastAllConfirmedInputFrameId = -1; self.lastUpsyncInputFrameId = -1; - self.chaserRenderFrameId = -1; // at any moment, "lastAllConfirmedRenderFrameId <= chaserRenderFrameId <= renderFrameId", but "chaserRenderFrameId" would fluctuate according to "handleInputFrameDownsyncBatch" + self.chaserRenderFrameId = -1; // at any moment, "lastAllConfirmedRenderFrameId <= chaserRenderFrameId <= renderFrameId", but "chaserRenderFrameId" would fluctuate according to "onInputFrameDownsyncBatch" self.recentRenderCache = new RingBuffer(1024); self.selfPlayerInfo = null; // This field is kept for distinguishing "self" and "others". - self.recentInputCache = new RingBuffer(1024); + self.recentInputCache = new RingBuffer(1024); self.latestCollisionSys = new collisions.Collisions(); self.chaserCollisionSys = new collisions.Collisions(); @@ -330,7 +312,7 @@ cc.Class({ self.collisionBarrierIndexPrefix = (1 << 16); // For tracking the movements of barriers, though not yet actually used self.latestCollisionSysMap = new Map(); self.chaserCollisionSysMap = new Map(); - + self.transitToState(ALL_MAP_STATES.VISUAL); self.battleState = ALL_BATTLE_STATES.WAITING; @@ -378,13 +360,11 @@ cc.Class({ const resultPanelScriptIns = self.resultPanelNode.getComponent("ResultPanel"); resultPanelScriptIns.mapScriptIns = self; resultPanelScriptIns.onAgainClicked = () => { - self.battleState = ALL_BATTLE_STATES.WAITING; + self.battleState = ALL_BATTLE_STATES.WAITING; window.clearBoundRoomIdInBothVolatileAndPersistentStorage(); window.initPersistentSessionClient(self.initAfterWSConnected, null /* Deliberately NOT passing in any `expectedRoomId`. -- YFLu */ ); }; - resultPanelScriptIns.onCloseDelegate = () => { - - }; + resultPanelScriptIns.onCloseDelegate = () => {}; self.gameRuleNode = cc.instantiate(self.gameRulePrefab); self.gameRuleNode.width = self.canvasNode.width; @@ -416,13 +396,13 @@ cc.Class({ /** Init required prefab ended. */ window.handleBattleColliderInfo = function(parsedBattleColliderInfo) { - self.inputDelayFrames = parsedBattleColliderInfo.inputDelayFrames; + self.inputDelayFrames = parsedBattleColliderInfo.inputDelayFrames; self.inputScaleFrames = parsedBattleColliderInfo.inputScaleFrames; self.inputFrameUpsyncDelayTolerance = parsedBattleColliderInfo.inputFrameUpsyncDelayTolerance; - self.rollbackEstimatedDt = 1.0/parsedBattleColliderInfo.serverFps; - self.rollbackEstimatedDtMillis = 1000.0*self.rollbackEstimatedDt; - self.rollbackEstimatedDtToleranceMillis = self.rollbackEstimatedDtMillis/1000.0; + self.rollbackEstimatedDt = 1.0 / parsedBattleColliderInfo.serverFps; + self.rollbackEstimatedDtMillis = 1000.0 * self.rollbackEstimatedDt; + self.rollbackEstimatedDtToleranceMillis = self.rollbackEstimatedDtMillis / 1000.0; self.maxChasingRenderFramesPerUpdate = parsedBattleColliderInfo.maxChasingRenderFramesPerUpdate; const tiledMapIns = self.node.getComponent(cc.TiledMap); @@ -433,7 +413,7 @@ cc.Class({ console.error(err); return; } - + /* [WARNING] @@ -444,12 +424,12 @@ cc.Class({ tiledMapIns.tmxAsset = null; mapNode.removeAllChildren(); - self._resetCurrentMatch(); + self._resetCurrentMatch(); tiledMapIns.tmxAsset = tmxAsset; const newMapSize = tiledMapIns.getMapSize(); const newTileSize = tiledMapIns.getTileSize(); - self.node.setContentSize(newMapSize.width*newTileSize.width, newMapSize.height*newTileSize.height); + self.node.setContentSize(newMapSize.width * newTileSize.width, newMapSize.height * newTileSize.height); self.node.setPosition(cc.v2(0, 0)); /* * Deliberately hiding "ImageLayer"s. This dirty fix is specific to "CocosCreator v2.2.1", where it got back the rendering capability of "ImageLayer of Tiled", yet made incorrectly. In this game our "markers of ImageLayers" are rendered by dedicated prefabs with associated colliders. @@ -458,24 +438,25 @@ cc.Class({ */ const existingImageLayers = tiledMapIns.getObjectGroups(); for (let singleImageLayer of existingImageLayers) { - singleImageLayer.node.opacity = 0; + singleImageLayer.node.opacity = 0; } let barrierIdCounter = 0; const boundaryObjs = tileCollisionManager.extractBoundaryObjects(self.node); for (let boundaryObj of boundaryObjs.barriers) { - const x0 = boundaryObj[0].x, y0 = boundaryObj[0].y; + const x0 = boundaryObj[0].x, + y0 = boundaryObj[0].y; let pts = []; // TODO: Simplify this redundant coordinate conversion within "extractBoundaryObjects", but since this routine is only called once per battle, not urgent. for (let i = 0; i < boundaryObj.length; ++i) { - pts.push([boundaryObj[i].x-x0, boundaryObj[i].y-y0]); - } + pts.push([boundaryObj[i].x - x0, boundaryObj[i].y - y0]); + } const newBarrierLatest = self.latestCollisionSys.createPolygon(x0, y0, pts); const newBarrierChaser = self.chaserCollisionSys.createPolygon(x0, y0, pts); ++barrierIdCounter; - const collisionBarrierIndex = (self.collisionBarrierIndexPrefix + barrierIdCounter); - self.latestCollisionSysMap.set(collisionBarrierIndex, newBarrierLatest); - self.chaserCollisionSysMap.set(collisionBarrierIndex, newBarrierChaser); + const collisionBarrierIndex = (self.collisionBarrierIndexPrefix + barrierIdCounter); + self.latestCollisionSysMap.set(collisionBarrierIndex, newBarrierLatest); + self.chaserCollisionSysMap.set(collisionBarrierIndex, newBarrierChaser); } self.selfPlayerInfo = JSON.parse(cc.sys.localStorage.getItem('selfPlayer')); @@ -495,7 +476,7 @@ cc.Class({ self.backgroundMapTiledIns.tmxAsset = backgroundMapTmxAsset; const newBackgroundMapSize = self.backgroundMapTiledIns.getMapSize(); const newBackgroundMapTileSize = self.backgroundMapTiledIns.getTileSize(); - self.backgroundMapTiledIns.node.setContentSize(newBackgroundMapSize.width*newBackgroundMapTileSize.width, newBackgroundMapSize.height*newBackgroundMapTileSize.height); + self.backgroundMapTiledIns.node.setContentSize(newBackgroundMapSize.width * newBackgroundMapTileSize.width, newBackgroundMapSize.height * newBackgroundMapTileSize.height); self.backgroundMapTiledIns.node.setPosition(cc.v2(0, 0)); const reqData = window.WsReq.encode({ @@ -512,90 +493,6 @@ cc.Class({ self.hideGameRuleNode(); self.transitToState(ALL_MAP_STATES.WAITING); self._inputControlEnabled = false; - - let findingPlayerScriptIns = self.findingPlayerNode.getComponent("FindingPlayer"); - window.handlePlayerAdded = function(rdf) { - // Update the "finding player" GUI and show it if not previously present - if (!self.findingPlayerNode.parent) { - self.showPopupInCanvas(self.findingPlayerNode); - } - findingPlayerScriptIns.updatePlayersInfo(rdf.playerMetas); - }; - - window.handleRoomDownsyncFrame = function(rdf) { - const frameId = rdf.id; - // Right upon establishment of the "PersistentSessionClient", we should receive an initial signal "BattleColliderInfo" earlier than any "RoomDownsyncFrame" containing "PlayerMeta" data. - switch (frameId) { - case window.MAGIC_ROOM_DOWNSYNC_FRAME_ID.BATTLE_READY_TO_START: - self.onBattleReadyToStart(rdf.playerMetas, false); - return; - case window.MAGIC_ROOM_DOWNSYNC_FRAME_ID.BATTLE_START: - self.onBattleStartedOrResynced(rdf); - return; - } - - // TODO: Inject a NetworkDoctor as introduced in https://app.yinxiang.com/shard/s61/nl/13267014/5c575124-01db-419b-9c02-ec81f78c6ddc/. - }; - - window.handleInputFrameDownsyncBatch = function(batch) { - if (ALL_BATTLE_STATES.IN_BATTLE != self.battleState - && ALL_BATTLE_STATES.IN_SETTLEMENT != self.battleState) { - return; - } - - // console.log("Received inputFrameDownsyncBatch=", batch, ", now correspondingLastLocalInputFrame=", self.recentInputCache.getByFrameId(batch[batch.length-1].inputFrameId)); - let firstPredictedYetIncorrectInputFrameId = null; - let firstPredictedYetIncorrectInputFrameJoinIndex = null; - for (let k in batch) { - const inputFrameDownsync = batch[k]; - const inputFrameDownsyncId = inputFrameDownsync.inputFrameId; - const localInputFrame = self.recentInputCache.getByFrameId(inputFrameDownsyncId); - if (null == localInputFrame) { - console.warn("handleInputFrameDownsyncBatch: recentInputCache is NOT having inputFrameDownsyncId=", inputFrameDownsyncId, "; now recentInputCache=", self._stringifyRecentInputCache(false)); - } else { - if (null == firstPredictedYetIncorrectInputFrameId) { - for (let i in localInputFrame.inputList) { - if (localInputFrame.inputList[i] != inputFrameDownsync.inputList[i]) { - firstPredictedYetIncorrectInputFrameId = inputFrameDownsyncId; - firstPredictedYetIncorrectInputFrameJoinIndex = (parseInt(i)+1); - break; - } - } - } - } - self.lastAllConfirmedInputFrameId = inputFrameDownsyncId; - self._dumpToInputCache(inputFrameDownsync); - } - - if (null != firstPredictedYetIncorrectInputFrameId) { - const inputFrameId1 = firstPredictedYetIncorrectInputFrameId; - const renderFrameId1 = self._convertToFirstUsedRenderFrameId(inputFrameId1, self.inputDelayFrames); // a.k.a. "firstRenderFrameIdUsingIncorrectInputFrameId" - if (renderFrameId1 < self.renderFrameId) { - /* - A typical case is as follows. - -------------------------------------------------------- - [self.lastAllConfirmedRenderFrameId] : 22 - - : 36 - - - : 62 - - [self.renderFrameId] : 64 - -------------------------------------------------------- - */ - if (renderFrameId1 < self.chaserRenderFrameId) { - // The actual rollback-and-chase would later be executed in update(dt). - console.warn("Mismatched input detected, resetting chaserRenderFrameId: inputFrameId1:", inputFrameId1, ", renderFrameId1:", renderFrameId1, ", chaserRenderFrameId before reset: ", self.chaserRenderFrameId); - self.chaserRenderFrameId = renderFrameId1; - } else { - // Deliberately left blank, chasing is ongoing. - } - } else { - // No need to rollback when "renderFrameId1 == self.renderFrameId", because the "corresponding delayedInputFrame for renderFrameId2" is NOT YET EXECUTED BY NOW, it just went through "++self.renderFrameId" in "update(dt)" and javascript-runtime is mostly single-threaded in our programmable range. - } - } - }; } // The player is now viewing "self.gameRuleNode" with button(s) to start an actual battle. -- YFLu @@ -608,15 +505,15 @@ cc.Class({ self.disableGameRuleNode(); // The player is now possibly viewing "self.gameRuleNode" with no button, and should wait for `self.initAfterWSConnected` to be called. - self.battleState = ALL_BATTLE_STATES.WAITING; + self.battleState = ALL_BATTLE_STATES.WAITING; window.initPersistentSessionClient(self.initAfterWSConnected, expectedRoomId); } else if (null != boundRoomId) { self.disableGameRuleNode(); - self.battleState = ALL_BATTLE_STATES.WAITING; + self.battleState = ALL_BATTLE_STATES.WAITING; window.initPersistentSessionClient(self.initAfterWSConnected, expectedRoomId); } else { self.showPopupInCanvas(self.gameRuleNode); - // Deliberately left blank. -- YFLu + // Deliberately left blank. -- YFLu } }, @@ -652,10 +549,27 @@ cc.Class({ onBattleStartedOrResynced(rdf) { // This function is also applicable to "re-joining". - console.log('On battle started or resynced! renderFrameId=', rdf.id); const self = window.mapIns; + const dumpRenderCacheRet = self.dumpToRenderCache(rdf); + if (window.RING_BUFF_FAILED_TO_SET == dumpRenderCacheRet) { + console.error("Something is wrong while setting the RingBuffer by frameId!"); + return dumpRenderCacheRet; + } + if (window.MAGIC_ROOM_DOWNSYNC_FRAME_ID.BATTLE_START < rdf.id && window.RING_BUFF_CONSECUTIVE_SET == dumpRenderCacheRet) { + if (rdf.id < self.chaserRenderFrameId) { + // This "rdf.id = backend.refRenderFrameId" could be small, see comments around "room.go". + self.chaserRenderFrameId = rdf.id; + } + // In this case, we'll also got proper all-confirmed inputFrames for advancing the renderFrames in the coming "update(dt)" + return dumpRenderCacheRet; + } + + // The logic below applies to (window.MAGIC_ROOM_DOWNSYNC_FRAME_ID.BATTLE_START == rdf.id || window.RING_BUFF_NON_CONSECUTIVE_SET == dumpRenderCacheRet) + console.log('On battle started or resynced! renderFrameId=', rdf.id); + self.renderFrameId = rdf.id; - self.lastAllConfirmedRenderFrameId = rdf.id; + self.lastRenderFrameIdTriggeredAt = performance.now(); + self.lastAllConfirmedRenderFrameId = rdf.id; self.chaserRenderFrameId = rdf.id; const players = rdf.players; @@ -682,30 +596,105 @@ cc.Class({ self.countdownToBeginGameNode.parent.removeChild(self.countdownToBeginGameNode); } self.transitToState(ALL_MAP_STATES.VISUAL); + self.battleState = ALL_BATTLE_STATES.IN_BATTLE; self.applyRoomDownsyncFrameDynamics(rdf); - self._dumpToRenderCache(rdf); - self.battleState = ALL_BATTLE_STATES.IN_BATTLE; // Starts the increment of "self.renderFrameId" in "self.update(dt)" - self.lastRenderFrameIdTriggeredAt = performance.now(); - if (null != window.boundRoomId) { - self.boundRoomIdLabel.string = window.boundRoomId; + + return dumpRenderCacheRet; + }, + + equalInputLists(lhs, rhs) { + if (null == lhs || null == rhs) return false; + if (lhs.length != rhs.length) return false; + for (let i in lhs) { + if (lhs[i] == rhs[i]) continue; + return false; } + return true; + }, + + onInputFrameDownsyncBatch(batch, dumpRenderCacheRet /* second param is default to null */ ) { + const self = this; + if (ALL_BATTLE_STATES.IN_BATTLE != self.battleState + && ALL_BATTLE_STATES.IN_SETTLEMENT != self.battleState) { + return; + } + + let firstPredictedYetIncorrectInputFrameId = null; + for (let k in batch) { + const inputFrameDownsync = batch[k]; + const inputFrameDownsyncId = inputFrameDownsync.inputFrameId; + if (window.RING_BUFF_NON_CONSECUTIVE_SET == dumpRenderCacheRet) { + // Deliberately left blank, in this case "chaserRenderFrameId" is already reset to proper value. + } else { + const localInputFrame = self.recentInputCache.getByFrameId(inputFrameDownsyncId); + if (null == localInputFrame) { + console.warn("localInputFrame not existing: recentInputCache is NOT having inputFrameDownsyncId=", inputFrameDownsyncId, "; now recentInputCache=", self._stringifyRecentInputCache(false)); + } else if (null == firstPredictedYetIncorrectInputFrameId && !self.equalInputLists(localInputFrame.inputList, inputFrameDownsync.inputList)) { + firstPredictedYetIncorrectInputFrameId = inputFrameDownsyncId; + } + } + self.lastAllConfirmedInputFrameId = inputFrameDownsyncId; + self.dumpToInputCache(inputFrameDownsync); + } + + if (null != firstPredictedYetIncorrectInputFrameId) { + const inputFrameId1 = firstPredictedYetIncorrectInputFrameId; + const renderFrameId1 = self._convertToFirstUsedRenderFrameId(inputFrameId1, self.inputDelayFrames); // a.k.a. "firstRenderFrameIdUsingIncorrectInputFrameId" + if (renderFrameId1 < self.renderFrameId) { + /* + A typical case is as follows. + -------------------------------------------------------- + [self.lastAllConfirmedRenderFrameId] : 22 + + : 36 + + + : 62 + + [self.renderFrameId] : 64 + -------------------------------------------------------- + */ + if (renderFrameId1 < self.chaserRenderFrameId) { + // The actual rollback-and-chase would later be executed in update(dt). + console.warn("Mismatched input detected, resetting chaserRenderFrameId: inputFrameId1:", inputFrameId1, ", renderFrameId1:", renderFrameId1, ", chaserRenderFrameId before reset: ", self.chaserRenderFrameId); + self.chaserRenderFrameId = renderFrameId1; + } else { + // Deliberately left blank, chasing is ongoing. + } + } else { + // No need to rollback when "renderFrameId1 == self.renderFrameId", because the "corresponding delayedInputFrame for renderFrameId2" is NOT YET EXECUTED BY NOW, it just went through "++self.renderFrameId" in "update(dt)" and javascript-runtime is mostly single-threaded in our programmable range. + } + } + }, + + onPlayerAdded(rdf) { + const self = this; + // Update the "finding player" GUI and show it if not previously present + if (!self.findingPlayerNode.parent) { + self.showPopupInCanvas(self.findingPlayerNode); + } + let findingPlayerScriptIns = self.findingPlayerNode.getComponent("FindingPlayer"); + findingPlayerScriptIns.updatePlayersInfo(rdf.playerMetas); }, logBattleStats() { const self = this; let s = []; s.push("Battle stats: lastUpsyncInputFrameId=" + self.lastUpsyncInputFrameId + ", lastAllConfirmedInputFrameId=" + self.lastAllConfirmedInputFrameId); - + for (let i = self.recentInputCache.stFrameId; i < self.recentInputCache.edFrameId; ++i) { - const inputFrameDownsync = self.recentInputCache.getByFrameId(i); + const inputFrameDownsync = self.recentInputCache.getByFrameId(i); s.push(JSON.stringify(inputFrameDownsync)); - } + } console.log(s.join('\n')); }, onBattleStopped() { const self = this; + if (ALL_BATTLE_STATES.IN_BATTLE != self.battleState) { + return; + } self.countdownNanos = null; self.logBattleStats(); if (self.musicEffectManagerScriptIns) { @@ -741,7 +730,10 @@ cc.Class({ newPlayerNode.active = true; const playerScriptIns = newPlayerNode.getComponent("SelfPlayer"); - playerScriptIns.scheduleNewDirection({dx: 0, dy: 0}, true); + playerScriptIns.scheduleNewDirection({ + dx: 0, + dy: 0 + }, true); return [newPlayerNode, playerScriptIns]; }, @@ -750,45 +742,47 @@ cc.Class({ const self = this; if (ALL_BATTLE_STATES.IN_BATTLE == self.battleState) { const elapsedMillisSinceLastFrameIdTriggered = performance.now() - self.lastRenderFrameIdTriggeredAt; - if (elapsedMillisSinceLastFrameIdTriggered < (self.rollbackEstimatedDtMillis)) { + if (elapsedMillisSinceLastFrameIdTriggered < (self.rollbackEstimatedDtMillis)) { // console.debug("Avoiding too fast frame@renderFrameId=", self.renderFrameId, ": elapsedMillisSinceLastFrameIdTriggered=", elapsedMillisSinceLastFrameIdTriggered); return; } try { - let st = performance.now(); - let prevSelfInput = null, currSelfInput = null; - const noDelayInputFrameId = self._convertToInputFrameId(self.renderFrameId, 0); // It's important that "inputDelayFrames == 0" here - if (self.shouldGenerateInputFrameUpsync(self.renderFrameId)) { - const prevAndCurrInputs = self._generateInputFrameUpsync(noDelayInputFrameId); - prevSelfInput = prevAndCurrInputs[0]; - currSelfInput = prevAndCurrInputs[1]; - } + let st = performance.now(); + let prevSelfInput = null, + currSelfInput = null; + const noDelayInputFrameId = self._convertToInputFrameId(self.renderFrameId, 0); // It's important that "inputDelayFrames == 0" here + if (self.shouldGenerateInputFrameUpsync(self.renderFrameId)) { + const prevAndCurrInputs = self._generateInputFrameUpsync(noDelayInputFrameId); + prevSelfInput = prevAndCurrInputs[0]; + currSelfInput = prevAndCurrInputs[1]; + } - let t0 = performance.now(); - if (self.shouldSendInputFrameUpsyncBatch(prevSelfInput, currSelfInput, self.lastUpsyncInputFrameId, noDelayInputFrameId)) { - // TODO: Is the following statement run asynchronously in an implicit manner? Should I explicitly run it asynchronously? - self.sendInputFrameUpsyncBatch(noDelayInputFrameId); - } + let t0 = performance.now(); + if (self.shouldSendInputFrameUpsyncBatch(prevSelfInput, currSelfInput, self.lastUpsyncInputFrameId, noDelayInputFrameId)) { + // TODO: Is the following statement run asynchronously in an implicit manner? Should I explicitly run it asynchronously? + self.sendInputFrameUpsyncBatch(noDelayInputFrameId); + } - let t1 = performance.now(); - // Use "fractional-frame-chasing" to guarantee that "self.update(dt)" is not jammed by a "large range of frame-chasing". See `/ConcerningEdgeCases.md` for the motivation. - const prevChaserRenderFrameId = self.chaserRenderFrameId; - let nextChaserRenderFrameId = (prevChaserRenderFrameId + self.maxChasingRenderFramesPerUpdate); - if (nextChaserRenderFrameId > self.renderFrameId) nextChaserRenderFrameId = self.renderFrameId; - self.rollbackAndChase(prevChaserRenderFrameId, nextChaserRenderFrameId, self.chaserCollisionSys, self.chaserCollisionSysMap); - self.chaserRenderFrameId = nextChaserRenderFrameId; // Move the cursor "self.chaserRenderFrameId", keep in mind that "self.chaserRenderFrameId" is not monotonic! - let t2 = performance.now(); + let t1 = performance.now(); + // Use "fractional-frame-chasing" to guarantee that "self.update(dt)" is not jammed by a "large range of frame-chasing". See `/ConcerningEdgeCases.md` for the motivation. + const prevChaserRenderFrameId = self.chaserRenderFrameId; + let nextChaserRenderFrameId = (prevChaserRenderFrameId + self.maxChasingRenderFramesPerUpdate); + if (nextChaserRenderFrameId > self.renderFrameId) + nextChaserRenderFrameId = self.renderFrameId; + self.rollbackAndChase(prevChaserRenderFrameId, nextChaserRenderFrameId, self.chaserCollisionSys, self.chaserCollisionSysMap); + self.chaserRenderFrameId = nextChaserRenderFrameId; // Move the cursor "self.chaserRenderFrameId", keep in mind that "self.chaserRenderFrameId" is not monotonic! + let t2 = performance.now(); - // Inside "self.rollbackAndChase", the "self.latestCollisionSys" is ALWAYS ROLLED BACK to "self.recentRenderCache.get(self.renderFrameId)" before being applied dynamics from corresponding inputFrameDownsync, REGARDLESS OF whether or not "self.chaserRenderFrameId == self.renderFrameId" now. - const rdf = self.rollbackAndChase(self.renderFrameId, self.renderFrameId+1, self.latestCollisionSys, self.latestCollisionSysMap); - self.applyRoomDownsyncFrameDynamics(rdf); - let t3 = performance.now(); + // Inside "self.rollbackAndChase", the "self.latestCollisionSys" is ALWAYS ROLLED BACK to "self.recentRenderCache.get(self.renderFrameId)" before being applied dynamics from corresponding inputFrameDownsync, REGARDLESS OF whether or not "self.chaserRenderFrameId == self.renderFrameId" now. + const rdf = self.rollbackAndChase(self.renderFrameId, self.renderFrameId + 1, self.latestCollisionSys, self.latestCollisionSysMap); + self.applyRoomDownsyncFrameDynamics(rdf); + let t3 = performance.now(); } catch (err) { console.error("Error during Map.update", err); } finally { // Update countdown if (null != self.countdownNanos) { - self.countdownNanos -= self.rollbackEstimatedDt*1000000000; + self.countdownNanos -= self.rollbackEstimatedDt * 1000000000; if (self.countdownNanos <= 0) { self.onBattleStopped(self.playerRichInfoDict); return; @@ -820,14 +814,16 @@ cc.Class({ const selfPlayerStr = cc.sys.localStorage.getItem("selfPlayer"); if (null == selfPlayerStr) { localClearance(); - return; + return; } const selfPlayerInfo = JSON.parse(selfPlayerStr); try { NetworkUtils.ajax({ url: backendAddress.PROTOCOL + '://' + backendAddress.HOST + ':' + backendAddress.PORT + constants.ROUTE_PATH.API + constants.ROUTE_PATH.PLAYER + constants.ROUTE_PATH.VERSION + constants.ROUTE_PATH.INT_AUTH_TOKEN + constants.ROUTE_PATH.LOGOUT, type: "POST", - data: { intAuthToken: selfPlayerInfo.intAuthToken }, + data: { + intAuthToken: selfPlayerInfo.intAuthToken + }, success: function(res) { if (res.ret != constants.RET_CODE.OK) { console.log("Logout failed: ", res); @@ -862,7 +858,7 @@ cc.Class({ onGameRule1v1ModeClicked(evt, cb) { const self = this; - self.battleState = ALL_BATTLE_STATES.WAITING; + self.battleState = ALL_BATTLE_STATES.WAITING; window.initPersistentSessionClient(self.initAfterWSConnected, null /* Deliberately NOT passing in any `expectedRoomId`. -- YFLu */ ); self.hideGameRuleNode(); }, @@ -888,8 +884,8 @@ cc.Class({ }; if (true == isSelfRejoining) { - hideFindingPlayersGUI(); - } else { + hideFindingPlayersGUI(); + } else { // Delay to hide the "finding player" GUI, then show a countdown clock window.setTimeout(() => { hideFindingPlayersGUI(); @@ -902,10 +898,10 @@ cc.Class({ _createRoomDownsyncFrameLocally(renderFrameId, collisionSys, collisionSysMap) { const self = this; - const prevRenderFrameId = renderFrameId-1; + const prevRenderFrameId = renderFrameId - 1; const inputFrameAppliedOnPrevRenderFrame = ( - 0 > prevRenderFrameId - ? + 0 > prevRenderFrameId + ? null : self.getCachedInputFrameDownsyncWithPrediction(self._convertToInputFrameId(prevRenderFrameId, self.inputDelayFrames)) @@ -914,8 +910,8 @@ cc.Class({ // TODO: Find a better way to assign speeds instead of using "speedRefRenderFrameId". const speedRefRenderFrameId = prevRenderFrameId; const speedRefRenderFrame = ( - 0 > speedRefRenderFrameId - ? + 0 > speedRefRenderFrameId + ? null : self.recentRenderCache.getByFrameId(speedRefRenderFrameId) @@ -924,32 +920,32 @@ cc.Class({ const rdf = { id: renderFrameId, refFrameId: renderFrameId, - players: {} + players: {} }; self.playerRichInfoDict.forEach((playerRichInfo, playerId) => { - const joinIndex = playerRichInfo.joinIndex; + const joinIndex = playerRichInfo.joinIndex; const collisionPlayerIndex = self.collisionPlayerIndexPrefix + joinIndex; const playerCollider = collisionSysMap.get(collisionPlayerIndex); rdf.players[playerRichInfo.id] = { id: playerRichInfo.id, x: playerCollider.x, y: playerCollider.y, - dir: self.ctrl.decodeDirection(null == inputFrameAppliedOnPrevRenderFrame ? 0 : inputFrameAppliedOnPrevRenderFrame.inputList[joinIndex-1]), + dir: self.ctrl.decodeDirection(null == inputFrameAppliedOnPrevRenderFrame ? 0 : inputFrameAppliedOnPrevRenderFrame.inputList[joinIndex - 1]), speed: (null == speedRefRenderFrame ? playerRichInfo.speed : speedRefRenderFrame.players[playerRichInfo.id].speed), - joinIndex: joinIndex + joinIndex: joinIndex }; }); if ( - null != inputFrameAppliedOnPrevRenderFrame && self._allConfirmed(inputFrameAppliedOnPrevRenderFrame.confirmedList) - && + null != inputFrameAppliedOnPrevRenderFrame && self._allConfirmed(inputFrameAppliedOnPrevRenderFrame.confirmedList) + && self.lastAllConfirmedRenderFrameId >= prevRenderFrameId && rdf.id > self.lastAllConfirmedRenderFrameId ) { - self.lastAllConfirmedRenderFrameId = rdf.id; + self.lastAllConfirmedRenderFrameId = rdf.id; self.chaserRenderFrameId = rdf.id; // it must be true that "chaserRenderFrameId >= lastAllConfirmedRenderFrameId" } - self._dumpToRenderCache(rdf); + self.dumpToRenderCache(rdf); return rdf; }, @@ -960,20 +956,20 @@ cc.Class({ const immediatePlayerInfo = rdf.players[playerId]; playerRichInfo.node.setPosition(immediatePlayerInfo.x, immediatePlayerInfo.y); playerRichInfo.scriptIns.scheduleNewDirection(immediatePlayerInfo.dir, true); - playerRichInfo.scriptIns.updateSpeed(immediatePlayerInfo.speed); + playerRichInfo.scriptIns.updateSpeed(immediatePlayerInfo.speed); }); - }, + }, getCachedInputFrameDownsyncWithPrediction(inputFrameId) { const self = this; let inputFrameDownsync = self.recentInputCache.getByFrameId(inputFrameId); if (null != inputFrameDownsync && -1 != self.lastAllConfirmedInputFrameId && inputFrameId > self.lastAllConfirmedInputFrameId) { - const lastAllConfirmedInputFrame = self.recentInputCache.getByFrameId(self.lastAllConfirmedInputFrameId); + const lastAllConfirmedInputFrame = self.recentInputCache.getByFrameId(self.lastAllConfirmedInputFrameId); for (let i = 0; i < inputFrameDownsync.inputList.length; ++i) { - if (i == self.selfPlayerInfo.joinIndex-1) continue; - inputFrameDownsync.inputList[i] = lastAllConfirmedInputFrame.inputList[i]; + if (i == self.selfPlayerInfo.joinIndex - 1) continue; + inputFrameDownsync.inputList[i] = lastAllConfirmedInputFrame.inputList[i]; } - } + } return inputFrameDownsync; }, @@ -986,13 +982,13 @@ cc.Class({ const self = this; const renderFrameSt = self.recentRenderCache.getByFrameId(renderFrameIdSt); // typed "RoomDownsyncFrame" if (null == renderFrameSt) { - console.error("Couldn't find renderFrameId=", renderFrameIdSt, " to rollback, recentRenderCache=", self._stringifyRecentRenderCache(false)); + console.error("Couldn't find renderFrameId=", renderFrameIdSt, " to rollback, lastAllConfirmedRenderFrameId=", self.lastAllConfirmedRenderFrameId, ", lastAllConfirmedInputFrameId=", self.lastAllConfirmedInputFrameId, ", recentRenderCache=", self._stringifyRecentRenderCache(false), ", recentInputCache=", self._stringifyRecentInputCache(false)); } /* Reset "position" of players in "collisionSys" according to "renderFrameSt". The easy part is that we don't have path-dependent-integrals to worry about like that of thermal dynamics. */ self.playerRichInfoDict.forEach((playerRichInfo, playerId) => { - const joinIndex = playerRichInfo.joinIndex; + const joinIndex = playerRichInfo.joinIndex; const collisionPlayerIndex = self.collisionPlayerIndexPrefix + joinIndex; const playerCollider = collisionSysMap.get(collisionPlayerIndex); const player = renderFrameSt.players[playerId]; @@ -1008,30 +1004,29 @@ cc.Class({ for (let i = renderFrameIdSt; i < renderFrameIdEd; ++i) { const renderFrame = self.recentRenderCache.getByFrameId(i); // typed "RoomDownsyncFrame" const j = self._convertToInputFrameId(i, self.inputDelayFrames); - const inputList = self.getCachedInputFrameDownsyncWithPrediction(j).inputList; + const inputFrameDownsync = self.getCachedInputFrameDownsyncWithPrediction(j); + if (null == inputFrameDownsync) { + console.error("Failed to get cached inputFrameDownsync for renderFrameId=", i, ", inputFrameId=", j, "lastAllConfirmedRenderFrameId=", self.lastAllConfirmedRenderFrameId, ", lastAllConfirmedInputFrameId=", self.lastAllConfirmedInputFrameId, ", recentRenderCache=", self._stringifyRecentRenderCache(false), ", recentInputCache=", self._stringifyRecentInputCache(false)); + } + const inputList = inputFrameDownsync.inputList; for (let j in self.playerRichInfoArr) { - const joinIndex = parseInt(j) + 1; - const playerId = self.playerRichInfoArr[j].id; + const joinIndex = parseInt(j) + 1; + const playerId = self.playerRichInfoArr[j].id; const collisionPlayerIndex = self.collisionPlayerIndexPrefix + joinIndex; const playerCollider = collisionSysMap.get(collisionPlayerIndex); const player = renderFrame.players[playerId]; - const encodedInput = inputList[joinIndex-1]; + const encodedInput = inputList[joinIndex - 1]; const decodedInput = self.ctrl.decodeDirection(encodedInput); - const baseChange = player.speed*self.rollbackEstimatedDt*decodedInput.speedFactor; - playerCollider.x += baseChange*decodedInput.dx; - playerCollider.y += baseChange*decodedInput.dy; - /* - if (0 < encodedInput) { - console.log("playerId=", playerId, "@renderFrameId=", i, ", delayedInputFrameId=", j, ", baseChange=", baseChange, ": x=", playerCollider.x, ", y=", playerCollider.y); - } - */ + const baseChange = player.speed * self.rollbackEstimatedDt * decodedInput.speedFactor; + playerCollider.x += baseChange * decodedInput.dx; + playerCollider.y += baseChange * decodedInput.dy; } collisionSys.update(); const result = collisionSys.createResult(); // Can I reuse a "self.latestCollisionSysResult" object throughout the whole battle? - + for (let i in self.playerRichInfoArr) { - const joinIndex = parseInt(i) + 1; + const joinIndex = parseInt(i) + 1; const collisionPlayerIndex = self.collisionPlayerIndexPrefix + joinIndex; const playerCollider = collisionSysMap.get(collisionPlayerIndex); const potentials = playerCollider.potentials(); @@ -1044,10 +1039,10 @@ cc.Class({ } } } - + return self._createRoomDownsyncFrameLocally(renderFrameIdEd, collisionSys, collisionSysMap); - }, - + }, + _initPlayerRichInfoDict(players, playerMetas) { const self = this; for (let k in players) { @@ -1056,7 +1051,7 @@ cc.Class({ const immediatePlayerInfo = players[playerId]; const immediatePlayerMeta = playerMetas[playerId]; const nodeAndScriptIns = self.spawnPlayerNode(immediatePlayerInfo.joinIndex, immediatePlayerInfo.x, immediatePlayerInfo.y); - self.playerRichInfoDict.set(playerId, immediatePlayerInfo); + self.playerRichInfoDict.set(playerId, immediatePlayerInfo); Object.assign(self.playerRichInfoDict.get(playerId), { node: nodeAndScriptIns[0], @@ -1064,14 +1059,14 @@ cc.Class({ }); if (self.selfPlayerInfo.id == playerId) { - self.selfPlayerInfo = Object.assign(self.selfPlayerInfo, immediatePlayerInfo); + self.selfPlayerInfo = Object.assign(self.selfPlayerInfo, immediatePlayerInfo); nodeAndScriptIns[1].showArrowTipNode(); } } self.playerRichInfoArr = new Array(self.playerRichInfoDict.size); self.playerRichInfoDict.forEach((playerRichInfo, playerId) => { - self.playerRichInfoArr[playerRichInfo.joinIndex-1] = playerRichInfo; - }); + self.playerRichInfoArr[playerRichInfo.joinIndex - 1] = playerRichInfo; + }); }, _stringifyRecentInputCache(usefullOutput) { diff --git a/frontend/assets/scripts/RingBuffer.js b/frontend/assets/scripts/RingBuffer.js index 6262c6d..57310fe 100644 --- a/frontend/assets/scripts/RingBuffer.js +++ b/frontend/assets/scripts/RingBuffer.js @@ -1,3 +1,7 @@ +window.RING_BUFF_CONSECUTIVE_SET = 0; +window.RING_BUFF_NON_CONSECUTIVE_SET = 1; +window.RING_BUFF_FAILED_TO_SET = 2; + var RingBuffer = function(capacity) { this.ed = 0; // write index, open index this.st = 0; // read index, closed index @@ -32,15 +36,15 @@ RingBuffer.prototype.pop = function() { return item; }; -RingBuffer.prototype.getByOffset = function(offsetFromSt) { - if (0 == this.cnt) { +RingBuffer.prototype.getArrIdxByOffset = function(offsetFromSt) { + if (0 > offsetFromSt || 0 == this.cnt) { return null; } let arrIdx = this.st + offsetFromSt; if (this.st < this.ed) { // case#1: 0...st...ed...n-1 if (this.st <= arrIdx && arrIdx < this.ed) { - return this.eles[arrIdx]; + return arrIdx; } } else { // if this.st >= this.sd @@ -49,7 +53,7 @@ RingBuffer.prototype.getByOffset = function(offsetFromSt) { arrIdx -= this.n } if (arrIdx >= this.st || arrIdx < this.ed) { - return this.eles[arrIdx]; + return arrIdx; } } @@ -57,7 +61,40 @@ RingBuffer.prototype.getByOffset = function(offsetFromSt) { }; RingBuffer.prototype.getByFrameId = function(frameId) { - return this.getByOffset(frameId - this.stFrameId); + const arrIdx = this.getArrIdxByOffset(frameId - this.stFrameId); + return (null == arrIdx ? null : this.eles[arrIdx]); +}; + +// [WARNING] During a battle, frontend could receive non-consecutive frames (either renderFrame or inputFrame) due to resync, the buffer should handle these frames properly. +RingBuffer.prototype.setByFrameId = function(item, frameId) { + if (frameId < this.stFrameId) { + console.error("Invalid putByFrameId#1: stFrameId=", stFrameId, ", edFrameId=", edFrameId, ", incoming item=", item); + return window.RING_BUFF_FAILED_TO_SET; + } + const arrIdx = this.getArrIdxByOffset(frameId - this.stFrameId); + if (null != arrIdx) { + this.eles[arrIdx] = item; + return window.RING_BUFF_CONSECUTIVE_SET; + } + + // When "null == arrIdx", should it still be deemed consecutive if "frameId == edFrameId" prior to the reset? + let ret = window.RING_BUFF_CONSECUTIVE_SET; + if (this.edFrameId < frameId) { + this.st = this.ed = 0; + this.stFrameId = this.edFrameId = frameId; + this.cnt = 0; + ret = window.RING_BUFF_NON_CONSECUTIVE_SET; + } + + this.eles[this.ed] = item + this.edFrameId++; + this.cnt++; + this.ed++; + if (this.ed >= this.n) { + this.ed -= this.n; // Deliberately not using "%" operator for performance concern + } + + return ret; }; module.exports = RingBuffer; diff --git a/frontend/assets/scripts/WsSessionMgr.js b/frontend/assets/scripts/WsSessionMgr.js index 38bd561..bf4efb8 100644 --- a/frontend/assets/scripts/WsSessionMgr.js +++ b/frontend/assets/scripts/WsSessionMgr.js @@ -1,3 +1,5 @@ +const RingBuffer = require('./RingBuffer'); + window.UPSYNC_MSG_ACT_HB_PING = 1; window.UPSYNC_MSG_ACT_PLAYER_CMD = 2; window.UPSYNC_MSG_ACT_PLAYER_COLLIDER_ACK = 3; @@ -8,7 +10,7 @@ window.DOWNSYNC_MSG_ACT_BATTLE_READY_TO_START = -1; window.DOWNSYNC_MSG_ACT_BATTLE_START = 0; window.DOWNSYNC_MSG_ACT_HB_REQ = 1; window.DOWNSYNC_MSG_ACT_INPUT_BATCH = 2; -window.DOWNSYNC_MSG_ACT_ROOM_FRAME = 3; +window.DOWNSYNC_MSG_ACT_BATTLE_STOPPED = 3; window.DOWNSYNC_MSG_ACT_FORCED_RESYNC = 4; @@ -160,30 +162,28 @@ window.initPersistentSessionClient = function(onopenCb, expectedRoomId) { window.handleHbRequirements(resp); // 获取boundRoomId并存储到localStorage break; case window.DOWNSYNC_MSG_ACT_PLAYER_ADDED_AND_ACKED: - window.handlePlayerAdded(resp.rdf); + mapIns.onPlayerAdded(resp.rdf); break; case window.DOWNSYNC_MSG_ACT_PLAYER_READDED_AND_ACKED: // Deliberately left blank for now break; case window.DOWNSYNC_MSG_ACT_BATTLE_READY_TO_START: + mapIns.onBattleReadyToStart(resp.rdf.playerMetas, false); + break; case window.DOWNSYNC_MSG_ACT_BATTLE_START: - case window.DOWNSYNC_MSG_ACT_ROOM_FRAME: - if (window.handleRoomDownsyncFrame) { - window.handleRoomDownsyncFrame(resp.rdf); - } + mapIns.onBattleStartedOrResynced(resp.rdf); + break; + case window.DOWNSYNC_MSG_ACT_BATTLE_STOPPED: + mapIns.onBattleStopped(); break; case window.DOWNSYNC_MSG_ACT_INPUT_BATCH: - if (window.handleInputFrameDownsyncBatch) { - window.handleInputFrameDownsyncBatch(resp.inputFrameDownsyncBatch); - } + mapIns.onInputFrameDownsyncBatch(resp.inputFrameDownsyncBatch); break; case window.DOWNSYNC_MSG_ACT_FORCED_RESYNC: - if (window.handleInputFrameDownsyncBatch && window.handleRoomDownsyncFrame) { - console.warn("Got forced resync:", JSON.stringify(resp), " @localRenderFrameId=", mapIns.renderFrameId, ", @localRecentInputCache=", mapIns._stringifyRecentInputCache(false)); - // The following order of execution is important, because "handleInputFrameDownsyncBatch" is only available when state is IN_BATTLE - window.handleRoomDownsyncFrame(resp.rdf); - window.handleInputFrameDownsyncBatch(resp.inputFrameDownsyncBatch); - } + console.warn("Got forced resync:", JSON.stringify(resp), " @localRenderFrameId=", mapIns.renderFrameId, ", @localRecentInputCache=", mapIns._stringifyRecentInputCache(false)); + // The following order of execution is important, because "onInputFrameDownsyncBatch" is only available when state is IN_BATTLE + const dumpRenderCacheRet = mapIns.onBattleStartedOrResynced(resp.rdf); + mapIns.onInputFrameDownsyncBatch(resp.inputFrameDownsyncBatch, dumpRenderCacheRet); break; default: break;