Compare commits

...

7 Commits

Author SHA1 Message Date
genxium
8d2665ebd7 Updated comments. 2022-12-19 14:56:56 +08:00
genxium
1694287083 Fixed script file modes. 2022-12-19 14:19:43 +08:00
genxium
e63ce2c484 Fixed backend session watchdog. 2022-12-19 14:16:53 +08:00
genxium
a28c9a31f9 Updated dungeon map. 2022-12-18 22:53:55 +08:00
genxium
3b186c7f75 Minor fix. 2022-12-18 20:28:53 +08:00
genxium
34c4a24b64 Further enhanced backend dynamics force confirmation trigger. 2022-12-18 19:41:13 +08:00
genxium
4e7c3060fe Fixed frontend packaging. 2022-12-18 16:59:38 +08:00
16 changed files with 700 additions and 490 deletions

View File

@@ -1,6 +1,7 @@
PROJECTNAME=server.exe
ROOT_DIR=.
GOPROXY=https://mirrors.aliyun.com/goproxy
#GOPROXY=https://mirrors.aliyun.com/goproxy
GOPROXY=https://goproxy.io
all: help
gen-constants:

0
battle_srv/check_daemon.sh Normal file → Executable file
View File

View File

@@ -151,6 +151,7 @@ type Room struct {
*/
PlayerDownsyncSessionDict map[int32]*websocket.Conn
PlayerDownsyncChanDict map[int32](chan InputsBufferSnapshot)
PlayerActiveWatchdogDict map[int32](*Watchdog)
PlayerSignalToCloseDict map[int32]SignalToCloseConnCbType
Score float32
State int32
@@ -207,6 +208,12 @@ func (pR *Room) AddPlayerIfPossible(pPlayerFromDbInit *Player, session *websocke
pR.Players[playerId] = pPlayerFromDbInit
pR.PlayerDownsyncSessionDict[playerId] = session
pR.PlayerSignalToCloseDict[playerId] = signalToCloseConnOfThisPlayer
newWatchdog := NewWatchdog(ConstVals.Ws.WillKickIfInactiveFor, func() {
Logger.Warn("Conn inactive watchdog triggered#1:", zap.Any("playerId", playerId), zap.Any("roomId", pR.Id), zap.Any("roomState", pR.State), zap.Any("roomEffectivePlayerCount", pR.EffectivePlayerCount))
signalToCloseConnOfThisPlayer(Constants.RetCode.UnknownError, "")
})
newWatchdog.Stop()
pR.PlayerActiveWatchdogDict[playerId] = newWatchdog
return true
}
@@ -238,6 +245,10 @@ func (pR *Room) ReAddPlayerIfPossible(pTmpPlayerInstance *Player, session *webso
pR.PlayerDownsyncSessionDict[playerId] = session
pR.PlayerSignalToCloseDict[playerId] = signalToCloseConnOfThisPlayer
pR.PlayerActiveWatchdogDict[playerId] = NewWatchdog(ConstVals.Ws.WillKickIfInactiveFor, func() {
Logger.Warn("Conn inactive watchdog triggered#2:", zap.Any("playerId", playerId), zap.Any("roomId", pR.Id), zap.Any("roomState", pR.State), zap.Any("roomEffectivePlayerCount", pR.EffectivePlayerCount))
signalToCloseConnOfThisPlayer(Constants.RetCode.UnknownError, "")
}) // For ReAdded player the new watchdog starts immediately
Logger.Warn("ReAddPlayerIfPossible finished.", zap.Any("roomId", pR.Id), zap.Any("playerId", playerId), zap.Any("joinIndex", pEffectiveInRoomPlayerInstance.JoinIndex), zap.Any("playerBattleState", pEffectiveInRoomPlayerInstance.BattleState), zap.Any("roomState", pR.State), zap.Any("roomEffectivePlayerCount", pR.EffectivePlayerCount), zap.Any("AckingFrameId", pEffectiveInRoomPlayerInstance.AckingFrameId), zap.Any("AckingInputFrameId", pEffectiveInRoomPlayerInstance.AckingInputFrameId), zap.Any("LastSentInputFrameId", pEffectiveInRoomPlayerInstance.LastSentInputFrameId))
return true
@@ -421,6 +432,9 @@ func (pR *Room) StartBattle() {
pR.LastRenderFrameIdTriggeredAt = utils.UnixtimeNano()
battleStartedAt := utils.UnixtimeNano()
Logger.Info("The `battleMainLoop` is started for:", zap.Any("roomId", pR.Id))
for _, watchdog := range pR.PlayerActiveWatchdogDict {
watchdog.Kick()
}
for {
stCalculation := utils.UnixtimeNano()
elapsedNanosSinceLastFrameIdTriggered := stCalculation - pR.LastRenderFrameIdTriggeredAt
@@ -559,6 +573,10 @@ func (pR *Room) OnBattleCmdReceived(pReq *WsReq) {
return
}
if watchdog, existent := pR.PlayerActiveWatchdogDict[playerId]; existent {
watchdog.Kick()
}
atomic.StoreInt32(&(player.AckingFrameId), ackingFrameId)
atomic.StoreInt32(&(player.AckingInputFrameId), ackingInputFrameId)
@@ -723,6 +741,10 @@ func (pR *Room) OnDismissed() {
pR.PlayersArr = make([]*Player, pR.Capacity)
pR.CollisionSysMap = make(map[int32]*resolv.Object)
pR.PlayerDownsyncSessionDict = make(map[int32]*websocket.Conn)
for _, oldWatchdog := range pR.PlayerActiveWatchdogDict {
oldWatchdog.Stop()
}
pR.PlayerActiveWatchdogDict = make(map[int32]*Watchdog)
for _, oldChan := range pR.PlayerDownsyncChanDict {
close(oldChan)
}
@@ -742,17 +764,17 @@ func (pR *Room) OnDismissed() {
pR.RenderFrameId = 0
pR.CurDynamicsRenderFrameId = 0
pR.InputDelayFrames = 8
pR.NstDelayFrames = 8
pR.NstDelayFrames = 16
pR.InputScaleFrames = uint32(2)
pR.ServerFps = 60
pR.RollbackEstimatedDtMillis = 16.667 // Use fixed-and-low-precision to mitigate the inconsistent floating-point-number issue between Golang and JavaScript
pR.RollbackEstimatedDtNanos = 16666666 // A little smaller than the actual per frame time, just for logging FAST FRAME
dilutedServerFps := float64(58.0) // Don't set this value too small, otherwise we might miss force confirmation needs for slow tickers!
pR.dilutedRollbackEstimatedDtNanos = int64(float64(pR.RollbackEstimatedDtNanos) * float64(pR.ServerFps) / dilutedServerFps)
pR.BattleDurationFrames = 90 * pR.ServerFps
pR.BattleDurationFrames = 120 * pR.ServerFps
pR.BattleDurationNanos = int64(pR.BattleDurationFrames) * (pR.RollbackEstimatedDtNanos + 1)
pR.InputFrameUpsyncDelayTolerance = 2
pR.MaxChasingRenderFramesPerUpdate = 8
pR.InputFrameUpsyncDelayTolerance = (pR.NstDelayFrames >> pR.InputScaleFrames) - 1 // this value should be strictly smaller than (NstDelayFrames >> InputScaleFrames), otherwise "type#1 forceConfirmation" might become a lag avalanche
pR.MaxChasingRenderFramesPerUpdate = 12 // Don't set this value too high to avoid exhausting frontend CPU within a single frame
pR.BackendDynamicsEnabled = true // [WARNING] When "false", recovery upon reconnection wouldn't work!
punchSkillId := int32(1)
@@ -884,6 +906,8 @@ func (pR *Room) clearPlayerNetworkSession(playerId int32) {
if _, y := pR.PlayerDownsyncSessionDict[playerId]; y {
Logger.Debug("clearPlayerNetworkSession:", zap.Any("roomId", pR.Id), zap.Any("playerId", playerId))
// [WARNING] No need to close "pR.PlayerDownsyncChanDict[playerId]" immediately!
pR.PlayerActiveWatchdogDict[playerId].Stop()
delete(pR.PlayerActiveWatchdogDict, playerId)
delete(pR.PlayerDownsyncSessionDict, playerId)
delete(pR.PlayerSignalToCloseDict, playerId)
}
@@ -946,7 +970,6 @@ func (pR *Room) OnPlayerBattleColliderAcked(playerId int32) bool {
if false == existing {
return false
}
shouldTryToStartBattle := true
Logger.Debug(fmt.Sprintf("OnPlayerBattleColliderAcked-before: roomId=%v, roomState=%v, targetPlayerId=%v, targetPlayerBattleState=%v, capacity=%v, EffectivePlayerCount=%v", pR.Id, pR.State, targetPlayer.Id, targetPlayer.BattleState, pR.Capacity, pR.EffectivePlayerCount))
targetPlayerBattleState := atomic.LoadInt32(&(targetPlayer.BattleState))
@@ -1060,7 +1083,11 @@ func (pR *Room) prefabInputFrameDownsync(inputFrameId int32) *InputFrameDownsync
ConfirmedList: uint64(0),
}
tmp2 := pR.InputsBuffer.GetByFrameId(j - 1) // There's no need for the backend to find the "lastAllConfirmed inputs" for prefabbing, either "BackendDynamicsEnabled" is true or false
j2 := j - 1
if 0 <= pR.LastAllConfirmedInputFrameId && j2 >= pR.LastAllConfirmedInputFrameId {
j2 = pR.LastAllConfirmedInputFrameId
}
tmp2 := pR.InputsBuffer.GetByFrameId(j2)
if nil != tmp2 {
prevInputFrameDownsync := tmp2.(*InputFrameDownsync)
for i, _ := range currInputFrameDownsync.InputList {
@@ -1087,7 +1114,7 @@ func (pR *Room) markConfirmationIfApplicable(inputFrameUpsyncBatch []*InputFrame
continue
}
if clientInputFrameId < pR.LastAllConfirmedInputFrameId {
Logger.Info(fmt.Sprintf("Omitting obsolete inputFrameUpsync#2: roomId=%v, playerId=%v, clientInputFrameId=%v, InputsBuffer=%v", pR.Id, playerId, clientInputFrameId, pR.InputsBufferString(false)))
Logger.Debug(fmt.Sprintf("Omitting obsolete inputFrameUpsync#2: roomId=%v, playerId=%v, clientInputFrameId=%v, InputsBuffer=%v", pR.Id, playerId, clientInputFrameId, pR.InputsBufferString(false)))
continue
}
if clientInputFrameId > pR.InputsBuffer.EdFrameId {
@@ -1129,7 +1156,7 @@ func (pR *Room) markConfirmationIfApplicable(inputFrameUpsyncBatch []*InputFrame
thatPlayerJoinMask := uint64(1 << uint32(player.JoinIndex-1))
if 0 == (inputFrameDownsync.ConfirmedList & thatPlayerJoinMask) {
if thatPlayerBattleState == PlayerBattleStateIns.ACTIVE {
shouldBreakConfirmation = true
shouldBreakConfirmation = true // Could be an `ACTIVE SLOW TICKER` here, but no action needed for now
break
} else {
Logger.Debug(fmt.Sprintf("markConfirmationIfApplicable for roomId=%v, skipping UNCONFIRMED BUT INACTIVE player(id:%v, joinIndex:%v) while checking inputFrameId=[%v, %v): InputsBuffer=%v", pR.Id, player.Id, player.JoinIndex, inputFrameId1, pR.InputsBuffer.EdFrameId, pR.InputsBufferString(false)))
@@ -1147,102 +1174,80 @@ func (pR *Room) markConfirmationIfApplicable(inputFrameUpsyncBatch []*InputFrame
}
if 0 < newAllConfirmedCount {
refRenderFrameIdIfNeeded := pR.CurDynamicsRenderFrameId - 1
/*
[WARNING]
If "pR.InputsBufferLock" was previously held by "doBattleMainLoopPerTickBackendDynamicsWithProperLocking", then this value would be just (pR.LastAllConfirmedInputFrameId - newAllConfirmedCount).
If "pR.InputsBufferLock" was previously held by "doBattleMainLoopPerTickBackendDynamicsWithProperLocking", then "snapshotStFrameId" would be just (pR.LastAllConfirmedInputFrameId - newAllConfirmedCount).
However if "pR.InputsBufferLock" was previously held by another "OnBattleCmdReceived", this value might be smaller than (pR.LastAllConfirmedInputFrameId - newAllConfirmedCount)!
However if "pR.InputsBufferLock" was previously held by another "OnBattleCmdReceived", "snapshotStFrameId" might be smaller than (pR.LastAllConfirmedInputFrameId - newAllConfirmedCount)!
*/
snapshotStFrameId := pR.ConvertToInputFrameId(refRenderFrameIdIfNeeded, pR.InputDelayFrames)
// Duplicate downsynced inputFrameIds will be filtered out by frontend.
toSendInputFrameDownsyncs := pR.cloneInputsBuffer(snapshotStFrameId, pR.LastAllConfirmedInputFrameId+1)
Logger.Debug(fmt.Sprintf("markConfirmationIfApplicable for roomId=%v returning newAllConfirmedCount=%d: InputsBuffer=%v", pR.Id, newAllConfirmedCount, pR.InputsBufferString(false)))
return &InputsBufferSnapshot{
RefRenderFrameId: refRenderFrameIdIfNeeded,
UnconfirmedMask: uint64(0),
ToSendInputFrameDownsyncs: toSendInputFrameDownsyncs,
snapshotStFrameId := (pR.LastAllConfirmedInputFrameId - newAllConfirmedCount)
refRenderFrameIdIfNeeded := pR.CurDynamicsRenderFrameId - 1
refSnapshotStFrameId := pR.ConvertToInputFrameId(refRenderFrameIdIfNeeded, pR.InputDelayFrames)
if refSnapshotStFrameId < snapshotStFrameId {
snapshotStFrameId = refSnapshotStFrameId
}
Logger.Debug(fmt.Sprintf("markConfirmationIfApplicable for roomId=%v returning newAllConfirmedCount=%d: InputsBuffer=%v", pR.Id, newAllConfirmedCount, pR.InputsBufferString(false)))
return pR.produceInputsBufferSnapshotWithCurDynamicsRenderFrameAsRef(uint64(0), snapshotStFrameId, pR.LastAllConfirmedInputFrameId+1)
} else {
return nil
}
}
func (pR *Room) forceConfirmationIfApplicable(prevRenderFrameId int32) *InputsBufferSnapshot {
func (pR *Room) forceConfirmationIfApplicable(prevRenderFrameId int32) uint64 {
// [WARNING] This function MUST BE called while "pR.InputsBufferLock" is locked!
// Force confirmation of non-all-confirmed inputFrame EXACTLY ONE AT A TIME, returns the non-confirmed mask of players, e.g. in a 4-player-battle returning 1001 means that players with JoinIndex=1 and JoinIndex=4 are non-confirmed for inputFrameId2
inputFrameId2 := int32(-1)
if pR.LatestPlayerUpsyncedInputFrameId > (pR.LastAllConfirmedInputFrameId + (pR.NstDelayFrames >> pR.InputScaleFrames)) {
// Type#1 check whether there's a significantly slow ticker among players
Logger.Warn(fmt.Sprintf("[type#1 forceConfirmation]For roomId=%d@renderFrameId=%d, curDynamicsRenderFrameId=%d, LatestPlayerUpsyncedInputFrameId:%d, LastAllConfirmedInputFrameId:%d, (pR.NstDelayFrames >> pR.InputScaleFrames):%d; there's a slow ticker suspect, forcing all-confirmation", pR.Id, pR.RenderFrameId, pR.CurDynamicsRenderFrameId, pR.LatestPlayerUpsyncedInputFrameId, pR.LastAllConfirmedInputFrameId, (pR.NstDelayFrames >> pR.InputScaleFrames)))
inputFrameId2 = pR.LastAllConfirmedInputFrameId + 1
} else {
// Type#2 check whether there's a significantly slow ticker w.r.t. BackendDynamics, this applies when all players are disconnected temporarily
renderFrameId1 := (pR.RenderFrameId - pR.NstDelayFrames) // the "renderFrameId" which should've been rendered on frontend
if 0 > renderFrameId1 {
// Battle is still in an early stage, no action needed even if there were slow tickers
return nil
}
if ok, renderFrameId2 := pR.shouldPrefabInputFrameDownsync(prevRenderFrameId-pR.NstDelayFrames, renderFrameId1); ok {
/*
The backend "shouldPrefabInputFrameDownsync" shares the same rule as frontend "shouldGenerateInputFrameUpsync".
It's also important that "forceConfirmationIfApplicable" is NOT EXECUTED for every renderFrame, such that when a player is forced to resync, it has some time, i.e. (1 << InputScaleFrames) renderFrames, to upsync again.
*/
inputFrameId2 = pR.ConvertToInputFrameId(renderFrameId2, 0) // The inputFrame to force confirmation (if necessary)
if inputFrameId2 > pR.LastAllConfirmedInputFrameId {
Logger.Debug(fmt.Sprintf("[type#2 forceConfirmation]For roomId=%d@renderFrameId=%d, curDynamicsRenderFrameId=%d, renderFrameId1:%d, renderFrameId2:%d, NstDelayFrames:%d, inputFrameId2:%d, LastAllConfirmedInputFrameId:%d; there's a slow ticker suspect, forcing all-confirmation", pR.Id, pR.RenderFrameId, pR.CurDynamicsRenderFrameId, renderFrameId1, renderFrameId2, pR.NstDelayFrames, inputFrameId2, pR.LastAllConfirmedInputFrameId))
}
}
}
if pR.LastAllConfirmedInputFrameId >= inputFrameId2 {
// No need to force confirmation for either type
Logger.Debug(fmt.Sprintf("inputFrameId2=%v is already all-confirmed for roomId=%v, no need to force confirmation", inputFrameId2, pR.Id))
return nil
}
tmp := pR.InputsBuffer.GetByFrameId(inputFrameId2)
if nil == tmp {
panic(fmt.Sprintf("For roomId=%d, inputFrameId2=%v doesn't exist, this is abnormal because the server should prefab inputFrameDownsync in a most advanced pace, check the prefab logic! InputsBuffer=%v", pR.Id, inputFrameId2, pR.InputsBufferString(false)))
}
totPlayerCnt := uint32(pR.Capacity)
allConfirmedMask := uint64((1 << totPlayerCnt) - 1)
// Force confirmation of "inputFrame2"
inputFrame2 := tmp.(*InputFrameDownsync)
oldConfirmedList := inputFrame2.ConfirmedList
unconfirmedMask := (oldConfirmedList ^ allConfirmedMask)
inputFrame2.ConfirmedList = allConfirmedMask
pR.onInputFrameDownsyncAllConfirmed(inputFrame2, -1)
if 0 < unconfirmedMask {
// This condition should be rarely met!
/*
Upon resynced on frontend, "refRenderFrameId" is now set to as advanced as possible, and it's the frontend's responsibility now to pave way for the "gap inputFrames"
If "NstDelayFrames" becomes larger, "pR.RenderFrameId - refRenderFrameId" possibly becomes larger because the force confirmation is delayed more.
Upon resync, it's still possible that "refRenderFrameId < frontend.chaserRenderFrameId" -- and this is allowed.
*/
refRenderFrameIdIfNeeded := pR.ConvertToLastUsedRenderFrameId(pR.LastAllConfirmedInputFrameId, pR.InputDelayFrames)
if 0 > refRenderFrameIdIfNeeded {
// Without a "refRenderFrame", there's no point to force confirmation, i.e. nothing to downsync to the "ACTIVE but slowly ticking frontend(s)"
return nil
unconfirmedMask := uint64(0)
if pR.LatestPlayerUpsyncedInputFrameId > (pR.LastAllConfirmedInputFrameId + (pR.NstDelayFrames >> pR.InputScaleFrames)) {
// Type#1 check whether there's a significantly slow ticker among players
oldLastAllConfirmedInputFrameId := pR.LastAllConfirmedInputFrameId
for j := pR.LastAllConfirmedInputFrameId + 1; j <= pR.LatestPlayerUpsyncedInputFrameId; j++ {
tmp := pR.InputsBuffer.GetByFrameId(j)
if nil == tmp {
panic(fmt.Sprintf("inputFrameId=%v doesn't exist for roomId=%v! InputsBuffer=%v", j, pR.Id, pR.InputsBufferString(false)))
}
inputFrameDownsync := tmp.(*InputFrameDownsync)
unconfirmedMask |= (allConfirmedMask ^ inputFrameDownsync.ConfirmedList)
inputFrameDownsync.ConfirmedList = allConfirmedMask
pR.onInputFrameDownsyncAllConfirmed(inputFrameDownsync, -1)
}
snapshotStFrameId := pR.ConvertToInputFrameId(refRenderFrameIdIfNeeded, pR.InputDelayFrames)
toSendInputFrameDownsyncs := pR.cloneInputsBuffer(snapshotStFrameId, pR.LastAllConfirmedInputFrameId+1)
return &InputsBufferSnapshot{
RefRenderFrameId: refRenderFrameIdIfNeeded,
UnconfirmedMask: unconfirmedMask,
ToSendInputFrameDownsyncs: toSendInputFrameDownsyncs,
if 0 < unconfirmedMask {
Logger.Warn(fmt.Sprintf("[type#1 forceConfirmation] For roomId=%d@renderFrameId=%d, curDynamicsRenderFrameId=%d, LatestPlayerUpsyncedInputFrameId:%d, LastAllConfirmedInputFrameId:%d, (pR.NstDelayFrames >> pR.InputScaleFrames):%d, InputFrameUpsyncDelayTolerance:%d, unconfirmedMask=%d; there's a slow ticker suspect, forcing all-confirmation", pR.Id, pR.RenderFrameId, pR.CurDynamicsRenderFrameId, pR.LatestPlayerUpsyncedInputFrameId, oldLastAllConfirmedInputFrameId, (pR.NstDelayFrames >> pR.InputScaleFrames), pR.InputFrameUpsyncDelayTolerance, unconfirmedMask))
}
} else {
// Type#2 helps resolve the edge case when all players are disconnected temporarily
shouldForceResync := false
for _, player := range pR.PlayersArr {
playerBattleState := atomic.LoadInt32(&(player.BattleState))
if PlayerBattleStateIns.READDED_BATTLE_COLLIDER_ACKED == playerBattleState {
shouldForceResync = true
break
}
}
if shouldForceResync {
Logger.Warn(fmt.Sprintf("[type#2 forceConfirmation] For roomId=%d@renderFrameId=%d, curDynamicsRenderFrameId=%d, LatestPlayerUpsyncedInputFrameId:%d, LastAllConfirmedInputFrameId:%d; there's at least one reconnected player, forcing all-confirmation", pR.Id, pR.RenderFrameId, pR.CurDynamicsRenderFrameId, pR.LatestPlayerUpsyncedInputFrameId, pR.LastAllConfirmedInputFrameId))
unconfirmedMask = allConfirmedMask
}
}
return unconfirmedMask
}
func (pR *Room) produceInputsBufferSnapshotWithCurDynamicsRenderFrameAsRef(unconfirmedMask uint64, snapshotStFrameId, snapshotEdFrameId int32) *InputsBufferSnapshot {
// [WARNING] This function MUST BE called while "pR.InputsBufferLock" is locked!
refRenderFrameIdIfNeeded := pR.CurDynamicsRenderFrameId - 1
if 0 > refRenderFrameIdIfNeeded {
return nil
}
// Duplicate downsynced inputFrameIds will be filtered out by frontend.
toSendInputFrameDownsyncs := pR.cloneInputsBuffer(snapshotStFrameId, snapshotEdFrameId)
return &InputsBufferSnapshot{
RefRenderFrameId: refRenderFrameIdIfNeeded,
UnconfirmedMask: unconfirmedMask,
ToSendInputFrameDownsyncs: toSendInputFrameDownsyncs,
}
}
func (pR *Room) applyInputFrameDownsyncDynamics(fromRenderFrameId int32, toRenderFrameId int32, spaceOffsetX, spaceOffsetY float64) {
@@ -1557,7 +1562,7 @@ func (pR *Room) applyInputFrameDownsyncDynamicsOnSingleRenderFrame(delayedInputF
for _, hardPushbackNorm := range hardPushbackNorms[joinIndex-1] {
projectedMagnitude := pushbackX*hardPushbackNorm.X + pushbackY*hardPushbackNorm.Y
if 0 > projectedMagnitude {
Logger.Info(fmt.Sprintf("defenderPlayerId=%d, joinIndex=%d reducing bullet pushback={%.3f, %.3f} by {%.3f, %.3f} where hardPushbackNorm={%.3f, %.3f}, projectedMagnitude=%.3f at renderFrame.id=%d", t.Id, joinIndex, pushbackX, pushbackY, projectedMagnitude*hardPushbackNorm.X, projectedMagnitude*hardPushbackNorm.Y, hardPushbackNorm.X, hardPushbackNorm.Y, projectedMagnitude, currRenderFrame.Id))
//Logger.Debug(fmt.Sprintf("defenderPlayerId=%d, joinIndex=%d reducing bullet pushback={%.3f, %.3f} by {%.3f, %.3f} where hardPushbackNorm={%.3f, %.3f}, projectedMagnitude=%.3f at renderFrame.id=%d", t.Id, joinIndex, pushbackX, pushbackY, projectedMagnitude*hardPushbackNorm.X, projectedMagnitude*hardPushbackNorm.Y, hardPushbackNorm.X, hardPushbackNorm.Y, projectedMagnitude, currRenderFrame.Id))
pushbackX -= projectedMagnitude * hardPushbackNorm.X
pushbackY -= projectedMagnitude * hardPushbackNorm.Y
}
@@ -1652,7 +1657,7 @@ func (pR *Room) refreshColliders(spaceW, spaceH int32) {
topPadding, bottomPadding, leftPadding, rightPadding := pR.SnapIntoPlatformOverlap, pR.SnapIntoPlatformOverlap, pR.SnapIntoPlatformOverlap, pR.SnapIntoPlatformOverlap
minStep := (int(float64(pR.PlayerDefaultSpeed)*pR.VirtualGridToWorldRatio) << 2) // the approx minimum distance a player can move per frame in world coordinate
minStep := (int(float64(pR.PlayerDefaultSpeed)*pR.VirtualGridToWorldRatio) << 3) // the approx minimum distance a player can move per frame in world coordinate
pR.Space = resolv.NewSpace(int(spaceW), int(spaceH), minStep, minStep) // allocate a new collision space everytime after a battle is settled
for _, player := range pR.Players {
wx, wy := VirtualGridToWorldPos(player.VirtualGridX, player.VirtualGridY, pR.VirtualGridToWorldRatio)
@@ -1691,13 +1696,12 @@ func (pR *Room) doBattleMainLoopPerTickBackendDynamicsWithProperLocking(prevRend
if ok, thatRenderFrameId := pR.shouldPrefabInputFrameDownsync(prevRenderFrameId, pR.RenderFrameId); ok {
noDelayInputFrameId := pR.ConvertToInputFrameId(thatRenderFrameId, 0)
if existingInputFrame := pR.InputsBuffer.GetByFrameId(noDelayInputFrameId); nil == existingInputFrame {
pR.prefabInputFrameDownsync(noDelayInputFrameId)
}
pR.prefabInputFrameDownsync(noDelayInputFrameId)
}
// Force setting all-confirmed of buffered inputFrames periodically, kindly note that if "pR.BackendDynamicsEnabled", what we want to achieve is "recovery upon reconnection", which certainly requires "forceConfirmationIfApplicable" to move "pR.LastAllConfirmedInputFrameId" forward as much as possible
inputsBufferSnapshot := pR.forceConfirmationIfApplicable(prevRenderFrameId)
oldLastAllConfirmedInputFrameId := pR.LastAllConfirmedInputFrameId
unconfirmedMask := pR.forceConfirmationIfApplicable(prevRenderFrameId)
if 0 <= pR.LastAllConfirmedInputFrameId {
dynamicsStartedAt := utils.UnixtimeNano()
@@ -1708,8 +1712,25 @@ func (pR *Room) doBattleMainLoopPerTickBackendDynamicsWithProperLocking(prevRend
*pDynamicsDuration = utils.UnixtimeNano() - dynamicsStartedAt
}
if nil != inputsBufferSnapshot {
Logger.Warn(fmt.Sprintf("roomId=%v, room.RenderFrameId=%v, room.CurDynamicsRenderFrameId=%v, room.LastAllConfirmedInputFrameId=%v, unconfirmedMask=%v", pR.Id, pR.RenderFrameId, pR.CurDynamicsRenderFrameId, pR.LastAllConfirmedInputFrameId, inputsBufferSnapshot.UnconfirmedMask))
/*
[WARNING]
It's critical to create the snapshot AFTER "applyInputFrameDownsyncDynamics" for `ACTIVE SLOW TICKER` to avoid lag avalanche (see `<proj-root>/ConcerningEdgeCases.md` for introduction).
Consider that in a 4-player battle, player#1 is once disconnected but soon reconnected in 2 seconds, during its absence, "markConfirmationIfApplicable" would skip it and increment "LastAllConfirmedInputFrameId" and when backend is sending "DOWNSYNC_MSG_ACT_FORCED_RESYNC" it'd be always based on "LatestPlayerUpsyncedInputFrameId == LastAllConfirmedInputFrameId" thus NOT triggering "[type#1 forceConfirmation]".
However, if player#1 remains connected but ticks very slowly (i.e. an "ACTIVE SLOW TICKER"), "markConfirmationIfApplicable" couldn't increment "LastAllConfirmedInputFrameId", thus "[type#1 forceConfirmation]" will be triggered, but what's worse is that after "[type#1 forceConfirmation]" if the "refRenderFrameId" is not advanced enough, player#1 could never catch up even if it resumed from slow ticking!
*/
if 0 < unconfirmedMask {
// [WARNING] As "pR.CurDynamicsRenderFrameId" was just incremented above, "refSnapshotStFrameId" is most possibly larger than "oldLastAllConfirmedInputFrameId + 1", therefore this initial assignment is critical for `ACTIVE NORMAL TICKER`s to receive consecutive ids of inputFrameDownsync.
snapshotStFrameId := oldLastAllConfirmedInputFrameId + 1
refSnapshotStFrameId := pR.ConvertToInputFrameId(pR.CurDynamicsRenderFrameId-1, pR.InputDelayFrames)
if refSnapshotStFrameId < snapshotStFrameId {
snapshotStFrameId = refSnapshotStFrameId
}
inputsBufferSnapshot := pR.produceInputsBufferSnapshotWithCurDynamicsRenderFrameAsRef(unconfirmedMask, snapshotStFrameId, pR.LastAllConfirmedInputFrameId+1)
Logger.Debug(fmt.Sprintf("[forceConfirmation] roomId=%v, room.RenderFrameId=%v, room.CurDynamicsRenderFrameId=%v, room.LastAllConfirmedInputFrameId=%v, unconfirmedMask=%v", pR.Id, pR.RenderFrameId, pR.CurDynamicsRenderFrameId, pR.LastAllConfirmedInputFrameId, unconfirmedMask))
pR.downsyncToAllPlayers(inputsBufferSnapshot)
}
}
@@ -1741,14 +1762,23 @@ func (pR *Room) downsyncToAllPlayers(inputsBufferSnapshot *InputsBufferSnapshot)
That said, we ensured that if "false == BackendDynamicsEnabled" and noone ever disconnects & reconnects, the frontend collision handling results are always consistent.
*/
playerBattleState := atomic.LoadInt32(&(player.BattleState))
thatPlayerJoinMask := uint64(1 << uint32(player.JoinIndex-1))
if PlayerBattleStateIns.READDED_BATTLE_COLLIDER_ACKED == playerBattleState {
inputsBufferSnapshot.ShouldForceResync = true
break
} else if PlayerBattleStateIns.ACTIVE == playerBattleState && 0 < (inputsBufferSnapshot.UnconfirmedMask&thatPlayerJoinMask) {
/*
[WARNING] Whenever there's an `ACTIVE SLOW TICKER`, all players should also be resynced to avoid inconsistent display. See `<proj-root>/ConcerningEdgeCases.md` for more information.
*/
}
/*
[WARNING]
There's a tradeoff here, if the `ACTIVE SLOW TICKER` doesn't resume for a long period of time, the current approach is to kick it out by "connWatchdog" instead of forcing resync of all players in the same battle all the way along.
[FIXME]
In practice, I tested in internet environment by toggling player#1 "CPU throttling: 1x -> 4x -> 1x -> 6x -> 1x" and checked the logs of all players which showed that "all received inputFrameIds are consecutive for all players", yet not forcing resync of all players here still result in occasional inconsistent graphics for the `ACTIVE NORMAL TICKER`s.
More investigation into this issue is needed, it's possible that the inconsistent graphics is just a result of difference of backend/frontend collision calculations, yet before it's totally resolved we'd keep forcing resync here.
*/
thatPlayerJoinMask := uint64(1 << uint32(player.JoinIndex-1))
isActiveSlowTicker := (0 < (thatPlayerJoinMask & inputsBufferSnapshot.UnconfirmedMask)) && (PlayerBattleStateIns.ACTIVE == playerBattleState)
if isActiveSlowTicker {
inputsBufferSnapshot.ShouldForceResync = true
break
}
@@ -1790,9 +1820,9 @@ func (pR *Room) downsyncToSinglePlayer(playerId int32, player *Player, refRender
return
}
isSlowTicker := (0 < (unconfirmedMask & uint64(1<<uint32(playerJoinIndex))))
shouldResync1 := (PlayerBattleStateIns.READDED_BATTLE_COLLIDER_ACKED == playerBattleState) // i.e. implies that "MAGIC_LAST_SENT_INPUT_FRAME_ID_READDED == player.LastSentInputFrameId"
shouldResync2 := (0 < (unconfirmedMask & uint64(1<<uint32(playerJoinIndex)))) // This condition is critical, if we don't send resync upon this condition, the "reconnected or slowly-clocking player" might never get its input synced
// shouldResync2 := (0 < unconfirmedMask) // An easier version of the above, might keep sending "refRenderFrame"s to still connected players when any player is disconnected
shouldResync2 := isSlowTicker // This condition is critical, if we don't send resync upon this condition, the "reconnected or slowly-clocking player" might never get its input synced
shouldResync3 := shouldForceResync
shouldResyncOverall := (shouldResync1 || shouldResync2 || shouldResync3)

View File

@@ -0,0 +1,27 @@
package models
import (
"time"
)
type Watchdog struct {
interval time.Duration
timer *time.Timer
}
func NewWatchdog(interval time.Duration, callback func()) *Watchdog {
w := Watchdog{
interval: interval,
timer: time.AfterFunc(interval, callback),
}
return &w
}
func (w *Watchdog) Stop() {
w.timer.Stop()
}
func (w *Watchdog) Kick() {
w.timer.Stop()
w.timer.Reset(w.interval)
}

3
battle_srv/start_daemon.sh Normal file → Executable file
View File

@@ -7,6 +7,7 @@ fi
basedir=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )
PROJECTNAME=server.exe
OS_USER=$USER
ServerEnv=$1
LOG_PATH="/var/log/treasure-hunter.log"
@@ -17,5 +18,5 @@ PID_FILE="$basedir/treasure-hunter.pid"
sudo su - root -c "touch $LOG_PATH"
sudo su - root -c "chown $OS_USER:$OS_USER $LOG_PATH"
ServerEnv=$ServerEnv $basedir/server >$LOG_PATH 2>&1 &
ServerEnv=$ServerEnv $basedir/$PROJECTNAME >$LOG_PATH 2>&1 &
echo $! > $PID_FILE

0
battle_srv/stop_daemon.sh Normal file → Executable file
View File

View File

@@ -2,7 +2,8 @@ PROJECTNAME=viscol.exe
ROOT_DIR=.
all: help
## Available proxies for downloading go modules are listed in "https://github.com/golang/go/wiki/Modules#how-do-i-use-vendoring-with-modules-is-vendoring-going-away".
GOPROXY=https://mirrors.aliyun.com/goproxy
#GOPROXY=https://mirrors.aliyun.com/goproxy
GOPROXY=https://goproxy.io
build:
GOPROXY=$(GOPROXY) go build -o $(ROOT_DIR)/$(PROJECTNAME)

View File

@@ -1,18 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<map version="1.2" tiledversion="1.2.3" orientation="orthogonal" renderorder="right-down" width="128" height="128" tilewidth="16" tileheight="16" infinite="0" nextlayerid="3" nextobjectid="87">
<map version="1.2" tiledversion="1.2.3" orientation="orthogonal" renderorder="right-down" width="128" height="128" tilewidth="16" tileheight="16" infinite="0" nextlayerid="4" nextobjectid="104">
<tileset firstgid="1" source="tiles0.tsx"/>
<tileset firstgid="65" source="tiles1.tsx"/>
<tileset firstgid="129" source="tiles2.tsx"/>
<layer id="2" name="Ground" width="128" height="128">
<data encoding="base64" compression="zlib">
eJzt3DFq3EAUgOFlTRoXJiEkkNQpAr5EyuDKVdKl8k1yh9wg94wMK1DEaqXVjvRGel/xgXFh7PnfSBqx+O5wONwBAAAAAAAAAAAAAAAAAMAM9yfRvwfreelo+78kFN0hsv/xpO3/q/E04LhD+v/ff4j++9Pt/zTS/76CVvov1/8c/fft9W//cHJubfrXhOhW+q/bPwP99Y/uENn//QzRzfSP7b+nGdBf/+gOW+xfE/1z9rf/c/cvMQP6b5f9r7/+efuXmAH9y69t+07x942W3vv666//Nvp/OtE/T/9u7+7X+uuvf1z/tyNK9u/SP7b/WPdz/f80flyp7e/5r57+U9s/Hzz/b8XU/lPbH/XflLH+5xo/Nh4G2vf79z1fSf+4/ufaP3T0uy/R/2Pji/7V9L903e/2L03/evvPeUa8lv7r958yA/21LNl3TMn+xwpa1Ni/1BpfMqe9/vvpf82MLPGz9d/GDCxF/9wzoL/+mftHr3+07Oe/6PWPpn9u+uemf27656Z/bvrnpn9u+uemf27656Z/bvrnpn9u+uemf276z7eHzwnpX0//dyPeDNB/H/3t/7r77/Fzovrnpv+6hu7tkf2PFbSouf/Pk/71vv1+9B7Wf9n+l+75+m/XlP5DZ65XY+e1yOv6Nf2jO9Tav+389Qa3ns/1j93/tyrxjkb/5fqXfp821L/WGcjef63936rtPZH+6/ZvZyDq/73oP96/5PP8pfPDEP3r2f+3zsGc/mveJ/SfZu4cfJ7Z/2/ju/7V9J+r6f9tbv/WknOQvT/xHQAAAAAAAAAAAAAAAID1/QPUuKyX
eJzt3MFq1EAcwOFli5ceikUU9NyD4Et47qle9OapLyB60ZMgvoIH776nWUggxkySnZ1kksx3+KC0S2jn9082E5ZeHQ6HKwAAAAAAAAAAAAAAAAAAiHBdy/17sJzHlqb/Y4Fyd8jZ/1hr+n+s3Accd0j/f/uH6L8/7f73I/2vV9BK//n699F/305/+/Na39p0rwm5W+m/bP8S6K9/7g45+z+LkLuZ/nn772kG9Nc/d4ct9l8T/cvs7/wvu3+KGdB/u5z/+utfbv8UM6B/+rVtnin+uNDc577++uu/jf4va/qX07/du/21/vrrn6//0xEp+7fpn7f/WPe+/r8q78/U9Hf/t57+U9s/HNz/b8XU/lPbH/XflLH+fY3fVG4C7bv9ux7OpH++/n3tb1q63efo/6Jyp/9q+g9d99v9U9N/vf1j7hHPpf/y/afMQHctU/Ydk7L/cQUt1tg/1RoPiWmv/376nzMjcxxb/23MwFz0L3sG9Ne/5P651z+30vd/udc/N/3Lpn/Z9C+b/mXTv2z6l03/sulfNv3Lpn/Z9C+b/mXTv2z6l03/9boL0H8d/ef6nFCo+xxzoP96+t+OeBKg/z7656B/fG/9t23t93/6769/6L29+fm7hNrHDF2ffP573Ida93rffD/3OXzp+a//sKH3/C3177tG6D8stOc6Gduv3U44fs72Tf/cHdbav+n8+gKX7s/nbK///Guf4hnNXO31T/88LdQ/xwxM2SOU3n+p87+x1HOiqXtE/Zft38zAnP/v5ZxnBPr/L+X9/ND+IeR7hJj2+o/3u3QOYvrHzMDPSPpPEzsHrw5x/f9UvgRaN75VPtWv1X/e/rGq/m9j+598PoT7f2297tTyd03/6f3J3wEAAAAAAAAAAAAAAABY3l9OOPTh
</data>
</layer>
<objectgroup id="1" name="PlayerStartingPos">
<object id="135" x="882" y="1437.33">
<object id="135" x="1290" y="1401.33">
<point/>
</object>
<object id="137" x="955.333" y="1434.67">
<object id="137" x="1648.33" y="1430.67">
<point/>
</object>
</objectgroup>
@@ -95,18 +95,6 @@
<property name="boundary_type" value="barrier"/>
</properties>
</object>
<object id="80" x="1296" y="1600">
<properties>
<property name="boundary_type" value="barrier"/>
</properties>
<polyline points="0,0 -32.5,0 -32.25,-16.5 -16.5,-16.5"/>
</object>
<object id="82" x="1328" y="1616">
<properties>
<property name="boundary_type" value="barrier"/>
</properties>
<polyline points="0,0 -64.5,0 -64.0038,-15.75 -16.4734,-15.75"/>
</object>
<object id="83" x="640" y="480" width="1056" height="16">
<properties>
<property name="boundary_type" value="barrier"/>
@@ -127,5 +115,90 @@
<property name="boundary_type" value="barrier"/>
</properties>
</object>
<object id="87" x="1456" y="1568" width="224" height="48">
<properties>
<property name="boundary_type" value="barrier"/>
</properties>
</object>
<object id="88" x="1264" y="1584" width="16" height="32">
<properties>
<property name="boundary_type" value="barrier"/>
</properties>
</object>
<object id="89" x="1280" y="1600" width="16" height="16">
<properties>
<property name="boundary_type" value="barrier"/>
</properties>
</object>
<object id="90" x="1232" y="1408" width="304" height="16">
<properties>
<property name="boundary_type" value="barrier"/>
</properties>
</object>
<object id="91" x="1440" y="1584" width="16" height="32">
<properties>
<property name="boundary_type" value="barrier"/>
</properties>
</object>
<object id="92" x="1424" y="1600" width="16" height="16">
<properties>
<property name="boundary_type" value="barrier"/>
</properties>
</object>
<object id="93" x="1488" y="1552" width="192" height="16">
<properties>
<property name="boundary_type" value="barrier"/>
</properties>
</object>
<object id="94" x="1504" y="1536" width="176" height="16">
<properties>
<property name="boundary_type" value="barrier"/>
</properties>
</object>
<object id="95" x="1520" y="1520" width="160" height="16">
<properties>
<property name="boundary_type" value="barrier"/>
</properties>
</object>
<object id="96" x="1568" y="1408" width="16" height="16">
<properties>
<property name="boundary_type" value="barrier"/>
</properties>
</object>
<object id="97" x="1248" y="1328" width="158" height="16">
<properties>
<property name="boundary_type" value="barrier"/>
</properties>
</object>
<object id="98" x="1280" y="1312" width="96" height="16">
<properties>
<property name="boundary_type" value="barrier"/>
</properties>
</object>
<object id="99" x="1536" y="1504" width="144" height="16">
<properties>
<property name="boundary_type" value="barrier"/>
</properties>
</object>
<object id="100" x="1552" y="1488" width="128" height="16">
<properties>
<property name="boundary_type" value="barrier"/>
</properties>
</object>
<object id="101" x="1568" y="1472" width="112" height="16">
<properties>
<property name="boundary_type" value="barrier"/>
</properties>
</object>
<object id="102" x="1584" y="1456" width="96" height="16">
<properties>
<property name="boundary_type" value="barrier"/>
</properties>
</object>
<object id="103" x="1600" y="1440" width="80" height="16">
<properties>
<property name="boundary_type" value="barrier"/>
</properties>
</object>
</objectgroup>
</map>

File diff suppressed because it is too large Load Diff

View File

@@ -440,7 +440,7 @@
"array": [
0,
0,
216.19964242526865,
216.05530045313827,
0,
0,
0,

View File

@@ -460,7 +460,7 @@
"array": [
0,
0,
215.81269742929726,
216.05530045313827,
0,
0,
0,

View File

@@ -347,7 +347,6 @@ cc.Class({
const mapNode = self.node;
const canvasNode = mapNode.parent;
cc.director.getCollisionManager().enabled = false;
// self.musicEffectManagerScriptIns = self.node.getComponent("MusicEffectManager");
self.musicEffectManagerScriptIns = null;
@@ -615,7 +614,7 @@ cc.Class({
}
}
if (null == self.renderFrameId || self.renderFrameId <= rdf.id) {
if (null == self.renderFrameId || self.renderFrameId <= rdf.id || shouldForceResync) {
// In fact, not having "window.RING_BUFF_CONSECUTIVE_SET == dumpRenderCacheRet" should already imply that "self.renderFrameId <= rdf.id", but here we double check and log the anomaly
if (window.MAGIC_ROOM_DOWNSYNC_FRAME_ID.BATTLE_START == rdf.id) {
@@ -1327,9 +1326,11 @@ cc.Class({
{renderFrame.id: ${currRenderFrame.id}, possiblyFallStoppedOnAnotherPlayer: ${possiblyFallStoppedOnAnotherPlayer}}
playerColliderPos=${self.stringifyColliderCenterInWorld(playerCollider, halfColliderWidth, halfColliderHeight, topPadding, bottomPadding, leftPadding, rightPadding)}, effPushback={${effPushbacks[joinIndex - 1][0].toFixed(3)}, ${effPushbacks[joinIndex - 1][1].toFixed(3)}}, overlayMag=${result.overlap.toFixed(4)}`);
} else if (currPlayerDownsync.inAir && isBarrier && !landedOnGravityPushback) {
/*
console.warn(`playerId=${playerId}, joinIndex=${currPlayerDownsync.joinIndex} inAir & pushed back by barrier & not landed:
{renderFrame.id: ${currRenderFrame.id}}
playerColliderPos=${self.stringifyColliderCenterInWorld(playerCollider, halfColliderWidth, halfColliderHeight, topPadding, bottomPadding, leftPadding, rightPadding)}, effPushback={${effPushbacks[joinIndex - 1][0].toFixed(3)}, ${effPushbacks[joinIndex - 1][1].toFixed(3)}}, overlayMag=${result.overlap.toFixed(4)}, len(hardPushbackNorms)=${hardPushbackNorms.length}`);
*/
} else if (currPlayerDownsync.inAir && isAnotherPlayer) {
console.warn(`playerId=${playerId}, joinIndex=${currPlayerDownsync.joinIndex} inAir and pushed back by another player
{renderFrame.id: ${currRenderFrame.id}}

View File

@@ -15,8 +15,6 @@ cc.Class({
window.mapIns = self;
self.showCriticalCoordinateLabels = true;
cc.director.getCollisionManager().enabled = false;
const mapNode = self.node;
const canvasNode = mapNode.parent;

View File

@@ -144,6 +144,7 @@ window.initPersistentSessionClient = function(onopenCb, expectedRoomId) {
if (null == evt || null == evt.data) {
return;
}
// FIXME: In practice, it seems like the thread invoking "onmessage" could be different from "Map.update(dt)", which makes it necessary to guard "recentRenderCache & recentInputCache" for "_generateInputFrameUpsync & rollbackAndChase & onRoomDownsyncFrame & onInputFrameDownsyncBatch" to avoid mysterious RAM contamination, but there's no explicit mutex in JavaScript for browsers.
try {
const resp = window.pb.protos.WsResp.decode(new Uint8Array(evt.data));
//console.log(`Got non-empty onmessage decoded: resp.act=${resp.act}`);

View File

@@ -17,13 +17,13 @@
},
"encryptJs": true,
"excludeScenes": [
"475b849b-44b3-4390-982d-bd0d9e695093"
"368b10b6-88fc-423c-9fcd-545d9fc673bd"
],
"fb-instant-games": {},
"includeSDKBox": false,
"inlineSpriteFrames": true,
"inlineSpriteFrames_native": true,
"md5Cache": true,
"md5Cache": false,
"mergeStartScene": true,
"optimizeHotUpdate": false,
"orientation": {

View File

@@ -34,7 +34,6 @@
"design-resolution-width": 960,
"excluded-modules": [
"Collider",
"DragonBones",
"Geom Utils",
"Mesh",
"MotionStreak",
@@ -69,7 +68,7 @@
"shelter_z_reducer",
"shelter"
],
"last-module-event-record-time": 1579766511782,
"last-module-event-record-time": 1671346284377,
"simulator-orientation": false,
"simulator-resolution": {
"height": 640,