Compare commits

...

32 Commits

Author SHA1 Message Date
genxium
8de2d6e4e7 Enhancement for type#1 force-confirmation trigger. 2023-01-31 09:57:37 +08:00
genxium
ba2dd0b22e Added thread-safety comments for libuv codes. 2023-01-30 23:41:22 +08:00
genxium
754610d31b Updated documentation. 2023-01-30 09:56:19 +08:00
genxium
a35de9b83c Enhanced backend udp tunnel workflow. 2023-01-30 09:21:06 +08:00
genxium
2b6cb57050 Enabled backend udp tunnel forwarding. 2023-01-30 00:25:11 +08:00
genxium
677e76179c Minor fix. 2023-01-29 22:38:12 +08:00
genxium
c65c122f45 Added front-to-back UDP channel punching. 2023-01-29 18:20:48 +08:00
genxium
b5530b352b Updated pb files. 2023-01-29 13:10:19 +08:00
genxium
4e638fb2ec Preparing in battle udp tunnel on backend. 2023-01-29 12:42:48 +08:00
genxium
7c454130db Minor fix. 2023-01-28 23:50:32 +08:00
genxium
5863f88435 Minor update. 2023-01-28 12:55:22 +08:00
genxium
bbf07fe518 Fixed Android segfault on mysterious udp reception callback. 2023-01-28 10:52:58 +08:00
genxium
26660d75d2 Minor fix. 2023-01-28 00:13:30 +08:00
genxium
76cdbc8f1f Updated trouble shooting doc. 2023-01-28 00:10:10 +08:00
genxium
4097a8da75 Fixed libuv multithreading. 2023-01-27 22:51:34 +08:00
genxium
e7bf6ec16b Added basic input upsync via udp. 2023-01-26 21:34:29 +08:00
genxium
7ab983949c Fixes in udp_session type usage for cross-platform use. 2023-01-26 12:43:38 +08:00
genxium
2028f8277d Fixed Android building setup. 2023-01-26 00:08:46 +08:00
genxium
60bb74169e Fixed libuv resource deallocation. 2023-01-25 23:47:54 +08:00
genxium
8536521136 Integrated basic holepunching for multiplayer codebase. 2023-01-25 18:26:13 +08:00
genxium
5df545e168 Fixes for frontend hole punching compatibility. 2023-01-25 11:57:59 +08:00
genxium
6bc3feab58 Fixed part of Cpp to Js callback scopes. 2023-01-24 21:30:58 +08:00
genxium
e21e1b840f Drafted udp holepunching upsync pathway. 2023-01-24 12:00:49 +08:00
genxium
ef345e0e48 Fine tune for rollback params. 2023-01-24 10:08:34 +08:00
genxium
0168e2182e Further updates on win32 build templates. 2023-01-23 23:18:21 +08:00
genxium
58b06f6a10 Updated win32 build templates. 2023-01-23 12:22:34 +08:00
genxium
58e60a789f Preparation for native app build. 2023-01-22 23:05:32 +08:00
genxium
b5b43bb596 Minor update. 2023-01-22 14:32:32 +08:00
genxium
59767c1ed5 Added network doctor stats. 2023-01-22 11:39:27 +08:00
genxium
1c6ad5c8f8 Minor fix. 2023-01-21 23:03:55 +08:00
genxium
34e0893eb8 Added network doctor for frontend. 2023-01-21 22:53:41 +08:00
genxium
cc7524becd Updated README. 2023-01-21 11:59:01 +08:00
54 changed files with 5402 additions and 2128 deletions

2
.gitignore vendored
View File

@@ -1,3 +1,5 @@
.vs
**/.vs
battle_srv/test_cases/test_cases
battle_srv/test_cases/tests
*.pid

View File

@@ -1,21 +1,21 @@
# Preface
This project is a demo for a websocket-based rollback netcode inspired by [GGPO](https://github.com/pond3r/ggpo/blob/master/doc/README.md). As lots of feedbacks ask for a discussion on using UDP instead, I tried to summarize my personal opinion about it in [ConcerningEdgeCases](./ConcerningEdgeCases.md) -- not necessarily correct but that's indeed a question to face :)
This project is a demo for a websocket-based rollback netcode inspired by [GGPO](https://github.com/pond3r/ggpo/blob/master/doc/README.md).
As lots of feedbacks ask for a discussion on using UDP instead, I tried to summarize my personal opinion about it in [ConcerningEdgeCases](./ConcerningEdgeCases.md) -- **since v0.9.25, the project is actually equipped with UDP capabilities as follows**.
- When using the so called `native apps` on `Android` and `Windows` (I'm working casually hard to support `iOS` next), the frontends will try to use UDP hole-punching w/ the help of backend as a registry. If UDP hole-punching is working, the rollback is often less than `turn-around frames to recover` and thus not noticeable, being much better than using websocket alone.
- If UDP hole-punching is not working, e.g. for Symmetric NAT like in 4G/5G cellular network, the frontends will use backend as a UDP tunnel (or relay, whatever you like to call it). This video shows how the UDP tunnel performs for a [Phone-4G v.s. PC-Wifi (viewed by PC side)](https://pan.baidu.com/s/1wdUTvRiyrTLWy7mF6G7uyQ?pwd=icmp).
- Browser vs `native app` is possible but in that case only websocket is used.
The following video is recorded over INTERNET using an input delay of 4 frames and it feels SMOOTH when playing! Please also checkout these demo videos
- [source video of the first gif](https://pan.baidu.com/s/1ML6hNupaPHPJRd5rcTvQvw?pwd=8ruc)
- [phone v.s. PC over internet battle#1](https://pan.baidu.com/s/1NuGxuMwrV_jalcToyUZPLg?pwd=kfkr)
- [phone v.s. PC over internet battle#2](https://pan.baidu.com/s/1kMiFdwDHyJpZJ0GGU1Y3eA?pwd=46gd)
- [PC Wifi viewing Phone 4g](https://pan.baidu.com/s/1PJEtC9iB_fcabMWhbx2oAg?pwd=tp7k)
- [PC Wifi viewing Phone Wifi (over internet of course)](https://pan.baidu.com/s/108rvC1CcUdiQeMauXWsLJg?pwd=mn39)
- [source video of the first gif (earlier version)](https://pan.baidu.com/s/1ML6hNupaPHPJRd5rcTvQvw?pwd=8ruc)
- [source video of the second gif (added turn-around optimization & dashing)](https://pan.baidu.com/s/1isMcLvxax4NNkDgitV_FDg?pwd=s1i6)
to see how this demo carries out a full 60fps synchronization with the help of _batched input upsync/downsync_ for satisfying network I/O performance.
![gif_demo_1](./charts/internet_fireball_explosion_wallmove_spedup.gif)
![gif_demo_2](./charts/jump_sync_spedup.gif)
All gifs are sped up to ~1.5x for file size reduction, kindly note that animations are resumed from a partial progress!
![gif_demo_2](./charts/internet_dash_turnaround_cut_spedup.gif)
# Notable Features
- Backend dynamics toggle via [Room.BackendDynamicsEnabled](https://github.com/genxium/DelayNoMore/blob/v0.9.14/battle_srv/models/room.go#L786)
@@ -111,3 +111,40 @@ Moreover, in practice I found that to spot sync anomalies, the following tools a
- Detection of [prediction mismatch on the frontend](https://github.com/genxium/DelayNoMore/blob/v0.9.19/frontend/assets/scripts/Map.js#L842).
- Detection of [type#1 forceConfirmation on the backend](https://github.com/genxium/DelayNoMore/blob/v0.9.19/battle_srv/models/room.go#L1246).
- Detection of [type#2 forceConfirmation on the backend](https://github.com/genxium/DelayNoMore/blob/v0.9.19/battle_srv/models/room.go#L1259).
There's also some useful information displayed on the frontend when `true == Map.showNetworkDoctorInfo`.
![networkstats](./charts/networkstats.png)
### 2.3 WIN32 platform tool versioning
![visual_studio](./charts/VisualStudioSetup.png)
When building for native platforms, it's much more convenient to trigger the CocosCreator project forming frmo CLI, e.g.
```
shell> cd <proj-root>
shell> /path/to/CocosCreator.exe --path ./frontend --build "platform=win32;debug=true"
```
### 2.4 CococCreator native build reloading
CocosCreator doesn't have perfect file cache management during native project building, use "Developer Tools > Reload" to reset the IDE status upon mysterious errors.
![ccc_reload](./charts/NativeBuildReload.png)
Another issue is with the package name, see the screenshot below, kindly note that after successfully building with a blank package name, you can then re-fill the desired package name and build again!
![ccc_package_name](./charts/PackageNameIssueInCcc.png)
### 2.5 Checking UDP port binding result
__*nix__
```
netstat -anp | grep <your_port>
```
__Windows__
```
netstat -ano | grep <your_port>
```
### 2.6 Checking native code crash on non-rooted Android phone
```
DeveloperOs> adb bugreport ./logs.zip
# The file "logs.zip" will be automatically pulled to current folder of the DeveloperOS, copy "logs/FS/data/tomestones" out of the zip, then use the binary "$NDK_ROOT/ndk-stack" to analyze whichever tombstone you're interested in, for example, I often use the following
DeveloperOs> ${NDK_ROOT}/ndk-stack.cmd -sym \path\to\DelayNoMore\frontend\build\jsb-link\frameworks\runtime-src\proj.android-studio\app\build\intermediates\ndkBuild\debug\obj\local\arm64-v8a -dump \path\to\tombstones\tombstone_03
# The param "-sym \path\to\objs" tells "ndk-stack" to decode "tombstone_03" with symbols provided by all the files inside that "\path\to\objs".
```

View File

@@ -37,6 +37,8 @@ type mysqlConf struct {
type sioConf struct {
HostAndPort string `json:"hostAndPort"`
UdpHost string `json:"udpHost"`
UdpPort int `json:"udpPort"`
}
type botServerConf struct {

View File

@@ -1,3 +1,5 @@
{
"hostAndPort": "0.0.0.0:9992"
"hostAndPort": "0.0.0.0:9992",
"udpHost": "0.0.0.0",
"udpPort": 3000
}

View File

@@ -23,6 +23,8 @@ import (
"github.com/gin-gonic/gin"
"github.com/robfig/cron"
"go.uber.org/zap"
"net"
)
func main() {
@@ -34,7 +36,7 @@ func main() {
env_tools.MergeTestPlayerAccounts()
}
models.InitRoomHeapManager()
startScheduler()
// startScheduler()
router := gin.Default()
setRouter(router)
@@ -54,6 +56,7 @@ func main() {
}
Logger.Info("Listening and serving HTTP on", zap.Any("Conf.Sio.HostAndPort", Conf.Sio.HostAndPort))
}()
go startGrandUdpServer()
var gracefulStop = make(chan os.Signal)
signal.Notify(gracefulStop, syscall.SIGTERM)
signal.Notify(gracefulStop, syscall.SIGINT)
@@ -114,3 +117,33 @@ func startScheduler() {
//c.AddFunc("*/1 * * * * *", FuncName)
c.Start()
}
func startGrandUdpServer() {
conn, err := net.ListenUDP("udp", &net.UDPAddr{
Port: Conf.Sio.UdpPort,
IP: net.ParseIP(Conf.Sio.UdpHost),
})
if err != nil {
panic(err)
}
defer func() {
conn.Close()
if r := recover(); r != nil {
Logger.Error("`GrandUdpServer`, recovery spot#1, recovered from: ", zap.Any("panic", r))
}
Logger.Info(fmt.Sprintf("The `GrandUdpServer` is stopped"))
}()
Logger.Info(fmt.Sprintf("`GrandUdpServer` started at %s", conn.LocalAddr().String()))
for {
message := make([]byte, 2046)
rlen, remote, err := conn.ReadFromUDP(message[:])
if err != nil {
panic(err)
}
Logger.Info(fmt.Sprintf("`GrandUdpServer` received: %d bytes from %s\n", rlen, remote))
ws.HandleUdpHolePunchingForPlayer(message[0:rlen], remote)
}
}

View File

@@ -8,6 +8,7 @@ import (
sq "github.com/Masterminds/squirrel"
"github.com/jmoiron/sqlx"
"go.uber.org/zap"
"net"
)
type PlayerBattleState struct {
@@ -50,6 +51,10 @@ type Player struct {
LastSentInputFrameId int32
AckingFrameId int32
AckingInputFrameId int32
UdpAddr *PeerUdpAddr
BattleUdpTunnelAddr *net.UDPAddr // This addr is used by backend only, not visible to frontend
BattleUdpTunnelAuthKey int32
}
func ExistPlayerByName(name string) (bool, error) {

View File

@@ -13,6 +13,7 @@ import (
"io/ioutil"
"jsexport/battle"
"math/rand"
"net"
"os"
"path/filepath"
"resolv"
@@ -32,6 +33,7 @@ const (
DOWNSYNC_MSG_ACT_BATTLE_STOPPED = int32(3)
DOWNSYNC_MSG_ACT_FORCED_RESYNC = int32(4)
DOWNSYNC_MSG_ACT_PEER_INPUT_BATCH = int32(5)
DOWNSYNC_MSG_ACT_PEER_UDP_ADDR = int32(6)
DOWNSYNC_MSG_ACT_BATTLE_READY_TO_START = int32(-1)
DOWNSYNC_MSG_ACT_BATTLE_START = int32(0)
@@ -156,6 +158,10 @@ type Room struct {
rdfIdToActuallyUsedInput map[int32]*pb.InputFrameDownsync
LastIndividuallyConfirmedInputList []uint64
BattleUdpTunnelLock sync.Mutex
BattleUdpTunnelAddr *pb.PeerUdpAddr
BattleUdpTunnel *net.UDPConn
}
func (pR *Room) updateScore() {
@@ -176,6 +182,9 @@ func (pR *Room) AddPlayerIfPossible(pPlayerFromDbInit *Player, session *websocke
defer pR.onPlayerAdded(playerId)
pPlayerFromDbInit.UdpAddr = nil
pPlayerFromDbInit.BattleUdpTunnelAddr = nil
pPlayerFromDbInit.BattleUdpTunnelAuthKey = rand.Int31()
pPlayerFromDbInit.AckingFrameId = -1
pPlayerFromDbInit.AckingInputFrameId = -1
pPlayerFromDbInit.LastSentInputFrameId = MAGIC_LAST_SENT_INPUT_FRAME_ID_NORMAL_ADDED
@@ -215,6 +224,9 @@ func (pR *Room) ReAddPlayerIfPossible(pTmpPlayerInstance *Player, session *webso
*/
defer pR.onPlayerReAdded(playerId)
pEffectiveInRoomPlayerInstance := pR.Players[playerId]
pEffectiveInRoomPlayerInstance.UdpAddr = nil
pEffectiveInRoomPlayerInstance.BattleUdpTunnelAddr = nil
pEffectiveInRoomPlayerInstance.BattleUdpTunnelAuthKey = rand.Int31()
pEffectiveInRoomPlayerInstance.AckingFrameId = -1
pEffectiveInRoomPlayerInstance.AckingInputFrameId = -1
pEffectiveInRoomPlayerInstance.LastSentInputFrameId = MAGIC_LAST_SENT_INPUT_FRAME_ID_READDED
@@ -315,8 +327,8 @@ func (pR *Room) InputsBufferString(allDetails bool) string {
// Appending of the array of strings can be very SLOW due to on-demand heap allocation! Use this printing with caution.
s := make([]string, 0)
s = append(s, fmt.Sprintf("{renderFrameId: %v, stInputFrameId: %v, edInputFrameId: %v, lastAllConfirmedInputFrameIdWithChange: %v, lastAllConfirmedInputFrameId: %v}", pR.RenderFrameId, pR.InputsBuffer.StFrameId, pR.InputsBuffer.EdFrameId, pR.LastAllConfirmedInputFrameIdWithChange, pR.LastAllConfirmedInputFrameId))
for playerId, player := range pR.PlayersArr {
s = append(s, fmt.Sprintf("{playerId: %v, ackingFrameId: %v, ackingInputFrameId: %v, lastSentInputFrameId: %v}", playerId, player.AckingFrameId, player.AckingInputFrameId, player.LastSentInputFrameId))
for _, player := range pR.PlayersArr {
s = append(s, fmt.Sprintf("{playerId: %v, ackingFrameId: %v, ackingInputFrameId: %v, lastSentInputFrameId: %v}", player.Id, player.AckingFrameId, player.AckingInputFrameId, player.LastSentInputFrameId))
}
for i := pR.InputsBuffer.StFrameId; i < pR.InputsBuffer.EdFrameId; i++ {
tmp := pR.InputsBuffer.GetByFrameId(i)
@@ -562,7 +574,7 @@ func (pR *Room) StartBattle() {
})
}
func (pR *Room) OnBattleCmdReceived(pReq *pb.WsReq) {
func (pR *Room) OnBattleCmdReceived(pReq *pb.WsReq, fromUDP bool) {
/*
[WARNING] This function "OnBattleCmdReceived" could be called by different ws sessions and thus from different threads!
@@ -607,20 +619,21 @@ func (pR *Room) OnBattleCmdReceived(pReq *pb.WsReq) {
//Logger.Debug(fmt.Sprintf("OnBattleCmdReceived-InputsBufferLock unlocked: roomId=%v, fromPlayerId=%v", pR.Id, playerId))
}()
inputsBufferSnapshot := pR.markConfirmationIfApplicable(inputFrameUpsyncBatch, playerId, player)
inputsBufferSnapshot := pR.markConfirmationIfApplicable(inputFrameUpsyncBatch, playerId, player, fromUDP)
if nil != inputsBufferSnapshot {
pR.downsyncToAllPlayers(inputsBufferSnapshot)
} else {
// no new all-confirmed
toSendInputFrameDownsyncs := pR.cloneInputsBuffer(inputFrameUpsyncBatch[0].InputFrameId, inputFrameUpsyncBatch[len(inputFrameUpsyncBatch)-1].InputFrameId+1)
} /*else {
// FIXME: Enable this block after we can proactively detect whether there's any "secondary ws session player" in the battle to avoid waste of resource in creating the snapshot
// no new all-confirmed
toSendInputFrameDownsyncs := pR.cloneInputsBuffer(inputFrameUpsyncBatch[0].InputFrameId, inputFrameUpsyncBatch[len(inputFrameUpsyncBatch)-1].InputFrameId+1)
inputsBufferSnapshot = &pb.InputsBufferSnapshot{
ToSendInputFrameDownsyncs: toSendInputFrameDownsyncs,
PeerJoinIndex: player.JoinIndex,
}
//Logger.Info(fmt.Sprintf("OnBattleCmdReceived no new all-confirmed: roomId=%v, fromPlayerId=%v, forming peer broadcasting snapshot=%v", pR.Id, playerId, inputsBufferSnapshot))
pR.broadcastPeerUpsyncForBetterPrediction(inputsBufferSnapshot)
}
inputsBufferSnapshot = &pb.InputsBufferSnapshot{
ToSendInputFrameDownsyncs: toSendInputFrameDownsyncs,
PeerJoinIndex: player.JoinIndex,
}
//Logger.Info(fmt.Sprintf("OnBattleCmdReceived no new all-confirmed: roomId=%v, fromPlayerId=%v, forming peer broadcasting snapshot=%v", pR.Id, playerId, inputsBufferSnapshot))
pR.broadcastPeerUpsyncForBetterPrediction(inputsBufferSnapshot)
}*/
}
func (pR *Room) onInputFrameDownsyncAllConfirmed(inputFrameDownsync *battle.InputFrameDownsync, playerId int32) {
@@ -662,6 +675,10 @@ func (pR *Room) StopBattleForSettlement() {
if RoomBattleStateIns.IN_BATTLE != pR.State {
return
}
pR.BattleUdpTunnelLock.Lock()
pR.BattleUdpTunnel.Close()
pR.BattleUdpTunnelLock.Unlock()
pR.State = RoomBattleStateIns.STOPPING_BATTLE_FOR_SETTLEMENT
Logger.Info("Stopping the `battleMainLoop` for:", zap.Any("roomId", pR.Id))
pR.RenderFrameId++
@@ -808,18 +825,23 @@ func (pR *Room) OnDismissed() {
pR.BattleDurationFrames = int32(60 * serverFps)
pR.BattleDurationNanos = int64(pR.BattleDurationFrames) * (pR.RollbackEstimatedDtNanos + 1)
pR.InputFrameUpsyncDelayTolerance = battle.ConvertToNoDelayInputFrameId(pR.NstDelayFrames) - 1 // this value should be strictly smaller than (NstDelayFrames >> InputScaleFrames), otherwise "type#1 forceConfirmation" might become a lag avalanche
pR.MaxChasingRenderFramesPerUpdate = 4 // Don't set this value too high to avoid exhausting frontend CPU within a single frame
pR.MaxChasingRenderFramesPerUpdate = 9 // Don't set this value too high to avoid exhausting frontend CPU within a single frame, roughly as the "turn-around frames to recover" is empirically OK
pR.BackendDynamicsEnabled = true // [WARNING] When "false", recovery upon reconnection wouldn't work!
pR.ForceAllResyncOnAnyActiveSlowTicker = true // See tradeoff discussion in "downsyncToAllPlayers"
pR.FrameDataLoggingEnabled = false // [WARNING] DON'T ENABLE ON LONG BATTLE DURATION! It consumes A LOT OF MEMORY!
pR.BattleUdpTunnelLock.Lock()
pR.BattleUdpTunnel = nil
pR.BattleUdpTunnelAddr = nil
pR.BattleUdpTunnelLock.Unlock()
pR.ChooseStage()
pR.EffectivePlayerCount = 0
// [WARNING] It's deliberately ordered such that "pR.State = RoomBattleStateIns.IDLE" is put AFTER all the refreshing operations above.
pR.State = RoomBattleStateIns.IDLE
go pR.startBattleUdpTunnel() // Would reassign "pR.BattleUdpTunnel"
pR.updateScore()
Logger.Info("The room is completely dismissed(all playerDownsyncChan closed):", zap.Any("roomId", pR.Id))
@@ -1068,7 +1090,9 @@ func (pR *Room) sendSafely(roomDownsyncFrame *pb.RoomDownsyncFrame, toSendInputF
panic(fmt.Sprintf("Error marshaling downsync message: roomId=%v, playerId=%v, roomState=%v, roomEffectivePlayerCount=%v", pR.Id, playerId, pR.State, pR.EffectivePlayerCount))
}
if MAGIC_JOIN_INDEX_DEFAULT == peerJoinIndex {
shouldUseSecondaryWsSession := (MAGIC_JOIN_INDEX_DEFAULT != peerJoinIndex && DOWNSYNC_MSG_ACT_INPUT_BATCH == act) // FIXME: Simplify the condition
//Logger.Info(fmt.Sprintf("shouldUseSecondaryWsSession=%v: roomId=%v, playerId=%v, roomState=%v, roomEffectivePlayerCount=%v", shouldUseSecondaryWsSession, pR.Id, playerId, pR.State, pR.EffectivePlayerCount))
if !shouldUseSecondaryWsSession {
if playerDownsyncSession, existent := pR.PlayerDownsyncSessionDict[playerId]; existent {
if err := playerDownsyncSession.WriteMessage(websocket.BinaryMessage, theBytes); nil != err {
panic(fmt.Sprintf("Error sending primary downsync message: roomId=%v, playerId=%v, roomState=%v, roomEffectivePlayerCount=%v, err=%v", pR.Id, playerId, pR.State, pR.EffectivePlayerCount, err))
@@ -1135,7 +1159,7 @@ func (pR *Room) getOrPrefabInputFrameDownsync(inputFrameId int32) *battle.InputF
return currInputFrameDownsync
}
func (pR *Room) markConfirmationIfApplicable(inputFrameUpsyncBatch []*pb.InputFrameUpsync, playerId int32, player *Player) *pb.InputsBufferSnapshot {
func (pR *Room) markConfirmationIfApplicable(inputFrameUpsyncBatch []*pb.InputFrameUpsync, playerId int32, player *Player, fromUDP bool) *pb.InputsBufferSnapshot {
// [WARNING] This function MUST BE called while "pR.InputsBufferLock" is locked!
// Step#1, put the received "inputFrameUpsyncBatch" into "pR.InputsBuffer"
for _, inputFrameUpsync := range inputFrameUpsyncBatch {
@@ -1158,11 +1182,14 @@ func (pR *Room) markConfirmationIfApplicable(inputFrameUpsyncBatch []*pb.InputFr
targetInputFrameDownsync.InputList[player.JoinIndex-1] = inputFrameUpsync.Encoded
targetInputFrameDownsync.ConfirmedList |= uint64(1 << uint32(player.JoinIndex-1))
player.LastReceivedInputFrameId = clientInputFrameId
pR.LastIndividuallyConfirmedInputList[player.JoinIndex-1] = inputFrameUpsync.Encoded
if clientInputFrameId > pR.LatestPlayerUpsyncedInputFrameId {
pR.LatestPlayerUpsyncedInputFrameId = clientInputFrameId
if false == fromUDP {
// [WARNING] We have to distinguish whether or not the incoming batch is from UDP here, otherwise "pR.LatestPlayerUpsyncedInputFrameId - pR.LastAllConfirmedInputFrameId" might become unexpectedly large in case of "UDP packet loss + slow ws session"!
player.LastReceivedInputFrameId = clientInputFrameId
if clientInputFrameId > pR.LatestPlayerUpsyncedInputFrameId {
pR.LatestPlayerUpsyncedInputFrameId = clientInputFrameId
}
// It's safe (in terms of getting an eventually correct "RenderFrameBuffer") to put the following update of "pR.LastIndividuallyConfirmedInputList" which is ONLY used for prediction in "InputsBuffer" out of "false == fromUDP" block, but I'm still putting it in for convenient debugging.
pR.LastIndividuallyConfirmedInputList[player.JoinIndex-1] = inputFrameUpsync.Encoded
}
}
@@ -1350,13 +1377,13 @@ func (pR *Room) printBarrier(barrierCollider *resolv.Object) {
}
func (pR *Room) doBattleMainLoopPerTickBackendDynamicsWithProperLocking(prevRenderFrameId int32, pDynamicsDuration *int64) {
Logger.Debug(fmt.Sprintf("doBattleMainLoopPerTickBackendDynamicsWithProperLocking-InputsBufferLock to about lock: roomId=%v", pR.Id))
//Logger.Debug(fmt.Sprintf("doBattleMainLoopPerTickBackendDynamicsWithProperLocking-InputsBufferLock to about lock: roomId=%v", pR.Id))
pR.InputsBufferLock.Lock()
Logger.Debug(fmt.Sprintf("doBattleMainLoopPerTickBackendDynamicsWithProperLocking-InputsBufferLock locked: roomId=%v", pR.Id))
//Logger.Debug(fmt.Sprintf("doBattleMainLoopPerTickBackendDynamicsWithProperLocking-InputsBufferLock locked: roomId=%v", pR.Id))
defer func() {
pR.InputsBufferLock.Unlock()
Logger.Debug(fmt.Sprintf("doBattleMainLoopPerTickBackendDynamicsWithProperLocking-InputsBufferLock unlocked: roomId=%v", pR.Id))
//Logger.Debug(fmt.Sprintf("doBattleMainLoopPerTickBackendDynamicsWithProperLocking-InputsBufferLock unlocked: roomId=%v", pR.Id))
}()
if ok, thatRenderFrameId := battle.ShouldPrefabInputFrameDownsync(prevRenderFrameId, pR.RenderFrameId); ok {
@@ -1607,3 +1634,144 @@ func (pR *Room) SetSecondarySession(playerId int32, session *websocket.Conn, sig
}
}
}
func (pR *Room) UpdatePeerUdpAddrList(playerId int32, peerAddr *net.UDPAddr, pReq *pb.HolePunchUpsync) {
// TODO: There's a chance that by now "player.JoinIndex" is not yet determined, use a lock to sync
if player, ok := pR.Players[playerId]; ok && MAGIC_JOIN_INDEX_DEFAULT != player.JoinIndex {
playerBattleState := atomic.LoadInt32(&(player.BattleState))
switch playerBattleState {
case PlayerBattleStateIns.DISCONNECTED, PlayerBattleStateIns.LOST, PlayerBattleStateIns.EXPELLED_DURING_GAME, PlayerBattleStateIns.EXPELLED_IN_DISMISSAL:
// Kindly note that "PlayerBattleStateIns.ADDED_PENDING_BATTLE_COLLIDER_ACK, PlayerBattleStateIns.READDED_PENDING_BATTLE_COLLIDER_ACK" are allowed
return
}
if _, existent := pR.PlayerDownsyncSessionDict[playerId]; existent {
player.UdpAddr = &pb.PeerUdpAddr{
Ip: peerAddr.IP.String(),
Port: int32(peerAddr.Port),
AuthKey: pReq.AuthKey,
}
Logger.Info(fmt.Sprintf("UpdatePeerUdpAddrList done for roomId=%v, playerId=%d, peerAddr=%s", pR.Id, playerId, peerAddr))
peerJoinIndex := player.JoinIndex
peerUdpAddrList := make([]*pb.PeerUdpAddr, pR.Capacity, pR.Capacity)
for _, otherPlayer := range pR.Players {
if MAGIC_JOIN_INDEX_DEFAULT == otherPlayer.JoinIndex {
// TODO: Again this shouldn't happen, apply proper locking
continue
}
// In case of highly concurrent update that might occur while later marshalling, use the ptr of a copy
peerUdpAddrList[otherPlayer.JoinIndex-1] = &pb.PeerUdpAddr{
Ip: otherPlayer.UdpAddr.Ip,
Port: otherPlayer.UdpAddr.Port,
AuthKey: otherPlayer.UdpAddr.AuthKey,
}
}
// Broadcast this new UDP addr to all the existing players
for otherPlayerId, otherPlayer := range pR.Players {
otherPlayerBattleState := atomic.LoadInt32(&(otherPlayer.BattleState))
switch otherPlayerBattleState {
case PlayerBattleStateIns.DISCONNECTED, PlayerBattleStateIns.LOST, PlayerBattleStateIns.EXPELLED_DURING_GAME, PlayerBattleStateIns.EXPELLED_IN_DISMISSAL:
continue
}
Logger.Info(fmt.Sprintf("Downsyncing peerUdpAddrList for roomId=%v, playerId=%d", pR.Id, otherPlayerId))
pR.sendSafely(&pb.RoomDownsyncFrame{
PeerUdpAddrList: peerUdpAddrList,
}, nil, DOWNSYNC_MSG_ACT_PEER_UDP_ADDR, otherPlayerId, false, peerJoinIndex)
}
}
}
}
func (pR *Room) startBattleUdpTunnel() {
defer func() {
if r := recover(); r != nil {
Logger.Error("`BattleUdpTunnel` recovery spot#1, recovered from: ", zap.Any("roomId", pR.Id), zap.Any("panic", r))
}
Logger.Info(fmt.Sprintf("`BattleUdpTunnel` stopped for (roomId=%d)@renderFrameId=%v", pR.Id, pR.RenderFrameId))
}()
pR.BattleUdpTunnelLock.Lock()
conn, err := net.ListenUDP("udp", &net.UDPAddr{
Port: 0,
IP: net.ParseIP(Conf.Sio.UdpHost),
})
if nil != err {
// No need to close the "conn" upon error here
pR.BattleUdpTunnelLock.Unlock()
panic(err)
}
pR.BattleUdpTunnel = conn
switch v := conn.LocalAddr().(type) {
case (*net.UDPAddr):
pR.BattleUdpTunnelAddr = &pb.PeerUdpAddr{
Ip: Conf.Sio.UdpHost,
Port: int32(v.Port),
AuthKey: 0, // To be determined for each specific player upon joining and sent to it by BattleColliderInfo
}
}
pR.BattleUdpTunnelLock.Unlock()
defer func() {
if r := recover(); r != nil {
Logger.Warn("`BattleUdpTunnel` recovery spot#2, recovered from: ", zap.Any("roomId", pR.Id), zap.Any("panic", r))
}
Logger.Info(fmt.Sprintf("`BattleUdpTunnel` closed for (roomId=%d)@renderFrameId=%v", pR.Id, pR.RenderFrameId))
}()
Logger.Info(fmt.Sprintf("`BattleUdpTunnel` started for roomId=%d at %s", pR.Id, conn.LocalAddr().String()))
for {
message := make([]byte, 128)
rlen, remote, err := conn.ReadFromUDP(message[:]) // Would be unblocked when "conn.Close()" is called from another thread/goroutine, reference https://pkg.go.dev/net@go1.18.6#PacketConn
if nil != err {
// Should proceed to close the "conn" upon error here, if "conn" is already closed it'd just throw another error to be catched by "spot#2"
conn.Close()
panic(err)
}
pReq := new(pb.WsReq)
bytes := message[0:rlen]
if unmarshalErr := proto.Unmarshal(bytes, pReq); nil != unmarshalErr {
Logger.Warn("`BattleUdpTunnel` for roomId=%d failed to unmarshal", zap.Error(unmarshalErr))
continue
}
playerId := pReq.PlayerId
//Logger.Info(fmt.Sprintf("`BattleUdpTunnel` for roomId=%d received decoded WsReq:", pR.Id), zap.Any("pReq", pReq))
if player, exists1 := pR.Players[playerId]; exists1 {
authKey := pReq.AuthKey
if authKey != player.BattleUdpTunnelAuthKey {
Logger.Warn(fmt.Sprintf("`BattleUdpTunnel` for roomId=%d received %d bytes for playerId=%d from %s, but (incomingAuthKey:%d != playerBattleUdpTunnelAuthKey:%d)\n", pR.Id, rlen, playerId, remote, authKey, player.BattleUdpTunnelAuthKey))
continue
}
if _, existent := pR.PlayerDownsyncSessionDict[playerId]; existent {
player.BattleUdpTunnelAddr = remote
//Logger.Info(fmt.Sprintf("`BattleUdpTunnel` for roomId=%d updated battleUdpAddr for playerId=%d to be %s\n", pR.Id, playerId, remote))
nowBattleState := atomic.LoadInt32(&pR.State)
if RoomBattleStateIns.IN_BATTLE == nowBattleState {
batch := pReq.InputFrameUpsyncBatch
if nil != batch && 0 < len(batch) {
peerJoinIndex := pReq.JoinIndex
// Broadcast to every other player in the same room/battle
for _, otherPlayer := range pR.PlayersArr {
if otherPlayer.JoinIndex == peerJoinIndex {
continue
}
_, wrerr := conn.WriteTo(bytes, otherPlayer.BattleUdpTunnelAddr)
if nil != wrerr {
Logger.Warn(fmt.Sprintf("`BattleUdpTunnel` for roomId=%d failed to forward upsync from (playerId:%d, joinIndex:%d, addr:%s) to (otherPlayerId:%d, otherPlayerJoinIndex:%d, otherPlayerAddr:%s)\n", pR.Id, playerId, peerJoinIndex, remote, otherPlayer.Id, otherPlayer.JoinIndex, otherPlayer.BattleUdpTunnelAddr))
}
}
pR.OnBattleCmdReceived(pReq, true) // To help advance "pR.LastAllConfirmedInputFrameId" asap, and even if "pR.LastAllConfirmedInputFrameId" is not advanced due to packet loss, these UDP packets would help prefill the "InputsBuffer" with correct player "future inputs (compared to ws session)" such that when "forceConfirmation" occurs we have as many correct predictions as possible
}
}
} else {
Logger.Warn(fmt.Sprintf("`BattleUdpTunnel` for roomId=%d received validated %d bytes for playerId=%d from %s, but primary downsync session for it doesn't exist\n", pR.Id, rlen, playerId, remote))
}
} else {
Logger.Warn(fmt.Sprintf("`BattleUdpTunnel` for roomId=%d received invalid %d bytes for playerId=%d from %s, but it doesn't belong to this room!\n", pR.Id, rlen, playerId, remote))
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -10,6 +10,7 @@ import (
"github.com/golang/protobuf/proto"
"github.com/gorilla/websocket"
"go.uber.org/zap"
"net"
"net/http"
"strconv"
"sync/atomic"
@@ -256,17 +257,25 @@ func Serve(c *gin.Context) {
SpaceOffsetX: pRoom.SpaceOffsetX,
SpaceOffsetY: pRoom.SpaceOffsetY,
RenderCacheSize: pRoom.RenderCacheSize,
CollisionMinStep: pRoom.CollisionMinStep,
RenderCacheSize: pRoom.RenderCacheSize,
CollisionMinStep: pRoom.CollisionMinStep,
BoundRoomCapacity: int32(pRoom.Capacity),
BattleUdpTunnel: &pb.PeerUdpAddr{
Ip: pRoom.BattleUdpTunnelAddr.Ip,
Port: pRoom.BattleUdpTunnelAddr.Port,
AuthKey: pThePlayer.BattleUdpTunnelAuthKey,
},
FrameDataLoggingEnabled: pRoom.FrameDataLoggingEnabled,
}
resp := &pb.WsResp{
Ret: int32(Constants.RetCode.Ok),
EchoedMsgId: int32(0),
Act: models.DOWNSYNC_MSG_ACT_HB_REQ,
BciFrame: bciFrame,
Ret: int32(Constants.RetCode.Ok),
EchoedMsgId: int32(0),
Act: models.DOWNSYNC_MSG_ACT_HB_REQ,
BciFrame: bciFrame,
PeerJoinIndex: pThePlayer.JoinIndex,
}
Logger.Debug("Sending downsync HeartbeatRequirements:", zap.Any("roomId", pRoom.Id), zap.Any("playerId", playerId), zap.Any("resp", resp))
@@ -379,7 +388,7 @@ func Serve(c *gin.Context) {
startOrFeedHeartbeatWatchdog(conn)
case models.UPSYNC_MSG_ACT_PLAYER_CMD:
startOrFeedHeartbeatWatchdog(conn)
pRoom.OnBattleCmdReceived(pReq)
pRoom.OnBattleCmdReceived(pReq, false)
case models.UPSYNC_MSG_ACT_PLAYER_COLLIDER_ACK:
res := pRoom.OnPlayerBattleColliderAcked(int32(playerId))
if false == res {
@@ -432,12 +441,12 @@ func HandleSecondaryWsSessionForPlayer(c *gin.Context) {
playerId, err := models.GetPlayerIdByToken(token)
if err != nil || playerId == 0 {
// TODO: Abort with specific message.
Logger.Warn("Secondary ws session playerLogin record not found for ws authentication:", zap.Any("intAuthToken", token))
Logger.Warn("Secondary ws session playerLogin record not found:", zap.Any("intAuthToken", token))
c.AbortWithStatus(http.StatusBadRequest)
return
}
Logger.Info("Secondary ws session playerLogin record has been found for ws authentication:", zap.Any("playerId", playerId), zap.Any("intAuthToken", token), zap.Any("boundRoomId", boundRoomId))
Logger.Info("Secondary ws session playerLogin record has been found:", zap.Any("playerId", playerId), zap.Any("intAuthToken", token), zap.Any("boundRoomId", boundRoomId))
conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
if err != nil {
@@ -482,3 +491,32 @@ func HandleSecondaryWsSessionForPlayer(c *gin.Context) {
pRoom.SetSecondarySession(int32(playerId), conn, signalToCloseConnOfThisPlayer)
}
func HandleUdpHolePunchingForPlayer(message []byte, peerAddr *net.UDPAddr) {
pReq := new(pb.HolePunchUpsync)
if unmarshalErr := proto.Unmarshal(message, pReq); nil != unmarshalErr {
Logger.Error("`GrandUdpServer` failed to unmarshal", zap.Error(unmarshalErr))
return
}
token := pReq.IntAuthToken
boundRoomId := pReq.BoundRoomId
pRoom, existent := (*models.RoomMapManagerIns)[int32(boundRoomId)]
// Deliberately querying playerId after querying room, because the former is against persistent storage and could be slow!
if !existent {
Logger.Warn("`GrandUdpServer` failed to get:\n", zap.Any("intAuthToken", token), zap.Any("forBoundRoomId", boundRoomId))
return
}
// TODO: Wrap the following 2 stmts by sql transaction!
playerId, err := models.GetPlayerIdByToken(token)
if err != nil || playerId == 0 {
// TODO: Abort with specific message.
Logger.Warn("`GrandUdpServer` playerLogin record not found for:", zap.Any("intAuthToken", token))
return
}
Logger.Info("`GrandUdpServer` playerLogin record has been found:", zap.Any("playerId", playerId), zap.Any("intAuthToken", token), zap.Any("boundRoomId", boundRoomId), zap.Any("peerAddr", peerAddr))
pRoom.UpdatePeerUdpAddrList(int32(playerId), peerAddr, pReq)
}

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 417 KiB

After

Width:  |  Height:  |  Size: 472 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 191 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 MiB

BIN
charts/networkstats.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

View File

@@ -1,66 +0,0 @@
function NetworkDoctor(serverFps, clientUpsyncFps) {
this.serverFps = serverFps;
this.clientUpsyncFps = clientUpsyncFps;
this.millisPerServerFrame = parseInt(1000 / this.serverFps);
this._tooLongSinceLastFrameDiffReceivedThreshold = (this.millisPerServerFrame << 6);
this.setupFps = function(fps) {
this.serverFps = this.clientUpsyncFps = fps;
this.millisPerServerFrame = parseInt(1000 / this.serverFps);
this._tooLongSinceLastFrameDiffReceivedThreshold = (this.millisPerServerFrame << 6);
}
this._lastFrameDiffRecvTime = null;
this._tooLongSinceLastFrameDiffReceived = function() {
if (undefined === this._lastFrameDiffRecvTime || null === this._lastFrameDiffRecvTime) return false;
return (this._tooLongSinceLastFrameDiffReceivedThreshold <= (Date.now() - this._lastFrameDiffRecvTime));
};
this._consecutiveALittleLongFrameDiffReceivedIntervalCount = 0;
this._consecutiveALittleLongFrameDiffReceivedIntervalCountThreshold = 120;
this.onNewFrameDiffReceived = function(frameDiff) {
var now = Date.now();
if (undefined !== this._lastFrameDiffRecvTime && null !== this._lastFrameDiffRecvTime) {
var intervalFromLastFrameDiff = (now - this._lastFrameDiffRecvTime);
if ((this.millisPerServerFrame << 5) < intervalFromLastFrameDiff) {
++this._consecutiveALittleLongFrameDiffReceivedIntervalCount;
console.log('Medium delay, intervalFromLastFrameDiff is', intervalFromLastFrameDiff);
} else {
this._consecutiveALittleLongFrameDiffReceivedIntervalCount = 0;
}
}
this._lastFrameDiffRecvTime = now;
};
this._networkComplaintPrefix = "\nNetwork is not good >_<\n";
this.generateNetworkComplaint = function(excludeTypeConstantALittleLongFrameDiffReceivedInterval, excludeTypeTooLongSinceLastFrameDiffReceived) {
if (this.hasBattleStopped) return null;
var shouldComplain = false;
var ret = this._networkComplaintPrefix;
if (true != excludeTypeConstantALittleLongFrameDiffReceivedInterval && this._consecutiveALittleLongFrameDiffReceivedIntervalCountThreshold <= this._consecutiveALittleLongFrameDiffReceivedIntervalCount) {
this._consecutiveALittleLongFrameDiffReceivedIntervalCount = 0;
ret += "\nConstantly having a little long recv interval.\n";
shouldComplain = true;
}
if (true != excludeTypeTooLongSinceLastFrameDiffReceived && this._tooLongSinceLastFrameDiffReceived()) {
ret += "\nToo long since last received frameDiff.\n";
shouldComplain = true;
}
return (shouldComplain ? ret : null);
};
this.hasBattleStopped = false;
this.onBattleStopped = function() {
this.hasBattleStopped = true;
};
this.isClientSessionConnected = function() {
if (!window.game) return false;
if (!window.game.clientSession) return false;
return window.game.clientSession.connected;
};
}
window.NetworkDoctor = NetworkDoctor;

View File

@@ -198,3 +198,6 @@ window.getOrCreateAnimationClipForGid = function(gid, tiledMapInfo, tilesElListU
animationClip: animClip,
};
};
// Node.js, this is a workaround to avoid accessing the non-existent "TextDecoder class" from "jsexport.js".
window.fs = function() {};

View File

@@ -73,18 +73,9 @@ message WsReq {
int32 joinIndex = 4;
int32 ackingFrameId = 5;
int32 ackingInputFrameId = 6;
repeated InputFrameUpsync inputFrameUpsyncBatch = 7;
HeartbeatUpsync hb = 8;
}
message WsResp {
int32 ret = 1;
int32 echoedMsgId = 2;
int32 act = 3;
RoomDownsyncFrame rdf = 4;
repeated InputFrameDownsync inputFrameDownsyncBatch = 5;
BattleColliderInfo bciFrame = 6;
int32 peerJoinIndex = 7; // Only used when "InputsBufferSnapshot.peerJoinIndex" is used.
int32 authKey = 7;
repeated InputFrameUpsync inputFrameUpsyncBatch = 8;
HeartbeatUpsync hb = 9;
}
message InputsBufferSnapshot {
@@ -92,7 +83,7 @@ message InputsBufferSnapshot {
uint64 unconfirmedMask = 2;
repeated InputFrameDownsync toSendInputFrameDownsyncs = 3;
bool shouldForceResync = 4;
int32 peerJoinIndex = 5; // Only used when "WsResp.peerJoinIndex" is used.
int32 peerJoinIndex = 5;
}
message MeleeBullet {
@@ -175,6 +166,18 @@ message FireballBullet {
int32 speed = 1005;
}
message HolePunchUpsync {
string intAuthToken = 1;
int32 boundRoomId = 2;
int32 authKey = 3;
}
message PeerUdpAddr {
string ip = 1;
int32 port = 2;
int32 authKey = 3;
}
message BattleColliderInfo {
string stageName = 1;
@@ -191,6 +194,8 @@ message BattleColliderInfo {
double spaceOffsetX = 11;
double spaceOffsetY = 12;
int32 collisionMinStep = 13;
int32 boundRoomCapacity = 14;
PeerUdpAddr battleUdpTunnel = 15;
bool frameDataLoggingEnabled = 1024;
}
@@ -207,4 +212,15 @@ message RoomDownsyncFrame {
repeated int32 speciesIdList = 1026;
int32 bulletLocalIdCounter = 1027;
repeated PeerUdpAddr peerUdpAddrList = 1028;
}
message WsResp {
int32 ret = 1;
int32 echoedMsgId = 2;
int32 act = 3;
RoomDownsyncFrame rdf = 4;
repeated InputFrameDownsync inputFrameDownsyncBatch = 5;
BattleColliderInfo bciFrame = 6;
int32 peerJoinIndex = 7;
}

View File

@@ -78,19 +78,19 @@
"_active": true,
"_components": [
{
"__id__": 40
"__id__": 50
},
{
"__id__": 41
"__id__": 51
},
{
"__id__": 42
"__id__": 52
},
{
"__id__": 43
"__id__": 53
},
{
"__id__": 44
"__id__": 54
}
],
"_prefab": null,
@@ -158,7 +158,7 @@
"__id__": 5
},
{
"__id__": 39
"__id__": 49
}
],
"_prefab": null,
@@ -247,10 +247,10 @@
"__uuid__": "670b477e-61a1-4778-879b-35913f7c79d2"
},
"boundRoomIdLabel": {
"__id__": 16
"__id__": 26
},
"countdownLabel": {
"__id__": 23
"__id__": 33
},
"resultPanelPrefab": {
"__uuid__": "c4cfe3bd-c59e-4d5b-95cb-c933b120e184"
@@ -269,9 +269,21 @@
},
"forceBigEndianFloatingNumDecoding": false,
"renderFrameIdLagTolerance": 4,
"jigglingEps1D": 0.001,
"bulletTriggerEnabled": true,
"closeOnForcedtoResyncNotSelf": true,
"sendingQLabel": {
"__id__": 13
},
"inputFrameDownsyncQLabel": {
"__id__": 15
},
"peerInputFrameUpsyncQLabel": {
"__id__": 17
},
"rollbackFramesLabel": {
"__id__": 19
},
"skippedRenderFrameCntLabel": {
"__id__": 21
},
"_id": "d12gkAmppNlIzqcRDELa91"
},
{
@@ -283,13 +295,13 @@
},
"_children": [
{
"__id__": 33
"__id__": 43
}
],
"_active": true,
"_components": [
{
"__id__": 38
"__id__": 48
}
],
"_prefab": null,
@@ -352,19 +364,19 @@
"__id__": 6
},
{
"__id__": 22
"__id__": 32
},
{
"__id__": 24
"__id__": 34
},
{
"__id__": 28
"__id__": 38
}
],
"_active": true,
"_components": [
{
"__id__": 32
"__id__": 42
}
],
"_prefab": null,
@@ -433,7 +445,7 @@
"_active": true,
"_components": [
{
"__id__": 21
"__id__": 31
}
],
"_prefab": null,
@@ -527,7 +539,7 @@
"array": [
0,
0,
209.57814771583418,
210.16474188040044,
0,
0,
0,
@@ -596,15 +608,30 @@
"_children": [
{
"__id__": 12
},
{
"__id__": 14
},
{
"__id__": 16
},
{
"__id__": 18
},
{
"__id__": 20
},
{
"__id__": 22
}
],
"_active": true,
"_components": [
{
"__id__": 19
"__id__": 29
},
{
"__id__": 20
"__id__": 30
}
],
"_prefab": null,
@@ -631,7 +658,7 @@
"ctor": "Float64Array",
"array": [
-447.294,
135.702,
138.942,
0,
0,
0,
@@ -655,6 +682,476 @@
"groupIndex": 0,
"_id": "35scg40jVAnKrTPiei5ckg"
},
{
"__type__": "cc.Node",
"_name": "sendingQ",
"_objFlags": 0,
"_parent": {
"__id__": 11
},
"_children": [],
"_active": true,
"_components": [
{
"__id__": 13
}
],
"_prefab": null,
"_opacity": 255,
"_color": {
"__type__": "cc.Color",
"r": 255,
"g": 255,
"b": 255,
"a": 255
},
"_contentSize": {
"__type__": "cc.Size",
"width": 18.33,
"height": 22.61
},
"_anchorPoint": {
"__type__": "cc.Vec2",
"x": 0,
"y": 0.5
},
"_trs": {
"__type__": "TypedArray",
"ctor": "Float64Array",
"array": [
0,
88.695,
0,
0,
0,
0,
1,
1,
1,
1
]
},
"_eulerAngles": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"_skewX": 0,
"_skewY": 0,
"_is3DNode": false,
"_groupIndex": 0,
"groupIndex": 0,
"_id": "ack7XH+0lEj6chSUsKBLU5"
},
{
"__type__": "cc.Label",
"_name": "",
"_objFlags": 0,
"node": {
"__id__": 12
},
"_enabled": true,
"_materials": [
{
"__uuid__": "eca5d2f2-8ef6-41c2-bbe6-f9c79d09c432"
}
],
"_useOriginalSize": false,
"_string": "0",
"_N$string": "0",
"_fontSize": 22,
"_lineHeight": 37,
"_enableWrapText": true,
"_N$file": {
"__uuid__": "a564b3db-b8cb-48b4-952e-25bb56949116"
},
"_isSystemFontUsed": false,
"_spacingX": 0,
"_batchAsBitmap": false,
"_N$horizontalAlign": 0,
"_N$verticalAlign": 1,
"_N$fontFamily": "Arial",
"_N$overflow": 0,
"_N$cacheMode": 0,
"_id": "deCJfLuoFO36c/O4lXWJ1N"
},
{
"__type__": "cc.Node",
"_name": "inputFrameDownsyncQ",
"_objFlags": 0,
"_parent": {
"__id__": 11
},
"_children": [],
"_active": true,
"_components": [
{
"__id__": 15
}
],
"_prefab": null,
"_opacity": 255,
"_color": {
"__type__": "cc.Color",
"r": 255,
"g": 255,
"b": 255,
"a": 255
},
"_contentSize": {
"__type__": "cc.Size",
"width": 18.33,
"height": 22.61
},
"_anchorPoint": {
"__type__": "cc.Vec2",
"x": 0,
"y": 0.5
},
"_trs": {
"__type__": "TypedArray",
"ctor": "Float64Array",
"array": [
0,
66.08499999999998,
0,
0,
0,
0,
1,
1,
1,
1
]
},
"_eulerAngles": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"_skewX": 0,
"_skewY": 0,
"_is3DNode": false,
"_groupIndex": 0,
"groupIndex": 0,
"_id": "d9n+NRm9pA0YtKxvy4bW+U"
},
{
"__type__": "cc.Label",
"_name": "",
"_objFlags": 0,
"node": {
"__id__": 14
},
"_enabled": true,
"_materials": [
{
"__uuid__": "eca5d2f2-8ef6-41c2-bbe6-f9c79d09c432"
}
],
"_useOriginalSize": false,
"_string": "0",
"_N$string": "0",
"_fontSize": 22,
"_lineHeight": 37,
"_enableWrapText": true,
"_N$file": {
"__uuid__": "a564b3db-b8cb-48b4-952e-25bb56949116"
},
"_isSystemFontUsed": false,
"_spacingX": 0,
"_batchAsBitmap": false,
"_N$horizontalAlign": 0,
"_N$verticalAlign": 1,
"_N$fontFamily": "Arial",
"_N$overflow": 0,
"_N$cacheMode": 0,
"_id": "90NvjGFrpBFqqzl1WZczta"
},
{
"__type__": "cc.Node",
"_name": "peerInputFrameUpsyncQ",
"_objFlags": 0,
"_parent": {
"__id__": 11
},
"_children": [],
"_active": true,
"_components": [
{
"__id__": 17
}
],
"_prefab": null,
"_opacity": 255,
"_color": {
"__type__": "cc.Color",
"r": 255,
"g": 255,
"b": 255,
"a": 255
},
"_contentSize": {
"__type__": "cc.Size",
"width": 18.33,
"height": 22.61
},
"_anchorPoint": {
"__type__": "cc.Vec2",
"x": 0,
"y": 0.5
},
"_trs": {
"__type__": "TypedArray",
"ctor": "Float64Array",
"array": [
0,
43.47499999999998,
0,
0,
0,
0,
1,
1,
1,
1
]
},
"_eulerAngles": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"_skewX": 0,
"_skewY": 0,
"_is3DNode": false,
"_groupIndex": 0,
"groupIndex": 0,
"_id": "45FAmgRLZNNbLt/GcnTXVx"
},
{
"__type__": "cc.Label",
"_name": "",
"_objFlags": 0,
"node": {
"__id__": 16
},
"_enabled": true,
"_materials": [
{
"__uuid__": "eca5d2f2-8ef6-41c2-bbe6-f9c79d09c432"
}
],
"_useOriginalSize": false,
"_string": "0",
"_N$string": "0",
"_fontSize": 22,
"_lineHeight": 37,
"_enableWrapText": true,
"_N$file": {
"__uuid__": "a564b3db-b8cb-48b4-952e-25bb56949116"
},
"_isSystemFontUsed": false,
"_spacingX": 0,
"_batchAsBitmap": false,
"_N$horizontalAlign": 0,
"_N$verticalAlign": 1,
"_N$fontFamily": "Arial",
"_N$overflow": 0,
"_N$cacheMode": 0,
"_id": "42PRTrjEpDw6Z8N5yWFTpd"
},
{
"__type__": "cc.Node",
"_name": "rollbackFrames",
"_objFlags": 0,
"_parent": {
"__id__": 11
},
"_children": [],
"_active": true,
"_components": [
{
"__id__": 19
}
],
"_prefab": null,
"_opacity": 255,
"_color": {
"__type__": "cc.Color",
"r": 255,
"g": 255,
"b": 255,
"a": 255
},
"_contentSize": {
"__type__": "cc.Size",
"width": 18.33,
"height": 22.61
},
"_anchorPoint": {
"__type__": "cc.Vec2",
"x": 0,
"y": 0.5
},
"_trs": {
"__type__": "TypedArray",
"ctor": "Float64Array",
"array": [
0,
20.86499999999998,
0,
0,
0,
0,
1,
1,
1,
1
]
},
"_eulerAngles": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"_skewX": 0,
"_skewY": 0,
"_is3DNode": false,
"_groupIndex": 0,
"groupIndex": 0,
"_id": "f5SBs3b1pPNbHbnqVLYsHp"
},
{
"__type__": "cc.Label",
"_name": "",
"_objFlags": 0,
"node": {
"__id__": 18
},
"_enabled": true,
"_materials": [
{
"__uuid__": "eca5d2f2-8ef6-41c2-bbe6-f9c79d09c432"
}
],
"_useOriginalSize": false,
"_string": "0",
"_N$string": "0",
"_fontSize": 22,
"_lineHeight": 37,
"_enableWrapText": true,
"_N$file": {
"__uuid__": "a564b3db-b8cb-48b4-952e-25bb56949116"
},
"_isSystemFontUsed": false,
"_spacingX": 0,
"_batchAsBitmap": false,
"_N$horizontalAlign": 0,
"_N$verticalAlign": 1,
"_N$fontFamily": "Arial",
"_N$overflow": 0,
"_N$cacheMode": 0,
"_id": "77aNARt1VATLsjIzwbqvkh"
},
{
"__type__": "cc.Node",
"_name": "skippedCnt",
"_objFlags": 0,
"_parent": {
"__id__": 11
},
"_children": [],
"_active": true,
"_components": [
{
"__id__": 21
}
],
"_prefab": null,
"_opacity": 255,
"_color": {
"__type__": "cc.Color",
"r": 243,
"g": 225,
"b": 11,
"a": 255
},
"_contentSize": {
"__type__": "cc.Size",
"width": 18.33,
"height": 22.61
},
"_anchorPoint": {
"__type__": "cc.Vec2",
"x": 0,
"y": 0.5
},
"_trs": {
"__type__": "TypedArray",
"ctor": "Float64Array",
"array": [
0,
-1.7450000000000188,
0,
0,
0,
0,
1,
1,
1,
1
]
},
"_eulerAngles": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"_skewX": 0,
"_skewY": 0,
"_is3DNode": false,
"_groupIndex": 0,
"groupIndex": 0,
"_id": "cdlF8Z8TZEdLRHQQ8T8qX7"
},
{
"__type__": "cc.Label",
"_name": "",
"_objFlags": 0,
"node": {
"__id__": 20
},
"_enabled": true,
"_materials": [
{
"__uuid__": "eca5d2f2-8ef6-41c2-bbe6-f9c79d09c432"
}
],
"_useOriginalSize": false,
"_string": "0",
"_N$string": "0",
"_fontSize": 22,
"_lineHeight": 37,
"_enableWrapText": true,
"_N$file": {
"__uuid__": "a564b3db-b8cb-48b4-952e-25bb56949116"
},
"_isSystemFontUsed": false,
"_spacingX": 0,
"_batchAsBitmap": false,
"_N$horizontalAlign": 0,
"_N$verticalAlign": 1,
"_N$fontFamily": "Arial",
"_N$overflow": 0,
"_N$cacheMode": 0,
"_id": "2flWrlaK1PeJMUB/in+S1W"
},
{
"__type__": "cc.Node",
"_name": "RoomIdIndicator",
@@ -664,19 +1161,19 @@
},
"_children": [
{
"__id__": 13
"__id__": 23
},
{
"__id__": 15
"__id__": 25
}
],
"_active": false,
"_components": [
{
"__id__": 17
"__id__": 27
},
{
"__id__": 18
"__id__": 28
}
],
"_prefab": null,
@@ -732,13 +1229,13 @@
"_name": "label",
"_objFlags": 0,
"_parent": {
"__id__": 12
"__id__": 22
},
"_children": [],
"_active": true,
"_components": [
{
"__id__": 14
"__id__": 24
}
],
"_prefab": null,
@@ -794,7 +1291,7 @@
"_name": "",
"_objFlags": 0,
"node": {
"__id__": 13
"__id__": 23
},
"_enabled": true,
"_materials": [],
@@ -822,13 +1319,13 @@
"_name": "BoundRoomIdLabel",
"_objFlags": 0,
"_parent": {
"__id__": 12
"__id__": 22
},
"_children": [],
"_active": true,
"_components": [
{
"__id__": 16
"__id__": 26
}
],
"_prefab": null,
@@ -884,7 +1381,7 @@
"_name": "",
"_objFlags": 0,
"node": {
"__id__": 15
"__id__": 25
},
"_enabled": true,
"_materials": [],
@@ -912,7 +1409,7 @@
"_name": "",
"_objFlags": 0,
"node": {
"__id__": 12
"__id__": 22
},
"_enabled": true,
"_layoutSize": {
@@ -945,7 +1442,7 @@
"_name": "",
"_objFlags": 0,
"node": {
"__id__": 12
"__id__": 22
},
"_enabled": true,
"_materials": [],
@@ -1067,7 +1564,7 @@
"_active": true,
"_components": [
{
"__id__": 23
"__id__": 33
}
],
"_prefab": null,
@@ -1123,7 +1620,7 @@
"_name": "",
"_objFlags": 0,
"node": {
"__id__": 22
"__id__": 32
},
"_enabled": true,
"_materials": [
@@ -1157,7 +1654,7 @@
},
"_children": [
{
"__id__": 25
"__id__": 35
}
],
"_active": true,
@@ -1215,16 +1712,16 @@
"_name": "Background",
"_objFlags": 0,
"_parent": {
"__id__": 24
"__id__": 34
},
"_children": [],
"_active": true,
"_components": [
{
"__id__": 26
"__id__": 36
},
{
"__id__": 27
"__id__": 37
}
],
"_prefab": null,
@@ -1280,7 +1777,7 @@
"_name": "",
"_objFlags": 0,
"node": {
"__id__": 25
"__id__": 35
},
"_enabled": true,
"_materials": [
@@ -1314,7 +1811,7 @@
"_name": "",
"_objFlags": 0,
"node": {
"__id__": 25
"__id__": 35
},
"_enabled": true,
"alignMode": 0,
@@ -1345,7 +1842,7 @@
},
"_children": [
{
"__id__": 29
"__id__": 39
}
],
"_active": true,
@@ -1403,16 +1900,16 @@
"_name": "Background",
"_objFlags": 0,
"_parent": {
"__id__": 28
"__id__": 38
},
"_children": [],
"_active": true,
"_components": [
{
"__id__": 30
"__id__": 40
},
{
"__id__": 31
"__id__": 41
}
],
"_prefab": null,
@@ -1468,7 +1965,7 @@
"_name": "",
"_objFlags": 0,
"node": {
"__id__": 29
"__id__": 39
},
"_enabled": true,
"_materials": [
@@ -1502,7 +1999,7 @@
"_name": "",
"_objFlags": 0,
"node": {
"__id__": 29
"__id__": 39
},
"_enabled": true,
"alignMode": 0,
@@ -1560,16 +2057,16 @@
},
"_children": [
{
"__id__": 34
"__id__": 44
}
],
"_active": true,
"_components": [
{
"__id__": 36
"__id__": 46
},
{
"__id__": 37
"__id__": 47
}
],
"_prefab": null,
@@ -1625,13 +2122,13 @@
"_name": "Joystick",
"_objFlags": 0,
"_parent": {
"__id__": 33
"__id__": 43
},
"_children": [],
"_active": true,
"_components": [
{
"__id__": 35
"__id__": 45
}
],
"_prefab": null,
@@ -1687,7 +2184,7 @@
"_name": "",
"_objFlags": 0,
"node": {
"__id__": 34
"__id__": 44
},
"_enabled": true,
"_materials": [
@@ -1721,7 +2218,7 @@
"_name": "",
"_objFlags": 0,
"node": {
"__id__": 33
"__id__": 43
},
"_enabled": true,
"_materials": [
@@ -1755,7 +2252,7 @@
"_name": "",
"_objFlags": 0,
"node": {
"__id__": 33
"__id__": 43
},
"_enabled": true,
"alignMode": 0,
@@ -1908,10 +2405,10 @@
"__id__": 3
},
"stickhead": {
"__id__": 34
"__id__": 44
},
"base": {
"__id__": 33
"__id__": 43
},
"joyStickEps": 0.1,
"magicLeanLowerBound": 0.414,
@@ -1932,10 +2429,10 @@
"linearMovingEps": 0.1,
"scaleByEps": 0.0375,
"btnA": {
"__id__": 24
"__id__": 34
},
"btnB": {
"__id__": 28
"__id__": 38
},
"_id": "e9oVYTr7ROlpp/IrNjBUmR"
}

View File

@@ -72,22 +72,25 @@
"__id__": 3
},
{
"__id__": 7
"__id__": 5
},
{
"__id__": 9
},
{
"__id__": 11
}
],
"_active": true,
"_components": [
{
"__id__": 60
},
{
"__id__": 61
},
{
"__id__": 62
},
{
"__id__": 63
},
{
"__id__": 64
}
],
"_prefab": null,
@@ -138,6 +141,102 @@
"groupIndex": 0,
"_id": "88kscZWXFCIZtNFekdSP/o"
},
{
"__type__": "cc.Node",
"_name": "Background",
"_objFlags": 0,
"_parent": {
"__id__": 2
},
"_children": [],
"_active": true,
"_components": [
{
"__id__": 4
}
],
"_prefab": null,
"_opacity": 255,
"_color": {
"__type__": "cc.Color",
"r": 255,
"g": 255,
"b": 255,
"a": 255
},
"_contentSize": {
"__type__": "cc.Size",
"width": 1280,
"height": 960
},
"_anchorPoint": {
"__type__": "cc.Vec2",
"x": 0.5,
"y": 0.5
},
"_trs": {
"__type__": "TypedArray",
"ctor": "Float64Array",
"array": [
0,
0,
0,
0,
0,
0,
1,
1,
1,
1
]
},
"_eulerAngles": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"_skewX": 0,
"_skewY": 0,
"_is3DNode": false,
"_groupIndex": 0,
"groupIndex": 0,
"_id": "c8r+ISXVhBZarylAeQMAQK"
},
{
"__type__": "cc.Sprite",
"_name": "",
"_objFlags": 0,
"node": {
"__id__": 3
},
"_enabled": true,
"_materials": [
{
"__uuid__": "eca5d2f2-8ef6-41c2-bbe6-f9c79d09c432"
}
],
"_srcBlendFactor": 770,
"_dstBlendFactor": 771,
"_spriteFrame": {
"__uuid__": "637f31c2-c53e-4dec-ae11-d56c0c6177ad"
},
"_type": 0,
"_sizeMode": 0,
"_fillType": 0,
"_fillCenter": {
"__type__": "cc.Vec2",
"x": 0,
"y": 0
},
"_fillStart": 0,
"_fillRange": 0,
"_isTrimmedMode": true,
"_atlas": {
"__uuid__": "030d9286-e8a2-40cf-98f8-baf713f0b8c4"
},
"_id": "6dhdFxELpEwZ4TjXC+WnyS"
},
{
"__type__": "cc.Node",
"_name": "Decorations",
@@ -147,13 +246,13 @@
},
"_children": [
{
"__id__": 4
"__id__": 6
}
],
"_active": true,
"_components": [
{
"__id__": 6
"__id__": 8
}
],
"_prefab": null,
@@ -209,13 +308,13 @@
"_name": "Logo",
"_objFlags": 0,
"_parent": {
"__id__": 3
"__id__": 5
},
"_children": [],
"_active": true,
"_components": [
{
"__id__": 5
"__id__": 7
}
],
"_prefab": null,
@@ -271,7 +370,7 @@
"_name": "",
"_objFlags": 0,
"node": {
"__id__": 4
"__id__": 6
},
"_enabled": true,
"_materials": [
@@ -301,7 +400,7 @@
"_name": "",
"_objFlags": 0,
"node": {
"__id__": 3
"__id__": 5
},
"_enabled": true,
"alignMode": 0,
@@ -334,7 +433,7 @@
"_active": true,
"_components": [
{
"__id__": 8
"__id__": 10
}
],
"_prefab": null,
@@ -362,7 +461,7 @@
"array": [
0,
0,
210.43934936178934,
209.73151519075364,
0,
0,
0,
@@ -390,7 +489,7 @@
"_name": "",
"_objFlags": 0,
"node": {
"__id__": 7
"__id__": 9
},
"_enabled": true,
"_cullingMask": 4294967295,
@@ -430,13 +529,13 @@
},
"_children": [
{
"__id__": 10
"__id__": 12
}
],
"_active": true,
"_components": [
{
"__id__": 59
"__id__": 61
}
],
"_prefab": null,
@@ -492,41 +591,41 @@
"_name": "InteractiveControls",
"_objFlags": 0,
"_parent": {
"__id__": 9
"__id__": 11
},
"_children": [
{
"__id__": 11
},
{
"__id__": 19
"__id__": 13
},
{
"__id__": 21
},
{
"__id__": 30
"__id__": 23
},
{
"__id__": 32
},
{
"__id__": 41
"__id__": 34
},
{
"__id__": 48
"__id__": 43
},
{
"__id__": 54
"__id__": 50
},
{
"__id__": 56
},
{
"__id__": 58
}
],
"_active": true,
"_components": [
{
"__id__": 58
"__id__": 60
}
],
"_prefab": null,
@@ -582,23 +681,23 @@
"_name": "phoneCountryCodeInput",
"_objFlags": 0,
"_parent": {
"__id__": 10
"__id__": 12
},
"_children": [
{
"__id__": 12
},
{
"__id__": 14
},
{
"__id__": 16
},
{
"__id__": 18
}
],
"_active": true,
"_components": [
{
"__id__": 18
"__id__": 20
}
],
"_prefab": null,
@@ -654,13 +753,13 @@
"_name": "BACKGROUND_SPRITE",
"_objFlags": 0,
"_parent": {
"__id__": 11
"__id__": 13
},
"_children": [],
"_active": true,
"_components": [
{
"__id__": 13
"__id__": 15
}
],
"_prefab": null,
@@ -716,7 +815,7 @@
"_name": "",
"_objFlags": 0,
"node": {
"__id__": 12
"__id__": 14
},
"_enabled": true,
"_materials": [
@@ -748,13 +847,13 @@
"_name": "TEXT_LABEL",
"_objFlags": 0,
"_parent": {
"__id__": 11
"__id__": 13
},
"_children": [],
"_active": true,
"_components": [
{
"__id__": 15
"__id__": 17
}
],
"_prefab": null,
@@ -810,7 +909,7 @@
"_name": "",
"_objFlags": 0,
"node": {
"__id__": 14
"__id__": 16
},
"_enabled": true,
"_materials": [
@@ -840,13 +939,13 @@
"_name": "PLACEHOLDER_LABEL",
"_objFlags": 0,
"_parent": {
"__id__": 11
"__id__": 13
},
"_children": [],
"_active": false,
"_components": [
{
"__id__": 17
"__id__": 19
}
],
"_prefab": null,
@@ -902,7 +1001,7 @@
"_name": "",
"_objFlags": 0,
"node": {
"__id__": 16
"__id__": 18
},
"_enabled": true,
"_materials": [],
@@ -928,7 +1027,7 @@
"_name": "",
"_objFlags": 0,
"node": {
"__id__": 11
"__id__": 13
},
"_enabled": true,
"_useOriginalSize": false,
@@ -941,13 +1040,13 @@
"editingDidEnded": [],
"editingReturn": [],
"_N$textLabel": {
"__id__": 15
},
"_N$placeholderLabel": {
"__id__": 17
},
"_N$placeholderLabel": {
"__id__": 19
},
"_N$background": {
"__id__": 13
"__id__": 15
},
"_N$inputFlag": 5,
"_N$inputMode": 3,
@@ -959,13 +1058,13 @@
"_name": "phoneLabel",
"_objFlags": 0,
"_parent": {
"__id__": 10
"__id__": 12
},
"_children": [],
"_active": true,
"_components": [
{
"__id__": 20
"__id__": 22
}
],
"_prefab": null,
@@ -1021,7 +1120,7 @@
"_name": "",
"_objFlags": 0,
"node": {
"__id__": 19
"__id__": 21
},
"_enabled": true,
"_materials": [
@@ -1051,23 +1150,23 @@
"_name": "phoneNumberInput",
"_objFlags": 0,
"_parent": {
"__id__": 10
"__id__": 12
},
"_children": [
{
"__id__": 22
},
{
"__id__": 24
},
{
"__id__": 26
},
{
"__id__": 28
}
],
"_active": true,
"_components": [
{
"__id__": 29
"__id__": 31
}
],
"_prefab": null,
@@ -1123,13 +1222,13 @@
"_name": "BACKGROUND_SPRITE",
"_objFlags": 0,
"_parent": {
"__id__": 21
"__id__": 23
},
"_children": [],
"_active": true,
"_components": [
{
"__id__": 23
"__id__": 25
}
],
"_prefab": null,
@@ -1185,7 +1284,7 @@
"_name": "",
"_objFlags": 0,
"node": {
"__id__": 22
"__id__": 24
},
"_enabled": true,
"_materials": [
@@ -1217,13 +1316,13 @@
"_name": "TEXT_LABEL",
"_objFlags": 0,
"_parent": {
"__id__": 21
"__id__": 23
},
"_children": [],
"_active": false,
"_components": [
{
"__id__": 25
"__id__": 27
}
],
"_prefab": null,
@@ -1279,7 +1378,7 @@
"_name": "",
"_objFlags": 0,
"node": {
"__id__": 24
"__id__": 26
},
"_enabled": true,
"_materials": [],
@@ -1305,16 +1404,16 @@
"_name": "PLACEHOLDER_LABEL",
"_objFlags": 0,
"_parent": {
"__id__": 21
"__id__": 23
},
"_children": [],
"_active": true,
"_components": [
{
"__id__": 27
"__id__": 29
},
{
"__id__": 28
"__id__": 30
}
],
"_prefab": null,
@@ -1370,7 +1469,7 @@
"_name": "",
"_objFlags": 0,
"node": {
"__id__": 26
"__id__": 28
},
"_enabled": true,
"_materials": [
@@ -1400,7 +1499,7 @@
"_name": "",
"_objFlags": 0,
"node": {
"__id__": 26
"__id__": 28
},
"_enabled": true,
"_dataID": "login.hint.phoneInputHint",
@@ -1411,7 +1510,7 @@
"_name": "",
"_objFlags": 0,
"node": {
"__id__": 21
"__id__": 23
},
"_enabled": true,
"_useOriginalSize": false,
@@ -1424,13 +1523,13 @@
"editingDidEnded": [],
"editingReturn": [],
"_N$textLabel": {
"__id__": 25
},
"_N$placeholderLabel": {
"__id__": 27
},
"_N$placeholderLabel": {
"__id__": 29
},
"_N$background": {
"__id__": 23
"__id__": 25
},
"_N$inputFlag": 5,
"_N$inputMode": 1,
@@ -1442,13 +1541,13 @@
"_name": " smsLoginCaptchaLabel",
"_objFlags": 0,
"_parent": {
"__id__": 10
"__id__": 12
},
"_children": [],
"_active": true,
"_components": [
{
"__id__": 31
"__id__": 33
}
],
"_prefab": null,
@@ -1504,7 +1603,7 @@
"_name": "",
"_objFlags": 0,
"node": {
"__id__": 30
"__id__": 32
},
"_enabled": true,
"_materials": [
@@ -1534,23 +1633,23 @@
"_name": "smsLoginCaptchaInput",
"_objFlags": 0,
"_parent": {
"__id__": 10
"__id__": 12
},
"_children": [
{
"__id__": 33
},
{
"__id__": 35
},
{
"__id__": 37
},
{
"__id__": 39
}
],
"_active": true,
"_components": [
{
"__id__": 40
"__id__": 42
}
],
"_prefab": null,
@@ -1606,13 +1705,13 @@
"_name": "BACKGROUND_SPRITE",
"_objFlags": 0,
"_parent": {
"__id__": 32
"__id__": 34
},
"_children": [],
"_active": true,
"_components": [
{
"__id__": 34
"__id__": 36
}
],
"_prefab": null,
@@ -1668,7 +1767,7 @@
"_name": "",
"_objFlags": 0,
"node": {
"__id__": 33
"__id__": 35
},
"_enabled": true,
"_materials": [
@@ -1700,13 +1799,13 @@
"_name": "TEXT_LABEL",
"_objFlags": 0,
"_parent": {
"__id__": 32
"__id__": 34
},
"_children": [],
"_active": false,
"_components": [
{
"__id__": 36
"__id__": 38
}
],
"_prefab": null,
@@ -1762,7 +1861,7 @@
"_name": "",
"_objFlags": 0,
"node": {
"__id__": 35
"__id__": 37
},
"_enabled": true,
"_materials": [],
@@ -1788,16 +1887,16 @@
"_name": "PLACEHOLDER_LABEL",
"_objFlags": 0,
"_parent": {
"__id__": 32
"__id__": 34
},
"_children": [],
"_active": true,
"_components": [
{
"__id__": 38
"__id__": 40
},
{
"__id__": 39
"__id__": 41
}
],
"_prefab": null,
@@ -1853,7 +1952,7 @@
"_name": "",
"_objFlags": 0,
"node": {
"__id__": 37
"__id__": 39
},
"_enabled": true,
"_materials": [
@@ -1883,7 +1982,7 @@
"_name": "",
"_objFlags": 0,
"node": {
"__id__": 37
"__id__": 39
},
"_enabled": true,
"_dataID": "login.hint.captchaInputHint",
@@ -1894,7 +1993,7 @@
"_name": "",
"_objFlags": 0,
"node": {
"__id__": 32
"__id__": 34
},
"_enabled": true,
"_useOriginalSize": false,
@@ -1907,13 +2006,13 @@
"editingDidEnded": [],
"editingReturn": [],
"_N$textLabel": {
"__id__": 36
},
"_N$placeholderLabel": {
"__id__": 38
},
"_N$placeholderLabel": {
"__id__": 40
},
"_N$background": {
"__id__": 34
"__id__": 36
},
"_N$inputFlag": 5,
"_N$inputMode": 2,
@@ -1925,20 +2024,20 @@
"_name": "smsLoginCaptchaButton",
"_objFlags": 0,
"_parent": {
"__id__": 10
"__id__": 12
},
"_children": [
{
"__id__": 42
"__id__": 44
},
{
"__id__": 44
"__id__": 46
}
],
"_active": true,
"_components": [
{
"__id__": 46
"__id__": 48
}
],
"_prefab": null,
@@ -1994,13 +2093,13 @@
"_name": "smsGetCaptcha",
"_objFlags": 0,
"_parent": {
"__id__": 41
"__id__": 43
},
"_children": [],
"_active": true,
"_components": [
{
"__id__": 43
"__id__": 45
}
],
"_prefab": null,
@@ -2056,7 +2155,7 @@
"_name": "",
"_objFlags": 0,
"node": {
"__id__": 42
"__id__": 44
},
"_enabled": true,
"_materials": [
@@ -2090,13 +2189,13 @@
"_name": "captchaLabel",
"_objFlags": 0,
"_parent": {
"__id__": 41
"__id__": 43
},
"_children": [],
"_active": true,
"_components": [
{
"__id__": 45
"__id__": 47
}
],
"_prefab": null,
@@ -2152,7 +2251,7 @@
"_name": "",
"_objFlags": 0,
"node": {
"__id__": 44
"__id__": 46
},
"_enabled": true,
"_materials": [
@@ -2182,7 +2281,7 @@
"_name": "",
"_objFlags": 0,
"node": {
"__id__": 41
"__id__": 43
},
"_enabled": true,
"_normalMaterial": null,
@@ -2191,7 +2290,7 @@
"zoomScale": 1.1,
"clickEvents": [
{
"__id__": 47
"__id__": 49
}
],
"_N$interactable": true,
@@ -2257,7 +2356,7 @@
"__uuid__": "29158224-f8dd-4661-a796-1ffab537140e"
},
"_N$target": {
"__id__": 41
"__id__": 43
},
"_id": "96L9tbxW9DHJHU5hUi9oNc"
},
@@ -2276,20 +2375,20 @@
"_name": "loginButton",
"_objFlags": 0,
"_parent": {
"__id__": 10
"__id__": 12
},
"_children": [
{
"__id__": 49
"__id__": 51
}
],
"_active": true,
"_components": [
{
"__id__": 51
"__id__": 53
},
{
"__id__": 52
"__id__": 54
}
],
"_prefab": null,
@@ -2345,13 +2444,13 @@
"_name": "loginLabel",
"_objFlags": 0,
"_parent": {
"__id__": 48
"__id__": 50
},
"_children": [],
"_active": true,
"_components": [
{
"__id__": 50
"__id__": 52
}
],
"_prefab": null,
@@ -2407,7 +2506,7 @@
"_name": "",
"_objFlags": 0,
"node": {
"__id__": 49
"__id__": 51
},
"_enabled": true,
"_materials": [
@@ -2437,7 +2536,7 @@
"_name": "",
"_objFlags": 0,
"node": {
"__id__": 48
"__id__": 50
},
"_enabled": true,
"_materials": [
@@ -2471,7 +2570,7 @@
"_name": "",
"_objFlags": 0,
"node": {
"__id__": 48
"__id__": 50
},
"_enabled": true,
"_normalMaterial": null,
@@ -2480,7 +2579,7 @@
"zoomScale": 1.2,
"clickEvents": [
{
"__id__": 53
"__id__": 55
}
],
"_N$interactable": true,
@@ -2546,7 +2645,7 @@
"__uuid__": "29158224-f8dd-4661-a796-1ffab537140e"
},
"_N$target": {
"__id__": 48
"__id__": 50
},
"_id": "0dQu3M5+hO8Ia4FD67Br39"
},
@@ -2565,13 +2664,13 @@
"_name": "phoneNumberTips",
"_objFlags": 0,
"_parent": {
"__id__": 10
"__id__": 12
},
"_children": [],
"_active": true,
"_components": [
{
"__id__": 55
"__id__": 57
}
],
"_prefab": null,
@@ -2627,7 +2726,7 @@
"_name": "",
"_objFlags": 0,
"node": {
"__id__": 54
"__id__": 56
},
"_enabled": true,
"_materials": [
@@ -2657,13 +2756,13 @@
"_name": "captchaTips",
"_objFlags": 0,
"_parent": {
"__id__": 10
"__id__": 12
},
"_children": [],
"_active": true,
"_components": [
{
"__id__": 57
"__id__": 59
}
],
"_prefab": null,
@@ -2719,7 +2818,7 @@
"_name": "",
"_objFlags": 0,
"node": {
"__id__": 56
"__id__": 58
},
"_enabled": true,
"_materials": [
@@ -2749,7 +2848,7 @@
"_name": "",
"_objFlags": 0,
"node": {
"__id__": 10
"__id__": 12
},
"_enabled": true,
"alignMode": 1,
@@ -2776,7 +2875,7 @@
"_name": "",
"_objFlags": 0,
"node": {
"__id__": 9
"__id__": 11
},
"_enabled": true,
"alignMode": 0,
@@ -2854,37 +2953,37 @@
"__id__": 2
},
"backgroundNode": {
"__id__": 9
},
"interactiveControls": {
"__id__": 10
},
"phoneLabel": {
"__id__": 19
},
"smsLoginCaptchaLabel": {
"__id__": 30
},
"phoneCountryCodeInput": {
"__id__": 11
},
"phoneNumberInput": {
"interactiveControls": {
"__id__": 12
},
"phoneLabel": {
"__id__": 21
},
"phoneNumberTips": {
"__id__": 54
},
"smsLoginCaptchaInput": {
"smsLoginCaptchaLabel": {
"__id__": 32
},
"smsLoginCaptchaButton": {
"__id__": 41
"phoneCountryCodeInput": {
"__id__": 13
},
"captchaTips": {
"phoneNumberInput": {
"__id__": 23
},
"phoneNumberTips": {
"__id__": 56
},
"smsLoginCaptchaInput": {
"__id__": 34
},
"smsLoginCaptchaButton": {
"__id__": 43
},
"captchaTips": {
"__id__": 58
},
"loginButton": {
"__id__": 48
"__id__": 50
},
"smsWaitCountdownPrefab": {
"__uuid__": "2c0101b8-c15a-4501-9fce-cd5a014af8bf"

File diff suppressed because it is too large Load Diff

View File

@@ -75,10 +75,10 @@ cc.Class({
onLoad() {
BaseCharacter.prototype.onLoad.call(this);
this.effAnimNode = this.animNode.getChildByName(this.speciesName);
this.animComp = this.effAnimNode.getComponent(dragonBones.ArmatureDisplay);
if (!this.animComp) {
this.animComp = this.effAnimNode.getComponent(cc.Animation);
}
//this.animComp = this.effAnimNode.getComponent(dragonBones.ArmatureDisplay);
//if (!this.animComp) {
this.animComp = this.effAnimNode.getComponent(cc.Animation);
//}
this.effAnimNode.active = true;
},
@@ -107,13 +107,13 @@ cc.Class({
let playingAnimName = null;
let underlyingAnimationCtrl = null;
if (this.animComp instanceof dragonBones.ArmatureDisplay) {
underlyingAnimationCtrl = this.animComp._armature.animation; // ALWAYS use the dragonBones api instead of ccc's wrapper!
playingAnimName = underlyingAnimationCtrl.lastAnimationName;
} else {
underlyingAnimationCtrl = this.animComp.currentClip;
playingAnimName = (!underlyingAnimationCtrl ? null : underlyingAnimationCtrl.name);
}
//if (this.animComp instanceof dragonBones.ArmatureDisplay) {
// underlyingAnimationCtrl = this.animComp._armature.animation; // ALWAYS use the dragonBones api instead of ccc's wrapper!
// playingAnimName = underlyingAnimationCtrl.lastAnimationName;
//} else {
underlyingAnimationCtrl = this.animComp.currentClip;
playingAnimName = (!underlyingAnimationCtrl ? null : underlyingAnimationCtrl.name);
//}
// It turns out that "prevRdfPlayer.CharacterState" is not useful in this function :)
if (newAnimName == playingAnimName && window.ATK_CHARACTER_STATE_INTERRUPT_WAIVE_SET.has(newCharacterState)) {
@@ -122,11 +122,11 @@ cc.Class({
return;
}
if (this.animComp instanceof dragonBones.ArmatureDisplay) {
this._interruptPlayingAnimAndPlayNewAnimDragonBones(rdfPlayer, prevRdfPlayer, newCharacterState, newAnimName, underlyingAnimationCtrl, playingAnimName, chConfig);
} else {
this._interruptPlayingAnimAndPlayNewAnimFrameAnim(rdfPlayer, prevRdfPlayer, newCharacterState, newAnimName, playingAnimName, chConfig);
}
//if (this.animComp instanceof dragonBones.ArmatureDisplay) {
// this._interruptPlayingAnimAndPlayNewAnimDragonBones(rdfPlayer, prevRdfPlayer, newCharacterState, newAnimName, underlyingAnimationCtrl, playingAnimName, chConfig);
//} else {
this._interruptPlayingAnimAndPlayNewAnimFrameAnim(rdfPlayer, prevRdfPlayer, newCharacterState, newAnimName, playingAnimName, chConfig);
//}
},
_interruptPlayingAnimAndPlayNewAnimDragonBones(rdfPlayer, prevRdfPlayer, newCharacterState, newAnimName, underlyingAnimationCtrl, playingAnimName, chConfig) {

View File

@@ -23,7 +23,8 @@ cc.Class({
// LIFE-CYCLE CALLBACKS:
onLoad() {},
init() {
init(mapIns) {
this.mapIns = mapIns;
if (null != this.firstPlayerInfoNode) {
this.firstPlayerInfoNode.active = false;
}
@@ -53,9 +54,10 @@ cc.Class({
},
exitBtnOnClick(evt) {
window.clearBoundRoomIdInBothVolatileAndPersistentStorage();
window.closeWSConnection();
cc.director.loadScene('login');
this.mapIns.hideFindingPlayersGUI();
cc.log(`FindingPlayers.exitBtnOnClick`);
window.closeWSConnection(constants.RET_CODE.BATTLE_STOPPED, "");
window.clearLocalStorageAndBackToLoginScene(false);
},
updatePlayersInfo(playerMetas) {

View File

@@ -2,6 +2,7 @@ const i18n = require('LanguageData');
i18n.init(window.language); // languageID should be equal to the one we input in New Language ID input field
const RingBuffer = require('./RingBuffer');
const NetworkDoctor = require('./NetworkDoctor');
const PriorityQueue = require("./PriorityQueue");
window.ALL_MAP_STATES = {
@@ -33,6 +34,23 @@ window.PlayerBattleState = {
EXPELLED_IN_DISMISSAL: 6
};
window.onUdpMessage = (args) => {
const self = window.mapIns;
const ui8Arr = args;
//cc.log(`#1 Js called back by CPP: onUdpMessage: args=${args}, typeof(args)=${typeof (args)}, argslen=${args.length}, ui8Arr=${ui8Arr}`);
const req = window.pb.protos.WsReq.decode(ui8Arr);
if (req) {
//cc.log(`#2 Js called back by CPP for upsync: onUdpMessage: ${JSON.stringify(req)}`);
if (req.act && window.UPSYNC_MSG_ACT_PLAYER_CMD == req.act) {
let effCnt = 0;
const renderedInputFrameIdUpper = gopkgs.ConvertToDelayedInputFrameId(self.renderFrameId);
const peerJoinIndex = req.joinIndex;
const batch = req.inputFrameUpsyncBatch;
self.onPeerInputFrameUpsync(peerJoinIndex, batch);
}
}
};
cc.Class({
extends: cc.Component,
@@ -96,16 +114,26 @@ cc.Class({
type: cc.Integer,
default: 4 // implies (renderFrameIdLagTolerance >> inputScaleFrames) count of inputFrameIds
},
jigglingEps1D: {
type: cc.Float,
default: 1e-3
sendingQLabel: {
type: cc.Label,
default: null
},
bulletTriggerEnabled: {
default: false
inputFrameDownsyncQLabel: {
type: cc.Label,
default: null
},
closeOnForcedtoResyncNotSelf: {
default: true
peerInputFrameUpsyncQLabel: {
type: cc.Label,
default: null
},
rollbackFramesLabel: {
type: cc.Label,
default: null
},
skippedRenderFrameCntLabel: {
type: cc.Label,
default: null
}
},
_inputFrameIdDebuggable(inputFrameId) {
@@ -184,6 +212,7 @@ cc.Class({
// Upon resync, "self.lastUpsyncInputFrameId" might not have been updated properly.
batchInputFrameIdSt = self.recentInputCache.StFrameId;
}
self.networkDoctor.logSending(batchInputFrameIdSt, latestLocalInputFrameId);
for (let i = batchInputFrameIdSt; i <= latestLocalInputFrameId; ++i) {
const inputFrameDownsync = self.recentInputCache.GetByFrameId(i);
if (null == inputFrameDownsync) {
@@ -205,7 +234,11 @@ cc.Class({
joinIndex: self.selfPlayerInfo.JoinIndex,
ackingInputFrameId: self.lastAllConfirmedInputFrameId,
inputFrameUpsyncBatch: inputFrameUpsyncBatch,
authKey: self.selfPlayerInfo.udpTunnelAuthKey,
}).finish();
if (cc.sys.isNative) {
DelayNoMore.UdpSession.broadcastInputFrameUpsync(reqData, window.boundRoomCapacity, self.selfPlayerInfo.JoinIndex);
}
window.sendSafely(reqData);
self.lastUpsyncInputFrameId = latestLocalInputFrameId;
if (self.lastUpsyncInputFrameId >= self.recentInputCache.EdFrameId) {
@@ -324,7 +357,6 @@ cc.Class({
self.recentRenderCache = new RingBuffer(self.renderCacheSize);
self.selfPlayerInfo = null; // This field is kept for distinguishing "self" and "others".
self.recentInputCache = gopkgs.NewRingBufferJs((self.renderCacheSize >> 1) + 1);
self.gopkgsCollisionSys = gopkgs.NewCollisionSpaceJs((self.spaceOffsetX << 1), (self.spaceOffsetY << 1), self.collisionMinStep, self.collisionMinStep);
@@ -342,14 +374,13 @@ cc.Class({
self.othersForcedDownsyncRenderFrameDict = new Map();
self.rdfIdToActuallyUsedInput = new Map();
self.networkDoctor = new NetworkDoctor(20);
self.skipRenderFrameFlag = false;
self.countdownNanos = null;
if (self.countdownLabel) {
self.countdownLabel.string = "";
}
if (self.findingPlayerNode) {
const findingPlayerScriptIns = self.findingPlayerNode.getComponent("FindingPlayer");
findingPlayerScriptIns.init();
}
if (self.playersInfoNode) {
safelyAddChild(self.widgetsAboveAllNode, self.playersInfoNode);
}
@@ -409,6 +440,7 @@ cc.Class({
},
onLoad() {
cc.game.setFrameRate(60);
cc.view.setOrientation(cc.macro.ORIENTATION_LANDSCAPE);
cc.view.enableAutoFullScreen(true);
@@ -417,6 +449,7 @@ cc.Class({
window.forceBigEndianFloatingNumDecoding = self.forceBigEndianFloatingNumDecoding;
self.showCriticalCoordinateLabels = false;
self.showNetworkDoctorInfo = true;
console.warn("+++++++ Map onLoad()");
@@ -454,7 +487,7 @@ cc.Class({
self.findingPlayerNode.width = self.canvasNode.width;
self.findingPlayerNode.height = self.canvasNode.height;
const findingPlayerScriptIns = self.findingPlayerNode.getComponent("FindingPlayer");
findingPlayerScriptIns.init();
findingPlayerScriptIns.init(self);
self.playersInfoNode = cc.instantiate(self.playersInfoPrefab);
@@ -476,6 +509,7 @@ cc.Class({
console.log(`Received parsedBattleColliderInfo via ws`);
// TODO: Upon reconnection, the backend might have already been sending down data that'd trigger "onRoomDownsyncFrame & onInputFrameDownsyncBatch", but frontend could reject those data due to "battleState != PlayerBattleState.ACTIVE".
Object.assign(self, parsedBattleColliderInfo);
self.inputFrameUpsyncDelayTolerance = parsedBattleColliderInfo.inputFrameUpsyncDelayTolerance;
const tiledMapIns = self.node.getComponent(cc.TiledMap);
@@ -483,7 +517,7 @@ cc.Class({
const fullPathOfTmxFile = cc.js.formatStr("map/%s/map", parsedBattleColliderInfo.stageName);
cc.loader.loadRes(fullPathOfTmxFile, cc.TiledMapAsset, (err, tmxAsset) => {
if (null != err) {
console.error(err);
console.error(`Error occurred when loading tiled stage ${parsedBattleColliderInfo.stageName}`, err);
return;
}
@@ -532,10 +566,6 @@ cc.Class({
const collisionBarrierIndex = (self.collisionBarrierIndexPrefix + barrierIdCounter);
self.gopkgsCollisionSysMap[collisionBarrierIndex] = newBarrierCollider;
}
self.selfPlayerInfo = JSON.parse(cc.sys.localStorage.getItem('selfPlayer'));
Object.assign(self.selfPlayerInfo, {
Id: self.selfPlayerInfo.playerId
});
self.initDebugDrawers();
const reqData = window.pb.protos.WsReq.encode({
msgId: Date.now(),
@@ -690,6 +720,7 @@ cc.Class({
self.lastRenderFrameIdTriggeredAt = performance.now();
// In this case it must be true that "rdf.id > chaserRenderFrameId".
self.chaserRenderFrameId = rdf.Id;
self.networkDoctor.logRollbackFrames(0);
const canvasNode = self.canvasNode;
self.ctrl = canvasNode.getComponent("TouchEventsManager");
@@ -796,6 +827,7 @@ cc.Class({
return;
}
self.networkDoctor.logInputFrameDownsync(batch[0].inputFrameId, batch[batch.length - 1].inputFrameId);
let firstPredictedYetIncorrectInputFrameId = null;
for (let k in batch) {
const inputFrameDownsync = batch[k];
@@ -844,6 +876,7 @@ lastAllConfirmedInputFrameId=${self.lastAllConfirmedInputFrameId}
recentInputCache=${self._stringifyRecentInputCache(false)}
batchInputFrameIdRange=[${batch[0].inputFrameId}, ${batch[batch.length - 1].inputFrameId}]`);
self.chaserRenderFrameId = renderFrameId1;
self.networkDoctor.logRollbackFrames(self.renderFrameId - self.chaserRenderFrameId);
},
onPeerInputFrameUpsync(peerJoinIndex, batch /* []*pb.InputFrameDownsync */ ) {
@@ -856,21 +889,41 @@ batchInputFrameIdRange=[${batch[0].inputFrameId}, ${batch[batch.length - 1].inpu
if (!self.recentInputCache) {
return;
}
if (ALL_BATTLE_STATES.IN_SETTLEMENT == self.battleState) {
if (ALL_BATTLE_STATES.IN_BATTLE != self.battleState) {
return;
}
let effCnt = 0;
//console.log(`Received peer inputFrameUpsync batch w/ inputFrameId in [${batch[0].inputFrameId}, ${batch[batch.length - 1].inputFrameId}] for prediction assistance`);
const renderedInputFrameIdUpper = gopkgs.ConvertToDelayedInputFrameId(self.renderFrameId);
for (let k in batch) {
const inputFrameDownsync = batch[k];
const inputFrameDownsyncId = inputFrameDownsync.inputFrameId;
if (inputFrameDownsyncId < renderedInputFrameIdUpper) {
// Avoid obfuscating already rendered history
continue;
}
if (inputFrameDownsyncId <= self.lastAllConfirmedInputFrameId) {
continue;
}
self.getOrPrefabInputFrameUpsync(inputFrameDownsyncId); // Make sure that inputFrame exists locally
const existingInputFrame = self.recentInputCache.GetByFrameId(inputFrameDownsyncId);
existingInputFrame.InputList[peerJoinIndex - 1] = inputFrameDownsync.inputList[peerJoinIndex - 1]; // No need to change "confirmedList", leave it to "onInputFrameDownsyncBatch" -- we're just helping prediction here
self.recentInputCache.SetByFrameId(existingInputFrame, inputFrameDownsyncId);
if (0 < (existingInputFrame.ConfirmedList & (1 << (peerJoinIndex - 1)))) {
continue;
}
effCnt += 1;
// the returned "gopkgs.NewInputFrameDownsync.InputList" is immutable, thus we can only modify the values in "newInputList" and "newConfirmedList"!
let newInputList = new Array(self.playerRichInfoDict.size).fill(0);
for (let i in existingInputFrame.InputList) {
newInputList[i] = existingInputFrame.InputList[i];
}
let newConfirmedList = (existingInputFrame.confirmedList | (1 << (peerJoinIndex - 1)));
// No need to change "lastAllConfirmedInputFrameId", leave it to "onInputFrameDownsyncBatch" -- we're just helping prediction here
const newInputFrameDownsyncLocal = gopkgs.NewInputFrameDownsync(inputFrameDownsyncId, newInputList, newConfirmedList);
self.recentInputCache.SetByFrameId(newInputFrameDownsyncLocal, inputFrameDownsyncId);
}
if (0 < effCnt) {
self.networkDoctor.logPeerInputFrameUpsync(batch[0].inputFrameId, batch[batch.length - 1].inputFrameId);
}
},
@@ -890,7 +943,7 @@ batchInputFrameIdRange=[${batch[0].inputFrameId}, ${batch[batch.length - 1].inpu
return;
}
self._stringifyRdfIdToActuallyUsedInput();
window.closeWSConnection(constants.RET_CODE.BATTLE_STOPPED);
window.closeWSConnection(constants.RET_CODE.BATTLE_STOPPED, "");
self.battleState = ALL_BATTLE_STATES.IN_SETTLEMENT;
self.countdownNanos = null;
if (self.musicEffectManagerScriptIns) {
@@ -945,6 +998,11 @@ batchInputFrameIdRange=[${batch[0].inputFrameId}, ${batch[batch.length - 1].inpu
Kindly note that Significantly different network bandwidths or delay fluctuations would result in frequent [type#1 forceConfirmation] too, but CAUSE FROM DIFFERENT LOCAL "update(dt)" RATE SHOULD BE THE FIRST TO INVESTIGATE AND ELIMINATE -- because we have control on it, but no one has control on the internet.
*/
if (self.skipRenderFrameFlag) {
self.networkDoctor.logSkippedRenderFrameCnt();
self.skipRenderFrameFlag = false;
return;
}
try {
let st = performance.now();
const noDelayInputFrameId = gopkgs.ConvertToNoDelayInputFrameId(self.renderFrameId);
@@ -981,6 +1039,7 @@ batchInputFrameIdRange=[${batch[0].inputFrameId}, ${batch[batch.length - 1].inpu
// Inside the following "self.rollbackAndChase" actually ROLLS FORWARD w.r.t. the corresponding delayedInputFrame, REGARDLESS OF whether or not "self.chaserRenderFrameId == self.renderFrameId" now.
const latestRdfResults = self.rollbackAndChase(self.renderFrameId, self.renderFrameId + 1, self.gopkgsCollisionSys, self.gopkgsCollisionSysMap, false);
self.networkDoctor.logRollbackFrames(self.renderFrameId - self.chaserRenderFrameId);
let prevRdf = latestRdfResults[0],
rdf = latestRdfResults[1];
/*
@@ -1005,9 +1064,13 @@ othersForcedDownsyncRenderFrame=${JSON.stringify(othersForcedDownsyncRenderFrame
}
self.applyRoomDownsyncFrameDynamics(rdf, prevRdf);
self.showDebugBoundaries(rdf);
if (self.showNetworkDoctorInfo) {
self.showNetworkDoctorLabels();
}
++self.renderFrameId; // [WARNING] It's important to increment the renderFrameId AFTER all the operations above!!!
self.lastRenderFrameIdTriggeredAt = performance.now();
let t3 = performance.now();
self.skipRenderFrameFlag = self.networkDoctor.isTooFast();
} catch (err) {
console.error("Error during Map.update", err);
self.onBattleStopped(); // TODO: Popup to ask player to refresh browser
@@ -1031,6 +1094,7 @@ othersForcedDownsyncRenderFrame=${JSON.stringify(othersForcedDownsyncRenderFrame
logout(byClick /* The case where this param is "true" will be triggered within `ConfirmLogout.js`.*/ , shouldRetainBoundRoomIdInBothVolatileAndPersistentStorage) {
const self = this;
const localClearance = () => {
window.closeWSConnection(constants.RET_CODE.BATTLE_STOPPED, "");
window.clearLocalStorageAndBackToLoginScene(shouldRetainBoundRoomIdInBothVolatileAndPersistentStorage);
}
@@ -1233,24 +1297,6 @@ othersForcedDownsyncRenderFrame=${JSON.stringify(othersForcedDownsyncRenderFrame
}
const j = gopkgs.ConvertToDelayedInputFrameId(i);
const delayedInputFrame = self.recentInputCache.GetByFrameId(j);
/*
const prevJ = gopkgs.ConvertToDelayedInputFrameId(i - 1);
const prevDelayedInputFrame = self.recentInputCache.GetByFrameId(prevJ);
const prevBtnALevel = (null == prevDelayedInputFrame ? 0 : ((prevDelayedInputFrame.InputList[self.selfPlayerInfo.JoinIndex - 1] >> 4) & 1));
const btnALevel = ((delayedInputFrame.InputList[self.selfPlayerInfo.JoinIndex - 1] >> 4) & 1);
if (
ATK_CHARACTER_STATE.Atk1[0] == currRdf.PlayersArr[self.selfPlayerInfo.JoinIndex - 1].CharacterState
||
ATK_CHARACTER_STATE.Atk2[0] == currRdf.PlayersArr[self.selfPlayerInfo.JoinIndex - 1].CharacterState
) {
console.log(`rdf.Id=${i}, (btnALevel,j)=(${btnALevel},${j}), (prevBtnALevel,prevJ) is (${prevBtnALevel},${prevJ}), in cancellable atk!`);
}
if (btnALevel > 0) {
if (btnALevel > prevBtnALevel) {
console.log(`rdf.Id=${i}, rising edge of btnA triggered`);
}
}
*/
if (self.frameDataLoggingEnabled) {
const actuallyUsedInputClone = delayedInputFrame.InputList.slice();
@@ -1296,7 +1342,7 @@ othersForcedDownsyncRenderFrame=${JSON.stringify(othersForcedDownsyncRenderFrame
const selfPlayerId = self.selfPlayerInfo.Id;
if (selfPlayerId == playerId) {
self.selfPlayerInfo.JoinIndex = immediatePlayerInfo.JoinIndex;
self.selfPlayerInfo.JoinIndex = immediatePlayerInfo.JoinIndex; // Update here in case of any change during WAITING phase
nodeAndScriptIns[1].showArrowTipNode();
}
}
@@ -1499,4 +1545,44 @@ actuallyUsedinputList:{${self.inputFrameDownsyncStr(actuallyUsedInputClone)}}`);
}
}
},
showNetworkDoctorLabels() {
const self = this;
const [sendingFps, srvDownsyncFps, peerUpsyncFps, rollbackFrames, skippedRenderFrameCnt] = self.networkDoctor.stats();
if (self.sendingQLabel) {
self.sendingQLabel.string = `${sendingFps} fps sending`;
if (sendingFps < self.networkDoctor.inputRateThreshold) {
self.sendingQLabel.node.color = cc.Color.RED;
} else {
self.sendingQLabel.node.color = cc.Color.WHITE;
}
}
if (self.inputFrameDownsyncQLabel) {
self.inputFrameDownsyncQLabel.string = `${srvDownsyncFps} fps srv-downsync`;
if (srvDownsyncFps < self.networkDoctor.inputRateThreshold) {
self.inputFrameDownsyncQLabel.node.color = cc.Color.RED;
} else {
self.inputFrameDownsyncQLabel.node.color = cc.Color.WHITE;
}
}
if (self.peerInputFrameUpsyncQLabel) {
self.peerInputFrameUpsyncQLabel.string = `${peerUpsyncFps} fps peer-upsync`;
if (peerUpsyncFps > self.networkDoctor.peerUpsyncFps) {
self.peerInputFrameUpsyncQLabel.node.color = cc.Color.RED;
} else {
self.peerInputFrameUpsyncQLabel.node.color = cc.Color.WHITE;
}
}
if (self.rollbackFramesLabel) {
self.rollbackFramesLabel.string = `rollbackFrames: ${rollbackFrames}`
if (rollbackFrames > self.networkDoctor.rollbackFramesThreshold) {
self.rollbackFramesLabel.node.color = cc.Color.RED;
} else {
self.rollbackFramesLabel.node.color = cc.Color.WHITE;
}
}
if (self.skippedRenderFrameCntLabel) {
self.skippedRenderFrameCntLabel.string = `${skippedRenderFrameCnt} frames skipped`
}
},
});

View File

@@ -0,0 +1,98 @@
const RingBuffer = require('./RingBuffer');
var NetworkDoctor = function(capacity) {
this.reset(capacity);
};
NetworkDoctor.prototype.reset = function(capacity) {
this.sendingQ = new RingBuffer(capacity);
this.inputFrameDownsyncQ = new RingBuffer(capacity);
this.peerInputFrameUpsyncQ = new RingBuffer(capacity);
this.peerInputFrameUpsyncCnt = 0;
this.immediateRollbackFrames = 0;
this.skippedRenderFrameCnt = 0;
this.inputRateThreshold = gopkgs.ConvertToNoDelayInputFrameId(60);
this.peerUpsyncThreshold = 8;
this.rollbackFramesThreshold = 8; // Roughly the minimum "TurnAroundFramesToRecover".
};
NetworkDoctor.prototype.logSending = function(stFrameId, edFrameId) {
this.sendingQ.put({
i: stFrameId,
j: edFrameId,
t: Date.now()
});
};
NetworkDoctor.prototype.logInputFrameDownsync = function(stFrameId, edFrameId) {
this.inputFrameDownsyncQ.put({
i: stFrameId,
j: edFrameId,
t: Date.now()
});
};
NetworkDoctor.prototype.logPeerInputFrameUpsync = function(stFrameId, edFrameId) {
const firstPopped = this.peerInputFrameUpsyncQ.put({
i: stFrameId,
j: edFrameId,
t: Date.now()
});
if (null != firstPopped) {
this.peerInputFrameUpsyncCnt -= (firstPopped.j - firstPopped.i + 1);
}
this.peerInputFrameUpsyncCnt += (edFrameId - stFrameId + 1);
};
NetworkDoctor.prototype.logRollbackFrames = function(x) {
this.immediateRollbackFrames = x;
};
NetworkDoctor.prototype.stats = function() {
let sendingFps = 0,
srvDownsyncFps = 0,
peerUpsyncFps = 0,
rollbackFrames = this.immediateRollbackFrames;
if (1 < this.sendingQ.cnt) {
const st = this.sendingQ.getByFrameId(this.sendingQ.stFrameId);
const ed = this.sendingQ.getByFrameId(this.sendingQ.edFrameId - 1);
const elapsedMillis = ed.t - st.t;
sendingFps = Math.round((ed.j - st.i) * 1000 / elapsedMillis);
}
if (1 < this.inputFrameDownsyncQ.cnt) {
const st = this.inputFrameDownsyncQ.getByFrameId(this.inputFrameDownsyncQ.stFrameId);
const ed = this.inputFrameDownsyncQ.getByFrameId(this.inputFrameDownsyncQ.edFrameId - 1);
const elapsedMillis = ed.t - st.t;
srvDownsyncFps = Math.round((ed.j - st.i) * 1000 / elapsedMillis);
}
if (1 < this.peerInputFrameUpsyncQ.cnt) {
const st = this.peerInputFrameUpsyncQ.getByFrameId(this.peerInputFrameUpsyncQ.stFrameId);
const ed = this.peerInputFrameUpsyncQ.getByFrameId(this.peerInputFrameUpsyncQ.edFrameId - 1);
const elapsedMillis = ed.t - st.t;
peerUpsyncFps = Math.round(this.peerInputFrameUpsyncCnt * 1000 / elapsedMillis);
}
return [sendingFps, srvDownsyncFps, peerUpsyncFps, rollbackFrames, this.skippedRenderFrameCnt];
};
NetworkDoctor.prototype.logSkippedRenderFrameCnt = function() {
this.skippedRenderFrameCnt += 1;
}
NetworkDoctor.prototype.isTooFast = function() {
return false;
const [sendingFps, srvDownsyncFps, peerUpsyncFps, rollbackFrames, skippedRenderFrameCnt] = this.stats();
if (sendingFps >= this.inputRateThreshold + 2) {
// Don't send too fast
return true;
} else if (sendingFps >= this.inputRateThreshold && srvDownsyncFps >= this.inputRateThreshold) {
// An outstanding lag within the "inputFrameDownsyncQ" will reduce "srvDownsyncFps", HOWEVER, a constant lag wouldn't impact "srvDownsyncFps"! In native platforms we might use PING value might help as a supplement information to confirm that the "selfPlayer" is not lagged within the time accounted by "inputFrameDownsyncQ".
if (rollbackFrames >= this.rollbackFramesThreshold) {
// I got many frames rolled back while none of my peers effectively helped my preciction. Deliberately not using "peerUpsyncThreshold" here because when using UDP p2p upsync broadcasting, we expect to receive effective p2p upsyncs from every other player.
return true;
}
}
return false;
};
module.exports = NetworkDoctor;

View File

@@ -1,6 +1,6 @@
{
"ver": "1.0.5",
"uuid": "477c07c3-0d50-4d55-96f0-6eaf9f25e2da",
"uuid": "affd726a-02f0-4079-aace-39fe525d7478",
"isPlugin": false,
"loadPluginInWeb": true,
"loadPluginInNative": true,

View File

@@ -11,11 +11,13 @@ cc.Class({
},
onLoad() {
cc.game.setFrameRate(60);
cc.view.setOrientation(cc.macro.ORIENTATION_LANDSCAPE);
cc.view.enableAutoFullScreen(true);
const self = this;
window.mapIns = self;
self.showCriticalCoordinateLabels = false;
self.showNetworkDoctorInfo = true;
const mapNode = self.node;
const canvasNode = mapNode.parent;
@@ -96,7 +98,7 @@ cc.Class({
const p2Vpos = gopkgs.WorldToVirtualGridPos(boundaryObjs.playerStartingPositions[1].x, boundaryObjs.playerStartingPositions[1].y);
const colliderRadiusV = gopkgs.WorldToVirtualGridPos(12.0, 0);
const speciesIdList = [4096, 1];
const speciesIdList = [1, 4096];
const chConfigsOrderedByJoinIndex = gopkgs.GetCharacterConfigsOrderedByJoinIndex(speciesIdList);
const startRdf = window.pb.protos.RoomDownsyncFrame.create({
@@ -142,6 +144,35 @@ cc.Class({
Id: 10,
JoinIndex: 1,
};
if (cc.sys.isNative) {
window.onUdpMessage = (args) => {
const len = args.length;
const ui8Arr = new Uint8Array(len);
for (let i = 0; i < len; i++) {
ui8Arr[i] = args.charCodeAt(i);
}
cc.log(`#1 Js called back by CPP: onUdpMessage: args=${args}, typeof(args)=${typeof (args)}, argslen=${args.length}, ui8Arr=${ui8Arr}`);
const echoed = window.pb.protos.HolePunchUpsync.decode(ui8Arr);
cc.log(`#2 Js called back by CPP: onUdpMessage: ${JSON.stringify(echoed)}`);
};
const res1 = DelayNoMore.UdpSession.openUdpSession(8888 + self.selfPlayerInfo.JoinIndex);
const holePunchData = window.pb.protos.HolePunchUpsync.encode({
boundRoomId: 22,
intAuthToken: "foobar",
authKey: Math.floor(Math.random() * 65535),
}).finish()
//const res2 = DelayNoMore.UdpSession.punchToServer("127.0.0.1", 3000, holePunchData, 19999, holePunchData);
const res3 = DelayNoMore.UdpSession.upsertPeerUdpAddr([window.pb.protos.PeerUdpAddr.create({
ip: "192.168.31.194",
port: 6789,
authKey: 123456,
}), window.pb.protos.PeerUdpAddr.create({
ip: "192.168.1.101",
port: 8771,
authKey: 654321,
})], 2, self.selfPlayerInfo.JoinIndex);
//const res4 = DelayNoMore.UdpSession.closeUdpSession();
}
self.onRoomDownsyncFrame(startRdf);
self.battleState = ALL_BATTLE_STATES.IN_BATTLE;
@@ -152,12 +183,6 @@ cc.Class({
update(dt) {
const self = this;
if (ALL_BATTLE_STATES.IN_BATTLE == self.battleState) {
const elapsedMillisSinceLastFrameIdTriggered = performance.now() - self.lastRenderFrameIdTriggeredAt;
if (elapsedMillisSinceLastFrameIdTriggered < self.tooFastDtIntervalMillis) {
// [WARNING] We should avoid a frontend ticking too fast to prevent cheating, as well as ticking too slow to cause a "resync avalanche" that impacts user experience!
// console.debug("Avoiding too fast frame@renderFrameId=", self.renderFrameId, ": elapsedMillisSinceLastFrameIdTriggered=", elapsedMillisSinceLastFrameIdTriggered);
//return;
}
try {
let st = performance.now();
let prevSelfInput = null,

View File

@@ -13,9 +13,12 @@ var RingBuffer = function(capacity) {
};
RingBuffer.prototype.put = function(item) {
let firstPopped = null;
while (0 < this.cnt && this.cnt >= this.n) {
// Make room for the new element
this.pop();
const popped = this.pop();
if (null == firstPopped)
firstPopped = popped;
}
this.eles[this.ed] = item
this.edFrameId++;
@@ -24,6 +27,7 @@ RingBuffer.prototype.put = function(item) {
if (this.ed >= this.n) {
this.ed -= this.n; // Deliberately not using "%" operator for performance concern
}
return firstPopped;
};
RingBuffer.prototype.pop = function() {

View File

@@ -12,6 +12,7 @@ window.DOWNSYNC_MSG_ACT_INPUT_BATCH = 2;
window.DOWNSYNC_MSG_ACT_BATTLE_STOPPED = 3;
window.DOWNSYNC_MSG_ACT_FORCED_RESYNC = 4;
window.DOWNSYNC_MSG_ACT_PEER_INPUT_BATCH = 5;
window.DOWNSYNC_MSG_ACT_PEER_UDP_ADDR = 6;
window.sendSafely = function(msgStr) {
/**
@@ -33,7 +34,7 @@ window.closeWSConnection = function(code, reason) {
console.log(`"window.clientSession" is already closed or destroyed.`);
return;
}
console.log(`Closing "window.clientSession" from the client-side.`);
console.log(`Closing "window.clientSession" from the client-side with code=${code}.`);
window.clientSession.close(code, reason);
}
@@ -43,12 +44,24 @@ window.getBoundRoomIdFromPersistentStorage = function() {
window.clearBoundRoomIdInBothVolatileAndPersistentStorage();
return null;
}
return cc.sys.localStorage.getItem("boundRoomId");
const boundRoomIdStr = cc.sys.localStorage.getItem("boundRoomId");
return (null == boundRoomIdStr ? null : parseInt(boundRoomIdStr));
};
window.getBoundRoomCapacityFromPersistentStorage = function() {
const boundRoomIdExpiresAt = parseInt(cc.sys.localStorage.getItem("boundRoomIdExpiresAt"));
if (!boundRoomIdExpiresAt || Date.now() >= boundRoomIdExpiresAt) {
window.clearBoundRoomIdInBothVolatileAndPersistentStorage();
return null;
}
const boundRoomCapacityStr = cc.sys.localStorage.getItem("boundRoomCapacity");
return (null == boundRoomCapacityStr ? null : parseInt(boundRoomCapacityStr));
};
window.clearBoundRoomIdInBothVolatileAndPersistentStorage = function() {
window.boundRoomId = null;
cc.sys.localStorage.removeItem("boundRoomId");
cc.sys.localStorage.removeItem("boundRoomCapacity");
cc.sys.localStorage.removeItem("boundRoomIdExpiresAt");
};
@@ -57,18 +70,51 @@ window.clearSelfPlayer = function() {
};
window.boundRoomId = getBoundRoomIdFromPersistentStorage();
window.boundRoomCapacity = getBoundRoomCapacityFromPersistentStorage();
window.handleHbRequirements = function(resp) {
console.log(`Handle hb requirements #1`);
if (constants.RET_CODE.OK != resp.ret) return;
if (null == window.boundRoomId) {
// The assignment of "window.mapIns" is inside "Map.onLoad", which precedes "initPersistentSessionClient".
window.mapIns.selfPlayerInfo = JSON.parse(cc.sys.localStorage.getItem('selfPlayer')); // This field is kept for distinguishing "self" and "others".
window.mapIns.selfPlayerInfo.Id = window.mapIns.selfPlayerInfo.playerId;
window.mapIns.selfPlayerInfo.JoinIndex = resp.peerJoinIndex;
console.log(`Handle hb requirements #2`);
if (null == window.boundRoomId || null == window.boundRoomCapacity) {
window.boundRoomId = resp.bciFrame.boundRoomId;
window.boundRoomCapacity = resp.bciFrame.boundRoomCapacity;
cc.sys.localStorage.setItem('boundRoomId', window.boundRoomId);
cc.sys.localStorage.setItem('boundRoomCapacity', window.boundRoomCapacity);
cc.sys.localStorage.setItem('boundRoomIdExpiresAt', Date.now() + 10 * 60 * 1000); // Temporarily hardcoded, for `boundRoomId` only.
}
console.log(`Handle hb requirements #3`);
if (window.handleBattleColliderInfo) {
window.initSecondarySession(null, window.boundRoomId);
window.handleBattleColliderInfo(resp.bciFrame);
}
console.log(`Handle hb requirements #4`);
if (!cc.sys.isNative) {
console.log(`Handle hb requirements #5, web`);
window.initSecondarySession(null, window.boundRoomId);
} else {
console.log(`Handle hb requirements #5, native, bciFrame.battleUdpTunnel=${resp.bciFrame.battleUdpTunnel}, selfPlayerInfo=${JSON.stringify(window.mapIns.selfPlayerInfo)}`);
const res1 = DelayNoMore.UdpSession.openUdpSession(8888 + window.mapIns.selfPlayerInfo.JoinIndex);
window.mapIns.selfPlayerInfo.udpTunnelAuthKey = resp.bciFrame.battleUdpTunnel.authKey;
const intAuthToken = window.mapIns.selfPlayerInfo.intAuthToken;
const authKey = Math.floor(Math.random() * 65535);
window.mapIns.selfPlayerInfo.authKey = authKey;
const holePunchData = window.pb.protos.HolePunchUpsync.encode({
boundRoomId: window.boundRoomId,
intAuthToken: intAuthToken,
authKey: authKey,
}).finish();
const udpTunnelHolePunchData = window.pb.protos.WsReq.encode({
msgId: Date.now(),
playerId: window.mapIns.selfPlayerInfo.Id,
act: window.UPSYNC_MSG_ACT_PLAYER_CMD,
authKey: resp.bciFrame.battleUdpTunnel.authKey,
}).finish();
const res2 = DelayNoMore.UdpSession.punchToServer(backendAddress.HOST, 3000, holePunchData, resp.bciFrame.battleUdpTunnel.port, udpTunnelHolePunchData);
}
};
function _uint8ToBase64(uint8Arr) {
@@ -126,6 +172,7 @@ window.initPersistentSessionClient = function(onopenCb, expectedRoomId) {
urlToConnect = urlToConnect + "&expectedRoomId=" + expectedRoomId;
} else {
window.boundRoomId = getBoundRoomIdFromPersistentStorage();
window.boundRoomCapacity = getBoundRoomCapacityFromPersistentStorage();
if (null != window.boundRoomId) {
console.log("initPersistentSessionClient with boundRoomId == " + boundRoomId);
urlToConnect = urlToConnect + "&boundRoomId=" + window.boundRoomId;
@@ -176,6 +223,15 @@ window.initPersistentSessionClient = function(onopenCb, expectedRoomId) {
}
mapIns.onRoomDownsyncFrame(resp.rdf, resp.inputFrameDownsyncBatch);
break;
case window.DOWNSYNC_MSG_ACT_PEER_UDP_ADDR:
console.warn(`Got DOWNSYNC_MSG_ACT_PEER_UDP_ADDR resp=${JSON.stringify(resp, null, 2)}`);
if (cc.sys.isNative) {
const peerJoinIndex = resp.peerJoinIndex;
const peerAddrList = resp.rdf.peerUdpAddrList;
console.log(`Got DOWNSYNC_MSG_ACT_PEER_UDP_ADDR peerAddrList=${JSON.stringify(peerAddrList)}; boundRoomCapacity=${window.boundRoomCapacity}`);
DelayNoMore.UdpSession.upsertPeerUdpAddr(peerAddrList, window.boundRoomCapacity, window.mapIns.selfPlayerInfo.JoinIndex); // In C++ impl it actually broadcasts the peer-punching message to all known peers within "window.boundRoomCapacity"
}
break;
default:
break;
}
@@ -192,6 +248,12 @@ window.initPersistentSessionClient = function(onopenCb, expectedRoomId) {
clientSession.onclose = function(evt) {
// [WARNING] The callback "onclose" might be called AFTER the webpage is refreshed with "1001 == evt.code".
console.warn(`The WS clientSession is closed: evt=${JSON.stringify(evt)}, evt.code=${evt.code}`);
if (cc.sys.isNative) {
if (mapIns.frameDataLoggingEnabled) {
console.warn(`${mapIns._stringifyRdfIdToActuallyUsedInput()}`);
}
DelayNoMore.UdpSession.closeUdpSession();
}
switch (evt.code) {
case constants.RET_CODE.CLIENT_MISMATCHED_RENDER_FRAME:
break;
@@ -213,7 +275,7 @@ window.initPersistentSessionClient = function(onopenCb, expectedRoomId) {
case constants.RET_CODE.MYSQL_ERROR:
case constants.RET_CODE.PLAYER_NOT_FOUND:
case constants.RET_CODE.PLAYER_CHEATING:
case 1006: // Peer(i.e. the backend) gone unexpectedly
case 1006: // Peer(i.e. the backend) gone unexpectedly, but not working for "cc.sys.isNative"
if (mapIns.frameDataLoggingEnabled) {
console.warn(`${mapIns._stringifyRdfIdToActuallyUsedInput()}`);
}
@@ -227,16 +289,15 @@ window.initPersistentSessionClient = function(onopenCb, expectedRoomId) {
window.clearLocalStorageAndBackToLoginScene = function(shouldRetainBoundRoomIdInBothVolatileAndPersistentStorage) {
console.warn("+++++++ Calling `clearLocalStorageAndBackToLoginScene`");
window.clearSelfPlayer();
if (true != shouldRetainBoundRoomIdInBothVolatileAndPersistentStorage) {
window.clearBoundRoomIdInBothVolatileAndPersistentStorage();
}
if (window.mapIns && window.mapIns.musicEffectManagerScriptIns) {
window.mapIns.musicEffectManagerScriptIns.stopAllMusic();
}
window.closeWSConnection();
window.clearSelfPlayer();
if (true != shouldRetainBoundRoomIdInBothVolatileAndPersistentStorage) {
window.clearBoundRoomIdInBothVolatileAndPersistentStorage();
}
cc.director.loadScene('login');
};

View File

@@ -1,7 +1,7 @@
{
"ver": "1.0.5",
"uuid": "40edd08e-316c-44b8-a50f-bd173554c554",
"isPlugin": true,
"isPlugin": false,
"loadPluginInWeb": true,
"loadPluginInNative": true,
"loadPluginInEditor": false,

View File

@@ -0,0 +1,75 @@
{
"do_default": {
"exclude_from_template": [
"frameworks/runtime-src"
]
},
"do_add_native_support": {
"append_from_template": {
"from": "frameworks/runtime-src",
"to": "frameworks/runtime-src",
"exclude": [
"proj.win32/Debug.win32",
"proj.win32/Release.win32",
"proj.win32/DelayNoMore.sdf"
]
},
"append_file": [{
"from": "cocos/scripting/js-bindings/manual/jsb_module_register.cpp",
"to": "frameworks/runtime-src/Classes/jsb_module_register.cpp"
}, {
"from": "frameworks/runtime-src/Classes/udp_session.hpp",
"to": "frameworks/runtime-src/Classes/udp_session.hpp"
}, {
"from": "frameworks/runtime-src/Classes/udp_session.cpp",
"to": "frameworks/runtime-src/Classes/udp_session.cpp"
}, {
"from": "frameworks/runtime-src/Classes/udp_session_bridge.hpp",
"to": "frameworks/runtime-src/Classes/udp_session_bridge.hpp"
}, {
"from": "frameworks/runtime-src/Classes/udp_session_bridge.cpp",
"to": "frameworks/runtime-src/Classes/udp_session_bridge.cpp"
}],
"project_rename": {
"src_project_name": "DelayNoMore",
"files": [
"frameworks/runtime-src/proj.win32/PROJECT_NAME.vcxproj",
"frameworks/runtime-src/proj.win32/PROJECT_NAME.vcxproj.filters",
"frameworks/runtime-src/proj.win32/PROJECT_NAME.vcxproj.user",
"frameworks/runtime-src/proj.win32/PROJECT_NAME.sln"
]
},
"project_replace_project_name": {
"src_project_name": "DelayNoMore",
"files": [
"frameworks/runtime-src/proj.win32/PROJECT_NAME.vcxproj",
"frameworks/runtime-src/proj.win32/PROJECT_NAME.vcxproj.filters",
"frameworks/runtime-src/proj.win32/PROJECT_NAME.vcxproj.user",
"frameworks/runtime-src/proj.win32/PROJECT_NAME.sln",
"frameworks/runtime-src/proj.win32/main.cpp",
"frameworks/runtime-src/proj.android-studio/settings.gradle",
"frameworks/runtime-src/proj.android-studio/app/res/values/strings.xml",
"frameworks/runtime-src/Classes/AppDelegate.cpp"
]
},
"project_replace_package_name": {
"src_package_name": "org.genxium.delaynomore",
"files": [
"frameworks/runtime-src/proj.android-studio/app/build.gradle",
"frameworks/runtime-src/proj.android-studio/app/AndroidManifest.xml"
]
},
"project_replace_mac_bundleid": {
"src_bundle_id": "org.genxium.delaynomore",
"files": [
"frameworks/runtime-src/proj.ios_mac/mac/Info.plist"
]
},
"project_replace_ios_bundleid": {
"src_bundle_id": "org.genxium.delaynomore",
"files": [
"frameworks/runtime-src/proj.ios_mac/ios/Info.plist"
]
}
}
}

View File

@@ -0,0 +1,93 @@
/****************************************************************************
Copyright (c) 2017-2018 Xiamen Yaji Software Co., Ltd.
http://www.cocos.com
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated engine source code (the "Software"), a limited,
worldwide, royalty-free, non-assignable, revocable and non-exclusive license
to use Cocos Creator solely to develop games on your target platforms. You shall
not use Cocos Creator software for developing other software or tools that's
used for developing games. You are not granted to publish, distribute,
sublicense, and/or sell copies of Cocos Creator.
The software or tools in this License Agreement are licensed, not sold.
Xiamen Yaji Software Co., Ltd. reserves all rights not expressly granted to you.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
****************************************************************************/
#include "AppDelegate.h"
#include "cocos2d.h"
#include "cocos/audio/include/AudioEngine.h"
#include "cocos/scripting/js-bindings/manual/jsb_module_register.hpp"
#include "cocos/scripting/js-bindings/manual/jsb_global.h"
#include "cocos/scripting/js-bindings/jswrapper/SeApi.h"
#include "cocos/scripting/js-bindings/event/EventDispatcher.h"
#include "cocos/scripting/js-bindings/manual/jsb_classtype.hpp"
#include "udp_session_bridge.hpp"
USING_NS_CC;
AppDelegate::AppDelegate(int width, int height) : Application("Cocos Game", width, height)
{
}
AppDelegate::~AppDelegate()
{
}
bool AppDelegate::applicationDidFinishLaunching()
{
se::ScriptEngine *se = se::ScriptEngine::getInstance();
jsb_set_xxtea_key("");
jsb_init_file_operation_delegate();
#if defined(COCOS2D_DEBUG) && (COCOS2D_DEBUG > 0)
// Enable debugger here
jsb_enable_debugger("0.0.0.0", 6086, false);
#endif
se->setExceptionCallback([](const char *location, const char *message, const char *stack) {
// Send exception information to server like Tencent Bugly.
});
jsb_register_all_modules();
se->addRegisterCallback(registerUdpSession);
se->start();
se::AutoHandleScope hs;
jsb_run_script("jsb-adapter/jsb-builtin.js");
jsb_run_script("main.js");
se->addAfterCleanupHook([]() {
JSBClassType::destroy();
});
return true;
}
// This function will be called when the app is inactive. When comes a phone call,it's be invoked too
void AppDelegate::applicationDidEnterBackground()
{
EventDispatcher::dispatchEnterBackgroundEvent();
// Ensure that handle AudioEngine enter background after all enter background events are handled
AudioEngine::onEnterBackground();
}
// this function will be called when the app is active again
void AppDelegate::applicationWillEnterForeground()
{
// Ensure that handle AudioEngine enter foreground before all enter foreground events are handled
AudioEngine::onEnterForeground();
EventDispatcher::dispatchEnterForegroundEvent();
}

View File

@@ -0,0 +1,406 @@
#include "udp_session.hpp"
#include "base/ccMacros.h"
#include "cocos/platform/CCApplication.h"
#include "cocos/base/CCScheduler.h"
#include "cocos/scripting/js-bindings/jswrapper/SeApi.h"
uv_udp_t* udpSocket = NULL;
uv_thread_t recvTid;
uv_timer_t peerPunchTimer;
uv_async_t uvLoopStopSig;
uv_loop_t* loop = NULL; // Only this loop is used for this simple PoC
struct PeerAddr peerAddrList[maxPeerCnt];
char SRV_IP[256];
int SRV_PORT = 0;
int UDP_TUNNEL_SRV_PORT = 0;
struct PeerAddr udpTunnelAddr;
void _onRead(uv_udp_t* req, ssize_t nread, uv_buf_t const* buf, struct sockaddr const* addr, unsigned flags) {
if (nread < 0) {
CCLOGERROR("Read error %s", uv_err_name(nread));
uv_close((uv_handle_t*)req, NULL);
free(buf->base);
return;
}
char ip[INET_ADDRSTRLEN];
memset(ip, 0, sizeof ip);
int port = 0;
if (NULL != addr) {
// The null check for "addr" is necessary, on Android there'd be such mysterious call to "_onRead"!
switch (addr->sa_family) {
case AF_INET: {
struct sockaddr_in const* sockAddr = (struct sockaddr_in const*)addr;
uv_inet_ntop(sockAddr->sin_family, &(sockAddr->sin_addr), ip, INET_ADDRSTRLEN);
port = ntohs(sockAddr->sin_port);
CCLOG("UDP received %d bytes from %s:%d", nread, ip, port);
break;
}
default:
break;
}
} else {
CCLOG("UDP received %d bytes from unknown sender", nread);
}
if (6 == nread) {
// holepunching
} else if (0 < nread) {
// Non-holepunching; it might be more effective in RAM usage to use a threadsafe RingBuff to pass msg to GameThread here, but as long as it's not a performance blocker don't bother optimize here...
uint8_t* const ui8Arr = (uint8_t*)malloc(maxUdpPayloadBytes*sizeof(uint8_t));
memset(ui8Arr, 0, sizeof ui8Arr);
for (int i = 0; i < nread; i++) {
*(ui8Arr+i) = *(buf->base + i);
}
cocos2d::Application::getInstance()->getScheduler()->performFunctionInCocosThread([=]() {
// [WARNING] Use of the "ScriptEngine" is only allowed in "GameThread a.k.a. CocosThread"!
se::Value onUdpMessageCb;
se::ScriptEngine::getInstance()->getGlobalObject()->getProperty("onUdpMessage", &onUdpMessageCb);
// [WARNING] Declaring "AutoHandleScope" is critical here, otherwise "onUdpMessageCb.toObject()" wouldn't be recognized as a function of the ScriptEngine!
se::AutoHandleScope hs;
//CCLOG("UDP received %d bytes upsync -- 1", nread);
se::Object* const gameThreadMsg = se::Object::createTypedArray(se::Object::TypedArrayType::UINT8, ui8Arr, nread);
//CCLOG("UDP received %d bytes upsync -- 2", nread);
se::ValueArray args = { se::Value(gameThreadMsg) };
if (onUdpMessageCb.isObject() && onUdpMessageCb.toObject()->isFunction()) {
// Temporarily assume that the "this" ptr within callback is NULL.
bool ok = onUdpMessageCb.toObject()->call(args, NULL);
if (!ok) {
se::ScriptEngine::getInstance()->clearException();
}
}
//CCLOG("UDP received %d bytes upsync -- 3", nread);
gameThreadMsg->decRef(); // Reference http://docs.cocos.com/creator/2.2/manual/en/advanced-topics/JSB2.0-learning.html#seobject
//CCLOG("UDP received %d bytes upsync -- 4", nread);
free(ui8Arr);
CCLOG("UDP received %d bytes upsync -- 5", nread);
});
}
free(buf->base);
/*
// [WARNING] Don't call either of the following statements! They will make "_onRead" no longer called for incoming packets!
//uv_udp_recv_stop(req);
//uv_close((uv_handle_t*)req, NULL);
*/
}
static void _allocBuffer(uv_handle_t* handle, size_t suggested_size, uv_buf_t* buf) {
(void)handle;
buf->base = (char*)malloc(suggested_size);
buf->len = suggested_size;
}
void _onUvStopSig(uv_async_t* handle) {
uv_stop(loop);
CCLOG("UDP recv loop is signaled to stop in UvThread");
}
void _onSend(uv_udp_send_t* req, int status) {
CCLOG("UDP send about to free req for status:%d...", status);
free(req); // No need to free "req->base", it'll be handled in each "_afterXxx" callback
CCLOG("UDP send freed req for status:%d...", status);
if (status) {
CCLOGERROR("uv_udp_send_cb error: %s\n", uv_strerror(status));
}
}
void _onUvTimerClosed(uv_handle_t* timer) {
free(timer);
}
int const punchServerCnt = 3;
class PunchServerWork {
public:
BYTEC bytes[maxUdpPayloadBytes]; // Wasting some RAM here thus no need for explicit recursive destruction
size_t bytesLen;
BYTEC udpTunnelBytes[maxUdpPayloadBytes];
size_t udpTunnelBytesLen;
PunchServerWork(BYTEC* const newBytes, size_t newBytesLen, BYTEC* const newUdpTunnelBytes, size_t newUdpTunnelBytesLen) {
memset(this->bytes, 0, sizeof(this->bytes));
memcpy(this->bytes, newBytes, newBytesLen);
this->bytesLen = newBytesLen;
memset(this->udpTunnelBytes, 0, sizeof(this->udpTunnelBytes));
memcpy(this->udpTunnelBytes, newUdpTunnelBytes, newUdpTunnelBytesLen);
this->udpTunnelBytesLen = newUdpTunnelBytesLen;
}
};
void _punchServerOnUvThread(uv_work_t* wrapper) {
PunchServerWork* work = (PunchServerWork*)wrapper->data;
for (int i = 0; i < punchServerCnt; i++) {
uv_udp_send_t* req = (uv_udp_send_t*)malloc(sizeof(uv_udp_send_t));
uv_buf_t sendBuffer = uv_buf_init(work->bytes, work->bytesLen);
struct sockaddr_in destAddr;
uv_ip4_addr(SRV_IP, SRV_PORT, &destAddr);
uv_udp_send(req, udpSocket, &sendBuffer, 1, (struct sockaddr const*)&destAddr, _onSend);
uv_udp_send_t* udpTunnelReq = (uv_udp_send_t*)malloc(sizeof(uv_udp_send_t));
uv_buf_t udpTunnelSendBuffer = uv_buf_init(work->udpTunnelBytes, work->udpTunnelBytesLen);
struct sockaddr_in udpTunnelDestAddr;
uv_ip4_addr(SRV_IP, UDP_TUNNEL_SRV_PORT, &udpTunnelDestAddr);
udpTunnelAddr.sockAddrIn = udpTunnelDestAddr;
uv_udp_send(udpTunnelReq, udpSocket, &udpTunnelSendBuffer, 1, (struct sockaddr const*)&udpTunnelDestAddr, _onSend);
}
}
void _afterPunchServer(uv_work_t* wrapper, int status) {
CCLOG("UDP send about to free PunchServerWork for status:%d...", status);
PunchServerWork* work = (PunchServerWork*)wrapper->data;
delete work;
CCLOG("UDP freed PunchServerWork for status:%d...", status);
}
class PunchPeerWork {
public:
int roomCapacity;
int selfJoinIndex;
int naiveRefCnt;
PunchPeerWork(int newRoomCapacity, int newSelfJoinIndex) {
this->roomCapacity = newRoomCapacity;
this->selfJoinIndex = newSelfJoinIndex;
this->naiveRefCnt = 0;
}
void refInc() {
++this->naiveRefCnt;
}
void refDecAndDelIfZero() {
--this->naiveRefCnt;
if (0 >= this->naiveRefCnt) {
delete this;
}
}
virtual ~PunchPeerWork() {
CCLOG("PunchPeerWork instance deleted...");
}
};
void _punchPeerOnUvThreadDelayed(uv_timer_t* timer, int status) {
//CCLOG("_punchPeerOnUvThreadDelayed started...");
PunchPeerWork* work = (PunchPeerWork*)timer->data;
int roomCapacity = work->roomCapacity;
int selfJoinIndex = work->selfJoinIndex;
for (int i = 0; i < roomCapacity; i++) {
if (i + 1 == selfJoinIndex) {
continue;
}
if (0 == peerAddrList[i].sockAddrIn.sin_port) {
// Peer addr not initialized
continue;
}
//CCLOG("UDP about to punch peer joinIndex:%d", i);
char peerIp[17] = { 0 };
uv_ip4_name((struct sockaddr_in*)&(peerAddrList[i].sockAddrIn), peerIp, sizeof peerIp);
int peerPortSt = ntohs(peerAddrList[i].sockAddrIn.sin_port);
int peerPortEd = ntohs(peerAddrList[i].sockAddrIn.sin_port) + 1; // Use tunnel of backend instead of sweeping ports blindly!
for (int peerPort = peerPortSt; peerPort < peerPortEd; peerPort++) {
if (0 > peerPort) continue;
uv_udp_send_t* req = (uv_udp_send_t*)malloc(sizeof(uv_udp_send_t));
uv_buf_t sendBuffer = uv_buf_init("foobar", 6); // hardcoded for now
struct sockaddr_in testPeerAddr;
uv_ip4_addr(peerIp, peerPort, &testPeerAddr);
uv_udp_send(req, udpSocket, &sendBuffer, 1, (struct sockaddr const*)&testPeerAddr, _onSend);
CCLOG("UDP punched peer %s:%d by 6 bytes", peerIp, peerPort);
}
}
uv_timer_stop(timer);
uv_close((uv_handle_t*)timer, _onUvTimerClosed);
//CCLOG("_punchPeerOnUvThreadDelayed stopped...");
work->refDecAndDelIfZero();
}
int const punchPeerCnt = 3;
void _startPunchPeerTimerOnUvThread(uv_work_t* wrapper) {
PunchPeerWork* work = (PunchPeerWork*)wrapper->data;
int roomCapacity = work->roomCapacity;
int selfJoinIndex = work->selfJoinIndex;
for (int j = 0; j < punchPeerCnt; j++) {
work->refInc();
}
for (int j = 0; j < punchPeerCnt; j++) {
uv_timer_t* punchTimer = (uv_timer_t*)malloc(sizeof(uv_timer_t)); // I don't think libuv timer is safe to be called from GameThread, thus calling it within UvThread here
uv_timer_init(loop, punchTimer);
punchTimer->data = work;
uv_timer_start(punchTimer, (uv_timer_cb)&_punchPeerOnUvThreadDelayed, j * 500, 0);
}
}
void _afterPunchPeerTimerStarted(uv_work_t* wrapper, int status) {
// RAM of PunchPeerWork handled by "naiveRefCnt"
}
class BroadcastInputFrameUpsyncWork {
public:
BYTEC bytes[maxUdpPayloadBytes]; // Wasting some RAM here thus no need for explicit recursive destruction
size_t bytesLen;
int roomCapacity;
int selfJoinIndex;
BroadcastInputFrameUpsyncWork(BYTEC* const newBytes, size_t newBytesLen, int newRoomCapacity, int newSelfJoinIndex) {
memset(this->bytes, 0, sizeof(this->bytes));
memcpy(this->bytes, newBytes, newBytesLen);
this->bytesLen = newBytesLen;
this->roomCapacity = newRoomCapacity;
this->selfJoinIndex = newSelfJoinIndex;
}
};
int const broadcastUpsyncCnt = 1;
void _broadcastInputFrameUpsyncOnUvThread(uv_work_t* wrapper) {
BroadcastInputFrameUpsyncWork* work = (BroadcastInputFrameUpsyncWork*)wrapper->data;
int roomCapacity = work->roomCapacity;
int selfJoinIndex = work->selfJoinIndex;
// Send to room udp tunnel in case of hole punching failure
for (int j = 0; j < broadcastUpsyncCnt; j++) {
uv_udp_send_t* req = (uv_udp_send_t*)malloc(sizeof(uv_udp_send_t));
uv_buf_t sendBuffer = uv_buf_init(work->bytes, work->bytesLen);
uv_udp_send(req, udpSocket, &sendBuffer, 1, (struct sockaddr const*)&(udpTunnelAddr.sockAddrIn), _onSend);
CCLOG("UDP sent upsync to udp tunnel %s:%d by %u bytes round-%d", SRV_IP, UDP_TUNNEL_SRV_PORT, work->bytesLen, j);
}
for (int i = 0; i < roomCapacity; i++) {
if (i + 1 == selfJoinIndex) {
continue;
}
if (0 == peerAddrList[i].sockAddrIn.sin_port) {
// Peer addr not initialized
continue;
}
char peerIp[17] = { 0 };
uv_ip4_name((struct sockaddr_in*)&(peerAddrList[i].sockAddrIn), peerIp, sizeof peerIp);
// Might want to send several times for better arrival rate
for (int j = 0; j < broadcastUpsyncCnt; j++) {
uv_udp_send_t* req = (uv_udp_send_t*)malloc(sizeof(uv_udp_send_t));
uv_buf_t sendBuffer = uv_buf_init(work->bytes, work->bytesLen);
uv_udp_send(req, udpSocket, &sendBuffer, 1, (struct sockaddr const*)&(peerAddrList[i].sockAddrIn), _onSend);
CCLOG("UDP broadcasted upsync to peer %s:%d by %u bytes round-%d", peerIp, ntohs(peerAddrList[i].sockAddrIn.sin_port), work->bytesLen, j);
}
}
}
void _afterBroadcastInputFrameUpsync(uv_work_t* wrapper, int status) {
BroadcastInputFrameUpsyncWork* work = (BroadcastInputFrameUpsyncWork*)wrapper->data;
delete work;
}
void _onWalkCleanup(uv_handle_t* handle, void* data) {
(void)data;
uv_close(handle, NULL);
}
void startRecvLoop(void* arg) {
uv_loop_t* l = (uv_loop_t*)arg;
int uvRunRet1 = uv_run(l, UV_RUN_DEFAULT);
CCLOG("UDP recv loop is ended in UvThread, uvRunRet1=%d", uvRunRet1);
uv_walk(l, _onWalkCleanup, NULL);
int uvRunRet2 = uv_run(l, UV_RUN_DEFAULT);
int uvCloseRet = uv_loop_close(l);
CCLOG("UDP recv loop is closed in UvThread, uvRunRet2=%d, uvCloseRet=%d", uvRunRet2, uvCloseRet);
}
bool DelayNoMore::UdpSession::openUdpSession(int port) {
loop = uv_loop_new();
udpSocket = (uv_udp_t*)malloc(sizeof(uv_udp_t));
int sockInitRes = uv_udp_init(loop, udpSocket); // "uv_udp_init" must precede that of "uv_udp_bind" for successful binding!
struct sockaddr_in recv_addr;
uv_ip4_addr("0.0.0.0", port, &recv_addr);
int bindRes = uv_udp_bind(udpSocket, (struct sockaddr const*)&recv_addr, UV_UDP_REUSEADDR);
if (0 != bindRes) {
CCLOGERROR("Failed to bind port=%d; bind result=%d, reason=%s", port, bindRes, uv_strerror(bindRes));
exit(-1);
}
uv_async_init(loop, &uvLoopStopSig, _onUvStopSig);
CCLOG("About to open UDP session at port=%d; bind result=%d, sock init result=%d...", port, bindRes, sockInitRes);
uv_udp_recv_start(udpSocket, _allocBuffer, _onRead);
// TODO: Currently "sending" is also done in the "receiving loop thread", shall I segregate it to another dedicated thread?
uv_thread_create(&recvTid, startRecvLoop, loop);
CCLOG("Finished opening UDP session at port=%d", port);
return true;
}
bool DelayNoMore::UdpSession::closeUdpSession() {
CCLOG("About to close udp session and dealloc all resources...");
for (int i = 0; i < maxPeerCnt; i++) {
peerAddrList[i].authKey = -1; // hardcoded for now
memset((char*)&peerAddrList[i].sockAddrIn, 0, sizeof(peerAddrList[i].sockAddrIn));
}
uv_async_send(&uvLoopStopSig); // The few if not only guaranteed thread safe utility of libuv :) See http://docs.libuv.org/en/v1.x/async.html#c.uv_async_send
CCLOG("Signaling UvThread to end in GameThread...");
uv_thread_join(&recvTid);
free(udpSocket);
free(loop);
CCLOG("Closed udp session and dealloc all resources in GameThread...");
return true;
}
bool DelayNoMore::UdpSession::punchToServer(CHARC* const srvIp, int const srvPort, BYTEC* const bytes, size_t bytesLen, int const udpTunnelSrvPort, BYTEC* const udpTunnelBytes, size_t udpTunnelBytesBytesLen) {
/*
[WARNING] The RAM space used for "bytes", either on stack or in heap, is preallocatedand managed by the caller which runs on the GameThread. Actual sending will be made on UvThread.
Therefore we make a copy of this message before dispatching it "GameThread -> UvThread".
*/
memset(SRV_IP, 0, sizeof SRV_IP);
memcpy(SRV_IP, srvIp, strlen(srvIp));
SRV_PORT = srvPort;
UDP_TUNNEL_SRV_PORT = udpTunnelSrvPort;
PunchServerWork* work = new PunchServerWork(bytes, bytesLen, udpTunnelBytes, udpTunnelBytesBytesLen);
/*
TODO: Libuv is really inconvenient here, neither "uv_queue_work" nor "uv_async_init" is threadsafe(http ://docs.libuv.org/en/v1.x/threadpool.html#c.uv_queue_work)! What's the point of such a queue? It's even more difficult than writing my own implementation -- again a threadsafe RingBuff could be used to the rescue, yet I'd like to investigate more into how to make the following threadsafe APIs with minimum cross-platform C++ codes
- _sendMessage(...), should be both non-blocking & threadsafe, called from GameThread
- _onRead(...), should be called first in UvThread in an edge-triggered manner like idiomatic "epoll" or "kqueue", then dispatch the received message to GameThread by a threadsafe RingBuff
*/
uv_work_t* wrapper = (uv_work_t*)malloc(sizeof(uv_work_t));
wrapper->data = work;
uv_queue_work(loop, wrapper, _punchServerOnUvThread, _afterPunchServer);
return true;
}
bool DelayNoMore::UdpSession::upsertPeerUdpAddr(struct PeerAddr* newPeerAddrList, int roomCapacity, int selfJoinIndex) {
CCLOG("upsertPeerUdpAddr called by js for roomCapacity=%d, selfJoinIndex=%d.", roomCapacity, selfJoinIndex);
// Punching between existing peer-pairs for Address/Port-restricted Cone NAT (not need for Full Cone NAT); UvThread never writes into "peerAddrList", so I assume that it's safe to skip locking for them
for (int i = 0; i < roomCapacity; i++) {
if (i == selfJoinIndex - 1) continue;
peerAddrList[i].sockAddrIn = (*(newPeerAddrList + i)).sockAddrIn;
peerAddrList[i].authKey = (*(newPeerAddrList + i)).authKey;
}
PunchPeerWork* work = new PunchPeerWork(roomCapacity, selfJoinIndex);
uv_work_t* wrapper = (uv_work_t*)malloc(sizeof(uv_work_t));
wrapper->data = work;
uv_queue_work(loop, wrapper, _startPunchPeerTimerOnUvThread, _afterPunchPeerTimerStarted);
return true;
}
bool DelayNoMore::UdpSession::broadcastInputFrameUpsync(BYTEC* const bytes, size_t bytesLen, int roomCapacity, int selfJoinIndex) {
BroadcastInputFrameUpsyncWork* work = new BroadcastInputFrameUpsyncWork(bytes, bytesLen, roomCapacity, selfJoinIndex);
uv_work_t* wrapper = (uv_work_t*)malloc(sizeof(uv_work_t));
wrapper->data = work;
uv_queue_work(loop, wrapper, _broadcastInputFrameUpsyncOnUvThread, _afterBroadcastInputFrameUpsync);
return true;
}

View File

@@ -0,0 +1,28 @@
#include "uv/uv.h"
#define __SSIZE_T // Otherwise "ssize_t" would have conflicting macros error that stops compiling
#ifndef udp_session_hpp
#define udp_session_hpp
typedef char BYTEC;
typedef char const CHARC;
int const maxUdpPayloadBytes = 128;
int const maxPeerCnt = 10;
struct PeerAddr {
struct sockaddr_in sockAddrIn;
uint32_t authKey;
};
namespace DelayNoMore {
class UdpSession {
public:
static bool openUdpSession(int port);
static bool closeUdpSession();
static bool upsertPeerUdpAddr(struct PeerAddr* newPeerAddrList, int roomCapacity, int selfJoinIndex);
//static bool clearPeerUDPAddrList();
static bool punchToServer(CHARC* const srvIp, int const srvPort, BYTEC* const bytes, size_t bytesLen, int const udpTunnelSrvPort, BYTEC* const udpTunnelBytes, size_t udpTunnelBytesBytesLen);
static bool broadcastInputFrameUpsync(BYTEC* const bytes, size_t bytesLen, int roomCapacity, int selfJoinIndex);
};
}
#endif

View File

@@ -0,0 +1,171 @@
#include "udp_session.hpp"
#include "base/ccMacros.h"
#include "scripting/js-bindings/manual/jsb_conversions.hpp"
bool openUdpSession(se::State& s) {
const auto& args = s.args();
size_t argc = args.size();
CC_UNUSED bool ok = true;
if (1 == argc && args[0].isNumber()) {
SE_PRECONDITION2(ok, false, "openUdpSession: Error processing arguments");
int port = args[0].toInt32();
return DelayNoMore::UdpSession::openUdpSession(port);
}
SE_REPORT_ERROR("wrong number of arguments: %d, was expecting %d; or wrong arg type!", (int)argc, 1);
return false;
}
SE_BIND_FUNC(openUdpSession)
bool punchToServer(se::State& s) {
const auto& args = s.args();
size_t argc = args.size();
CC_UNUSED bool ok = true;
if (5 == argc && args[0].isString() && args[1].isNumber() && args[2].isObject() && args[2].toObject()->isTypedArray()
&& args[3].isNumber() && args[4].isObject() && args[4].toObject()->isTypedArray()
) {
SE_PRECONDITION2(ok, false, "punchToServer: Error processing arguments");
CHARC* srvIp = args[0].toString().c_str();
int srvPort = args[1].toInt32();
BYTEC bytes[maxUdpPayloadBytes];
memset(bytes, 0, sizeof bytes);
se::Object* obj = args[2].toObject();
size_t sz = 0;
uint8_t* ptr = NULL;
obj->getTypedArrayData(&ptr, &sz);
for (size_t i = 0; i < sz; i++) {
bytes[i] = (char)(*(ptr + i));
}
int udpTunnelSrvPort = args[3].toInt32();
BYTEC udpTunnelBytes[maxUdpPayloadBytes];
memset(udpTunnelBytes, 0, sizeof udpTunnelBytes);
se::Object* udpTunnelObj = args[4].toObject();
size_t udpTunnelSz = 0;
uint8_t* udpTunnelPtr = NULL;
udpTunnelObj->getTypedArrayData(&udpTunnelPtr, &udpTunnelSz);
for (size_t i = 0; i < udpTunnelSz; i++) {
udpTunnelBytes[i] = (char)(*(udpTunnelPtr + i));
}
CCLOG("Should punch %s:%d by %d bytes; should punch udp tunnel %s:%d by %d bytes.", srvIp, srvPort, sz, srvIp, udpTunnelSrvPort, udpTunnelSz);
return DelayNoMore::UdpSession::punchToServer(srvIp, srvPort, bytes, sz, udpTunnelSrvPort, udpTunnelBytes, udpTunnelSz);
}
SE_REPORT_ERROR("wrong number of arguments: %d, was expecting %d; or wrong arg type!", (int)argc, 5);
return false;
}
SE_BIND_FUNC(punchToServer)
bool broadcastInputFrameUpsync(se::State& s) {
const auto& args = s.args();
size_t argc = args.size();
CC_UNUSED bool ok = true;
if (3 == argc && args[0].toObject()->isTypedArray() && args[1].isNumber() && args[2].isNumber()) {
SE_PRECONDITION2(ok, false, "broadcastInputFrameUpsync: Error processing arguments");
BYTEC bytes[1024];
memset(bytes, 0, sizeof bytes);
se::Object* obj = args[0].toObject();
size_t sz = 0;
uint8_t* ptr = NULL;
obj->getTypedArrayData(&ptr, &sz);
for (size_t i = 0; i < sz; i++) {
bytes[i] = (char)(*(ptr + i));
}
int roomCapacity = args[1].toInt32();
int selfJoinIndex = args[2].toInt32();
CCLOG("Should broadcastInputFrameUpsync %u bytes; roomCapacity=%d, selfJoinIndex=%d.", sz, roomCapacity, selfJoinIndex);
return DelayNoMore::UdpSession::broadcastInputFrameUpsync(bytes, sz, roomCapacity, selfJoinIndex);
}
SE_REPORT_ERROR("wrong number of arguments: %d, was expecting %d; or wrong arg type!", (int)argc, 3);
return false;
}
SE_BIND_FUNC(broadcastInputFrameUpsync)
bool closeUdpSession(se::State& s) {
const auto& args = s.args();
size_t argc = args.size();
CC_UNUSED bool ok = true;
if (0 == argc) {
SE_PRECONDITION2(ok, false, "closeUdpSession: Error processing arguments");
return DelayNoMore::UdpSession::closeUdpSession();
}
SE_REPORT_ERROR("wrong number of arguments: %d, was expecting %d", (int)argc, 0);
return false;
}
SE_BIND_FUNC(closeUdpSession)
struct PeerAddr newPeerAddrList[maxPeerCnt];
bool upsertPeerUdpAddr(se::State& s) {
const auto& args = s.args();
size_t argc = args.size();
CC_UNUSED bool ok = true;
if (3 == argc && args[0].isObject() && args[0].toObject()->isArray() && args[1].isNumber() && args[2].isNumber()) {
SE_PRECONDITION2(ok, false, "upsertPeerUdpAddr: Error processing arguments");
int roomCapacity = args[1].toInt32();
int selfJoinIndex = args[2].toInt32();
se::Object* newPeerAddrValArr = args[0].toObject();
for (int i = 0; i < roomCapacity; i++) {
se::Value newPeerAddrVal;
newPeerAddrValArr->getArrayElement(i, &newPeerAddrVal);
se::Object* newPeerAddrObj = newPeerAddrVal.toObject();
se::Value newIp, newPort, newAuthKey;
newPeerAddrObj->getProperty("ip", &newIp);
newPeerAddrObj->getProperty("port", &newPort);
newPeerAddrObj->getProperty("authKey", &newAuthKey);
uv_ip4_addr(newIp.toString().c_str(), newPort.toInt32(), &(newPeerAddrList[i].sockAddrIn));
newPeerAddrList[i].authKey = newAuthKey.toInt32();
}
return DelayNoMore::UdpSession::upsertPeerUdpAddr(newPeerAddrList, roomCapacity, selfJoinIndex);
}
SE_REPORT_ERROR("wrong number of arguments: %d, was expecting %d; or wrong arg type!", (int)argc, 3);
return false;
}
SE_BIND_FUNC(upsertPeerUdpAddr)
static bool udpSessionFinalize(se::State& s)
{
CCLOGINFO("jsbindings: finalizing JS object %p (DelayNoMore::UdpSession)", s.nativeThisObject());
auto iter = se::NonRefNativePtrCreatedByCtorMap::find(s.nativeThisObject());
if (iter != se::NonRefNativePtrCreatedByCtorMap::end()) {
se::NonRefNativePtrCreatedByCtorMap::erase(iter);
DelayNoMore::UdpSession* cobj = (DelayNoMore::UdpSession*)s.nativeThisObject();
delete cobj;
}
return true;
}
SE_BIND_FINALIZE_FUNC(udpSessionFinalize)
se::Object* __jsb_udp_session_proto = nullptr;
se::Class* __jsb_udp_session_class = nullptr;
bool registerUdpSession(se::Object* obj)
{
// Get the ns
se::Value nsVal;
if (!obj->getProperty("DelayNoMore", &nsVal))
{
se::HandleObject jsobj(se::Object::createPlainObject());
nsVal.setObject(jsobj);
obj->setProperty("DelayNoMore", nsVal);
}
se::Object* ns = nsVal.toObject();
auto cls = se::Class::create("UdpSession", ns, nullptr, nullptr);
cls->defineStaticFunction("openUdpSession", _SE(openUdpSession));
cls->defineStaticFunction("punchToServer", _SE(punchToServer));
cls->defineStaticFunction("broadcastInputFrameUpsync", _SE(broadcastInputFrameUpsync));
cls->defineStaticFunction("closeUdpSession", _SE(closeUdpSession));
cls->defineStaticFunction("upsertPeerUdpAddr", _SE(upsertPeerUdpAddr));
cls->defineFinalizeFunction(_SE(udpSessionFinalize));
cls->install();
JSBClassType::registerClass<DelayNoMore::UdpSession>(cls);
__jsb_udp_session_proto = cls->getProto();
__jsb_udp_session_class = cls;
se::ScriptEngine::getInstance()->clearException();
return true;
}

View File

@@ -0,0 +1,19 @@
#ifndef udp_session_bridge_hpp
#define udp_session_bridge_hpp
#pragma once
#include "base/ccConfig.h"
#include "cocos/scripting/js-bindings/jswrapper/SeApi.h"
extern se::Object* __jsb_udp_session_proto;
extern se::Class* __jsb_udp_session_class;
bool registerUdpSession(se::Object* obj);
SE_DECLARE_FUNC(openUdpSession);
SE_DECLARE_FUNC(punchToServer);
SE_DECLARE_FUNC(broadcastInputFrameUpsync);
SE_DECLARE_FUNC(closeUdpSession);
SE_DECLARE_FUNC(upsertPeerUdpAddr);
#endif

View File

@@ -0,0 +1,40 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.genxium.delaynomore"
android:installLocation="auto">
<uses-feature android:glEsVersion="0x00020000" />
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
<!-- TODO: Remove "usesCleartextTraffic" in production -->
<application
android:allowBackup="true"
android:label="@string/app_name"
android:usesCleartextTraffic="true"
android:icon="@mipmap/ic_launcher">
<!-- Tell Cocos2dxActivity the name of our .so -->
<meta-data android:name="android.app.lib_name"
android:value="cocos2djs" />
<activity
android:name="org.cocos2dx.javascript.AppActivity"
android:screenOrientation="sensorLandscape"
android:configChanges="orientation|keyboardHidden|screenSize"
android:label="@string/app_name"
android:usesCleartextTraffic="true"
android:theme="@android:style/Theme.NoTitleBar.Fullscreen"
android:launchMode="singleTask"
android:taskAffinity="" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@@ -0,0 +1,25 @@
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := cocos2djs_shared
LOCAL_MODULE_FILENAME := libcocos2djs
ifeq ($(USE_ARM_MODE),1)
LOCAL_ARM_MODE := arm
endif
LOCAL_SRC_FILES := hellojavascript/main.cpp \
../../../Classes/AppDelegate.cpp \
../../../Classes/jsb_module_register.cpp \
../../../Classes/udp_session.cpp \
../../../Classes/udp_session_bridge.cpp \
LOCAL_C_INCLUDES := $(LOCAL_PATH)/../../../Classes
LOCAL_STATIC_LIBRARIES := cocos2dx_static
include $(BUILD_SHARED_LIBRARY)
$(call import-module, cocos)

View File

@@ -0,0 +1,119 @@
/****************************************************************************
Copyright (c) 2015-2016 Chukong Technologies Inc.
Copyright (c) 2017-2018 Xiamen Yaji Software Co., Ltd.
http://www.cocos2d-x.org
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
****************************************************************************/
package org.cocos2dx.javascript;
import android.content.Intent;
import android.content.res.Configuration;
import android.os.Bundle;
import org.cocos2dx.lib.Cocos2dxActivity;
import org.cocos2dx.lib.Cocos2dxGLSurfaceView;
public class AppActivity extends Cocos2dxActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Workaround in https://stackoverflow.com/questions/16283079/re-launch-of-activity-on-home-button-but-only-the-first-time/16447508
if (!isTaskRoot()) {
// Android launched another instance of the root activity into an existing task
// so just quietly finish and go away, dropping the user back into the activity
// at the top of the stack (ie: the last state of this task)
// Don't need to finish it again since it's finished in super.onCreate .
return;
}
// DO OTHER INITIALIZATION BELOW
}
@Override
public Cocos2dxGLSurfaceView onCreateView() {
Cocos2dxGLSurfaceView glSurfaceView = new Cocos2dxGLSurfaceView(this);
// TestCpp should create stencil buffer
glSurfaceView.setEGLConfigChooser(5, 6, 5, 0, 16, 8);
return glSurfaceView;
}
@Override
protected void onResume() {
super.onResume();
}
@Override
protected void onPause() {
super.onPause();
}
@Override
protected void onDestroy() {
super.onDestroy();
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
}
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
}
@Override
protected void onRestart() {
super.onRestart();
}
@Override
protected void onStop() {
super.onStop();
}
@Override
public void onBackPressed() {
super.onBackPressed();
}
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
}
@Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
}
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
}
@Override
protected void onStart() {
super.onStart();
}
}

View File

@@ -0,0 +1,30 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.2.0'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
allprojects {
repositories {
google()
jcenter()
flatDir {
dirs 'libs'
}
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}

View File

@@ -0,0 +1,210 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Debug|Win32">
<Configuration>Debug</Configuration>
<Platform>Win32</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|Win32">
<Configuration>Release</Configuration>
<Platform>Win32</Platform>
</ProjectConfiguration>
</ItemGroup>
<PropertyGroup Label="Globals">
<ProjectGuid>{3B0B58B1-2734-488E-A542-ECEC11EB2455}</ProjectGuid>
<RootNamespace>DelayNoMore</RootNamespace>
<WindowsTargetPlatformVersion>8.1</WindowsTargetPlatformVersion>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<CharacterSet>Unicode</CharacterSet>
<PlatformToolset Condition="'$(VisualStudioVersion)' == '12.0'">v120</PlatformToolset>
<PlatformToolset Condition="'$(VisualStudioVersion)' == '12.0' and exists('$(MSBuildProgramFiles32)\Microsoft SDKs\Windows\v7.1A')">v120_xp</PlatformToolset>
<PlatformToolset Condition="'$(VisualStudioVersion)' == '14.0'">v140</PlatformToolset>
<PlatformToolset Condition="'$(VisualStudioVersion)' == '14.0' and exists('$(MSBuildProgramFiles32)\Microsoft SDKs\Windows\v7.1A')">v140_xp</PlatformToolset>
<PlatformToolset Condition="'$(VisualStudioVersion)' == '15.0'">v141</PlatformToolset>
<PlatformToolset Condition="'$(VisualStudioVersion)' == '15.0' and exists('$(MSBuildProgramFiles32)\Microsoft SDKs\Windows\v7.1A')">v140_xp</PlatformToolset>
<PlatformToolset>v140</PlatformToolset>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<CharacterSet>Unicode</CharacterSet>
<PlatformToolset Condition="'$(VisualStudioVersion)' == '12.0'">v120</PlatformToolset>
<PlatformToolset Condition="'$(VisualStudioVersion)' == '12.0' and exists('$(MSBuildProgramFiles32)\Microsoft SDKs\Windows\v7.1A')">v120_xp</PlatformToolset>
<PlatformToolset Condition="'$(VisualStudioVersion)' == '14.0'">v140</PlatformToolset>
<PlatformToolset Condition="'$(VisualStudioVersion)' == '14.0' and exists('$(MSBuildProgramFiles32)\Microsoft SDKs\Windows\v7.1A')">v140_xp</PlatformToolset>
<PlatformToolset Condition="'$(VisualStudioVersion)' == '15.0'">v141</PlatformToolset>
<PlatformToolset Condition="'$(VisualStudioVersion)' == '15.0' and exists('$(MSBuildProgramFiles32)\Microsoft SDKs\Windows\v7.1A')">v140_xp</PlatformToolset>
<PlatformToolset>v140</PlatformToolset>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<ImportGroup Label="ExtensionSettings">
</ImportGroup>
<ImportGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'" Label="PropertySheets">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
<Import Project="C:\CocosCreator_2.2.1\resources\cocos2d-x\build\cocos2dx.props" />
<Import Project="C:\CocosCreator_2.2.1\resources\cocos2d-x\build\cocos2d_headers.props" />
</ImportGroup>
<ImportGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'" Label="PropertySheets">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
<Import Project="C:\CocosCreator_2.2.1\resources\cocos2d-x\build\cocos2dx.props" />
<Import Project="C:\CocosCreator_2.2.1\resources\cocos2d-x\build\cocos2d_headers.props" />
</ImportGroup>
<PropertyGroup Label="UserMacros" />
<PropertyGroup>
<_ProjectFileVersion>12.0.21005.1</_ProjectFileVersion>
<OutDir Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">$(SolutionDir)$(Configuration).win32\</OutDir>
<IntDir Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">$(Configuration).win32\</IntDir>
<LinkIncremental Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">false</LinkIncremental>
<OutDir Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">$(SolutionDir)$(Configuration).win32\</OutDir>
<IntDir Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">$(Configuration).win32\</IntDir>
<LinkIncremental Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">false</LinkIncremental>
<CodeAnalysisRuleSet Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">AllRules.ruleset</CodeAnalysisRuleSet>
<CodeAnalysisRules Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'" />
<CodeAnalysisRuleAssemblies Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'" />
<CodeAnalysisRuleSet Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">AllRules.ruleset</CodeAnalysisRuleSet>
<CodeAnalysisRules Condition="'$(Configuration)|$(Platform)'=='Release|Win32'" />
<CodeAnalysisRuleAssemblies Condition="'$(Configuration)|$(Platform)'=='Release|Win32'" />
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
<LibraryPath>$(MSBuildProgramFiles32)\Microsoft SDKs\Windows\v7.1A\lib;$(LibraryPath)</LibraryPath>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
<LibraryPath>$(MSBuildProgramFiles32)\Microsoft SDKs\Windows\v7.1A\lib;$(LibraryPath)</LibraryPath>
</PropertyGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
<Midl>
<PreprocessorDefinitions>_DEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<MkTypLibCompatible>false</MkTypLibCompatible>
<TargetEnvironment>Win32</TargetEnvironment>
<GenerateStublessProxies>true</GenerateStublessProxies>
<TypeLibraryName>$(IntDir)game.tlb</TypeLibraryName>
<HeaderFileName>game.h</HeaderFileName>
<DllDataFileName>
</DllDataFileName>
<InterfaceIdentifierFileName>game_i.c</InterfaceIdentifierFileName>
<ProxyFileName>game_p.c</ProxyFileName>
</Midl>
<ClCompile>
<Optimization>Disabled</Optimization>
<AdditionalIncludeDirectories>$(ProjectDir)..\Classes;$(EngineRoot);$(EngineRoot)cocos;$(EngineRoot)cocos\base;$(EngineRoot)cocos\scripting\js-bindings\auto;$(EngineRoot)cocos\scripting\js-bindings\manual;$(EngineRoot)cocos\audio\include;$(EngineRoot)external\win32\include\;$(EngineRoot)external\win32\include\v8;$(EngineRoot)extensions;$(EngineRoot)cocos\editor-support;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<PreprocessorDefinitions>WIN32;_WINDOWS;STRICT;_DEBUG;XP_WIN;JS_HAVE___INTN;JS_INTPTR_TYPE=int;COCOS2D_DEBUG=1;_CRT_SECURE_NO_WARNINGS;_SCL_SECURE_NO_WARNINGS;_USRLIBSIMSTATIC;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<MinimalRebuild>false</MinimalRebuild>
<BasicRuntimeChecks>EnableFastChecks</BasicRuntimeChecks>
<RuntimeLibrary>MultiThreadedDebugDLL</RuntimeLibrary>
<PrecompiledHeader>
</PrecompiledHeader>
<WarningLevel>Level3</WarningLevel>
<DebugInformationFormat>EditAndContinue</DebugInformationFormat>
<DisableSpecificWarnings>4267;4251;4244;4800;%(DisableSpecificWarnings)</DisableSpecificWarnings>
<MultiProcessorCompilation>true</MultiProcessorCompilation>
</ClCompile>
<ResourceCompile>
<PreprocessorDefinitions>_DEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<Culture>0x0409</Culture>
<AdditionalIncludeDirectories>$(MSBuildProgramFiles32)\Microsoft SDKs\Windows\v7.1A\include;$(IntDir);%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
</ResourceCompile>
<PreLinkEvent>
<Command>
</Command>
</PreLinkEvent>
<Link>
<AdditionalDependencies>v8.dll.lib;v8_libbase.dll.lib;v8_libplatform.dll.lib;libuv.lib;%(AdditionalDependencies)</AdditionalDependencies>
<AdditionalLibraryDirectories>$(OutDir);%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
<GenerateDebugInformation>true</GenerateDebugInformation>
<SubSystem>Windows</SubSystem>
<TargetMachine>MachineX86</TargetMachine>
</Link>
<PreBuildEvent>
<Command>
</Command>
</PreBuildEvent>
<PreBuildEvent>
<Command>
</Command>
</PreBuildEvent>
</ItemDefinitionGroup>
<ItemDefinitionGroup>
<CustomBuildStep>
<Command>xcopy "$(ProjectDir)..\..\..\src" "$(OutDir)\src" /D /E /I /F /Y
xcopy "$(ProjectDir)..\..\..\res" "$(OutDir)\res" /D /E /I /F /Y
xcopy "$(ProjectDir)..\..\..\jsb-adapter" "$(OutDir)\jsb-adapter" /D /E /I /F /Y
copy "$(ProjectDir)..\..\..\main.js" "$(OutDir)\" /Y
copy "$(ProjectDir)..\..\..\project.json" "$(OutDir)\" /Y</Command>
<Outputs>$(TargetName).cab</Outputs>
<Inputs>$(TargetFileName)</Inputs>
</CustomBuildStep>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
<Midl>
<PreprocessorDefinitions>NDEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<MkTypLibCompatible>false</MkTypLibCompatible>
<TargetEnvironment>Win32</TargetEnvironment>
<GenerateStublessProxies>true</GenerateStublessProxies>
<TypeLibraryName>$(IntDir)game.tlb</TypeLibraryName>
<HeaderFileName>game.h</HeaderFileName>
<DllDataFileName>
</DllDataFileName>
<InterfaceIdentifierFileName>game_i.c</InterfaceIdentifierFileName>
<ProxyFileName>game_p.c</ProxyFileName>
</Midl>
<ClCompile>
<AdditionalIncludeDirectories>$(ProjectDir)..\Classes;$(EngineRoot);$(EngineRoot)cocos;$(EngineRoot)cocos\base;$(EngineRoot)cocos\scripting\js-bindings\auto;$(EngineRoot)cocos\scripting\js-bindings\manual;$(EngineRoot)cocos\audio\include;$(EngineRoot)external\win32\include\;$(EngineRoot)external\win32\include\v8;$(EngineRoot)extensions;$(EngineRoot)cocos\editor-support;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<PreprocessorDefinitions>WIN32;_WINDOWS;STRICT;NDEBUG;XP_WIN;JS_HAVE___INTN;JS_INTPTR_TYPE=int;_CRT_SECURE_NO_WARNINGS;_SCL_SECURE_NO_WARNINGS;_USRLIBSIMSTATIC;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ExceptionHandling>
</ExceptionHandling>
<RuntimeLibrary>MultiThreadedDLL</RuntimeLibrary>
<PrecompiledHeader>
</PrecompiledHeader>
<WarningLevel>Level3</WarningLevel>
<DebugInformationFormat>
</DebugInformationFormat>
<DisableSpecificWarnings>4267;4251;4244;4800;%(DisableSpecificWarnings)</DisableSpecificWarnings>
<MultiProcessorCompilation>true</MultiProcessorCompilation>
</ClCompile>
<ResourceCompile>
<PreprocessorDefinitions>NDEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<Culture>0x0409</Culture>
<AdditionalIncludeDirectories>$(MSBuildProgramFiles32)\Microsoft SDKs\Windows\v7.1A\include;$(IntDir);%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
</ResourceCompile>
<PreLinkEvent>
<Command>
</Command>
</PreLinkEvent>
<Link>
<AdditionalDependencies>v8.dll.lib;v8_libbase.dll.lib;v8_libplatform.dll.lib;libuv.lib;%(AdditionalDependencies)</AdditionalDependencies>
<AdditionalLibraryDirectories>$(OutDir);%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
<SubSystem>Windows</SubSystem>
<TargetMachine>MachineX86</TargetMachine>
<GenerateDebugInformation>true</GenerateDebugInformation>
</Link>
<PreBuildEvent>
<Command>
</Command>
</PreBuildEvent>
</ItemDefinitionGroup>
<ItemGroup>
<ClCompile Include="..\Classes\jsb_module_register.cpp" />
<ClCompile Include="main.cpp" />
<ClCompile Include="..\Classes\AppDelegate.cpp" />
<ClCompile Include="..\Classes\udp_session.cpp" />
<ClCompile Include="..\Classes\udp_session_bridge.cpp" />
</ItemGroup>
<ItemGroup>
<ClInclude Include="main.h" />
<ClInclude Include="..\Classes\udp_session.hpp" />
<ClInclude Include="..\Classes\udp_session_bridge.hpp" />
<ClInclude Include="..\Classes\AppDelegate.h" />
<ClInclude Include="resource.h" />
</ItemGroup>
<ItemGroup>
<ResourceCompile Include="game.rc" />
</ItemGroup>
<ItemGroup>
<Image Include="res\game.ico" />
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<ImportGroup Label="ExtensionTargets">
</ImportGroup>
</Project>

View File

@@ -0,0 +1,57 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<Filter Include="resource">
<UniqueIdentifier>{ca9c9e15-d942-43a1-aa7a-5f0b74ca1afd}</UniqueIdentifier>
<Extensions>rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;jpg;jpeg;jpe;png;manifest</Extensions>
</Filter>
<Filter Include="win32">
<UniqueIdentifier>{ccb2323b-1cfa-41ea-bcf4-ba5f07309396}</UniqueIdentifier>
</Filter>
<Filter Include="Classes">
<UniqueIdentifier>{e93a77e1-af1e-4400-87d3-504b62ebdbb0}</UniqueIdentifier>
</Filter>
</ItemGroup>
<ItemGroup>
<ClCompile Include="main.cpp">
<Filter>win32</Filter>
</ClCompile>
<ClCompile Include="..\Classes\AppDelegate.cpp">
<Filter>Classes</Filter>
</ClCompile>
<ClCompile Include="..\Classes\jsb_module_register.cpp">
<Filter>Classes</Filter>
</ClCompile>
<ClCompile Include="..\Classes\udp_session.cpp">
<Filter>Classes</Filter>
</ClCompile>
<ClCompile Include="..\Classes\udp_session_bridge.cpp">
<Filter>Classes</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ClInclude Include="..\Classes\AppDelegate.h">
<Filter>Classes</Filter>
</ClInclude>
<ClInclude Include="main.h">
<Filter>win32</Filter>
</ClInclude>
<ClInclude Include="resource.h" />
<ClInclude Include="..\Classes\udp_session.hpp">
<Filter>Classes</Filter>
</ClInclude>
<ClInclude Include="..\Classes\udp_session_bridge.hpp">
<Filter>Classes</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<ResourceCompile Include="game.rc">
<Filter>resource</Filter>
</ResourceCompile>
</ItemGroup>
<ItemGroup>
<Image Include="res\game.ico">
<Filter>resource</Filter>
</Image>
</ItemGroup>
</Project>

View File

@@ -2,7 +2,7 @@
"android-instant": {
"REMOTE_SERVER_ROOT": "",
"host": "",
"packageName": "org.cocos2d.helloworld",
"packageName": "org.genxium.delaynomore",
"pathPattern": "",
"recordPath": "",
"scheme": "https",
@@ -15,14 +15,14 @@
"orientation": "portrait",
"subContext": ""
},
"encryptJs": true,
"encryptJs": false,
"excludeScenes": [
"8491a86c-bec9-4813-968a-128ca01639e0"
],
"fb-instant-games": {},
"includeSDKBox": false,
"inlineSpriteFrames": true,
"inlineSpriteFrames_native": true,
"inlineSpriteFrames_native": false,
"md5Cache": false,
"mergeStartScene": true,
"optimizeHotUpdate": false,
@@ -32,14 +32,14 @@
"portrait": false,
"upsideDown": false
},
"packageName": "org.cocos2d.helloworld",
"packageName": "org.genxium.delaynomore",
"qqplay": {
"REMOTE_SERVER_ROOT": "",
"orientation": "portrait"
},
"startScene": "2ff474d9-0c9e-4fe3-87ec-fbff7cae85b4",
"title": "DelayNoMore",
"webOrientation": "portrait",
"webOrientation": "landscape",
"wechatgame": {
"REMOTE_SERVER_ROOT": "https://bgmoba.lokcol.com/static/",
"appid": "wxf497c910a2a25edc",
@@ -48,13 +48,14 @@
"xxteaKey": "4d54a3d5-e6f3-49",
"zipCompressJs": true,
"android": {
"packageName": "org.cocos2d.helloworld"
"packageName": "org.genxium.delaynomore"
},
"ios": {
"packageName": "org.cocos2d.helloworld"
"packageName": "org.genxium.delaynomore"
},
"mac": {
"packageName": "org.cocos2d.helloworld"
"packageName": "org.genxium.delaynomore"
},
"win32": {}
"win32": {},
"includeAnySDK": false
}

View File

@@ -1,7 +1,7 @@
{
"excludeScenes": [],
"packageName": "org.cocos2d.helloworld",
"platform": "web-mobile",
"packageName": "org.genxium.delaynomore",
"platform": "android",
"startScene": "2d2f792f-a40c-49bb-a189-ed176a246e49",
"title": "HelloWorld"
}
"title": "DelayNoMore"
}

View File

@@ -34,9 +34,12 @@
"design-resolution-width": 960,
"excluded-modules": [
"Collider",
"DragonBones",
"Geom Utils",
"Intersection",
"Mesh",
"MotionStreak",
"NodePool",
"Physics",
"PageView",
"PageViewIndicator",
@@ -68,7 +71,7 @@
"shelter_z_reducer",
"shelter"
],
"last-module-event-record-time": 1673930863015,
"last-module-event-record-time": 1674632533161,
"simulator-orientation": false,
"simulator-resolution": {
"height": 640,

View File

@@ -1,5 +1,5 @@
{
"name": "TEMPLATES.helloworld.name",
"desc": "TEMPLATES.helloworld.desc",
"name": "DelayNoMore",
"desc": "DelayNoMore",
"banner": "template-banner.png"
}
}

View File

@@ -4,24 +4,25 @@ 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://goproxy.io
DST=../frontend/assets/scripts/modules
serve:
gopherjs serve $(PROJECTNAME)
clean:
gopherjs clean
rm -f ../frontend/assets/plugin_scripts/jsexport.js
#rm -f ../frontend/assets/plugin_scripts/jsexport.js.map
rm -f $(DST)/jsexport.js
#rm -f $(DST)/jsexport.js.map
build: clean
gopherjs build $(PROJECTNAME)
mv ./jsexport.js ../frontend/assets/plugin_scripts/
#mv ./jsexport.js.map ../frontend/assets/plugin_scripts/
mv ./jsexport.js $(DST)/
#mv ./jsexport.js.map $(DST)/
build-min: clean
gopherjs build -m $(PROJECTNAME)
mv ./jsexport.js ../frontend/assets/plugin_scripts/
#mv ./jsexport.js.map ../frontend/assets/plugin_scripts/
mv ./jsexport.js $(DST)/
#mv ./jsexport.js.map $(DST)/
.PHONY: help

View File

@@ -22,7 +22,7 @@ const (
GRAVITY_X = int32(0)
GRAVITY_Y = -int32(float64(0.5) * WORLD_TO_VIRTUAL_GRID_RATIO) // makes all "playerCollider.Y" a multiple of 0.5 in all cases
INPUT_DELAY_FRAMES = int32(8) // in the count of render frames
INPUT_DELAY_FRAMES = int32(6) // in the count of render frames
INPUT_SCALE_FRAMES = uint32(2) // inputDelayedAndScaledFrameId = ((originalFrameId - InputDelayFrames) >> InputScaleFrames)
NST_DELAY_FRAMES = int32(16) // network-single-trip delay in the count of render frames, proposed to be (InputDelayFrames >> 1) because we expect a round-trip delay to be exactly "InputDelayFrames"
@@ -644,7 +644,7 @@ func ApplyInputFrameDownsyncDynamicsOnSingleRenderFrame(inputsBuffer *RingBuffer
if 0 == currPlayerDownsync.FramesToRecover {
prevCapturedByInertia := currPlayerDownsync.CapturedByInertia
isWallJumping := (currPlayerDownsync.Speed < intAbs(currPlayerDownsync.VelX))
isWallJumping := (chConfig.OnWallEnabled && chConfig.WallJumpingInitVelX == intAbs(currPlayerDownsync.VelX))
/*
if isWallJumping {
fmt.Printf("joinIndex=%d is wall jumping\n{renderFrame.id: %d, currPlayerDownsync.Speed: %d, currPlayerDownsync.VelX: %d}\n", currPlayerDownsync.JoinIndex, currRenderFrame.Id, currPlayerDownsync.Speed, currPlayerDownsync.VelX)
@@ -669,9 +669,11 @@ func ApplyInputFrameDownsyncDynamicsOnSingleRenderFrame(inputsBuffer *RingBuffer
*/
//fmt.Printf("joinIndex=%d is not wall jumping and not aligned w/ inertia\n{renderFrame.id: %d, effDx: %d, thatPlayerInNextFrame.VelX: %d}\n", currPlayerDownsync.JoinIndex, currRenderFrame.Id, effDx, thatPlayerInNextFrame.VelX)
thatPlayerInNextFrame.CapturedByInertia = true
thatPlayerInNextFrame.FramesToRecover = chConfig.InertiaFramesToRecover
if exactTurningAround {
thatPlayerInNextFrame.CharacterState = ATK_CHARACTER_STATE_TURNAROUND
thatPlayerInNextFrame.FramesToRecover = chConfig.InertiaFramesToRecover
} else {
thatPlayerInNextFrame.FramesToRecover = (chConfig.InertiaFramesToRecover >> 1)
}
} else {
thatPlayerInNextFrame.CapturedByInertia = false
@@ -752,6 +754,8 @@ func ApplyInputFrameDownsyncDynamicsOnSingleRenderFrame(inputsBuffer *RingBuffer
if ATK_CHARACTER_STATE_ONWALL == currPlayerDownsync.CharacterState && !jumpedOrNotList[i] {
thatPlayerInNextFrame.VelX += GRAVITY_X
thatPlayerInNextFrame.VelY = chConfig.WallSlidingVelY
} else if ATK_CHARACTER_STATE_DASHING == currPlayerDownsync.CharacterState {
thatPlayerInNextFrame.VelX += GRAVITY_X
} else {
thatPlayerInNextFrame.VelX += GRAVITY_X
thatPlayerInNextFrame.VelY += GRAVITY_Y

View File

@@ -49,7 +49,7 @@ var Characters = map[int]*CharacterConfig{
JumpingInitVelY: int32(float64(8) * WORLD_TO_VIRTUAL_GRID_RATIO),
JumpingFramesToRecover: int32(2),
InertiaFramesToRecover: int32(8),
InertiaFramesToRecover: int32(9),
DashingEnabled: false,
OnWallEnabled: false,
@@ -98,7 +98,7 @@ var Characters = map[int]*CharacterConfig{
JumpingInitVelY: int32(float64(7.5) * WORLD_TO_VIRTUAL_GRID_RATIO),
JumpingFramesToRecover: int32(2),
InertiaFramesToRecover: int32(8),
InertiaFramesToRecover: int32(9),
DashingEnabled: true,
OnWallEnabled: true,
@@ -154,7 +154,7 @@ var Characters = map[int]*CharacterConfig{
JumpingInitVelY: int32(float64(7.8) * WORLD_TO_VIRTUAL_GRID_RATIO),
JumpingFramesToRecover: int32(2),
InertiaFramesToRecover: int32(8),
InertiaFramesToRecover: int32(9),
DashingEnabled: false,
OnWallEnabled: false,
@@ -551,9 +551,9 @@ var skills = map[int]*Skill{
},
},
12: &Skill{
RecoveryFrames: int32(15),
RecoveryFramesOnBlock: int32(15),
RecoveryFramesOnHit: int32(15),
RecoveryFrames: int32(12),
RecoveryFramesOnBlock: int32(12),
RecoveryFramesOnHit: int32(12),
ReleaseTriggerType: int32(1),
BoundChState: ATK_CHARACTER_STATE_DASHING,
Hits: []interface{}{

39
udp_server_prac/main.go Normal file
View File

@@ -0,0 +1,39 @@
package main
import (
"fmt"
"net"
"strings"
)
func main() {
conn, err := net.ListenUDP("udp", &net.UDPAddr{
Port: 3000,
IP: net.ParseIP("0.0.0.0"),
})
if err != nil {
panic(err)
}
defer conn.Close()
fmt.Printf("server listening %s\n", conn.LocalAddr().String())
for {
message := make([]byte, 2046)
rlen, remote, err := conn.ReadFromUDP(message[:])
if err != nil {
panic(err)
}
data := strings.TrimSpace(string(message[:rlen]))
fmt.Printf("received: %d bytes, content=%s from %s\n", rlen, data, remote)
// echo
rlen, wrerr := conn.WriteTo(message[0:rlen], remote)
if wrerr != nil {
fmt.Printf("net.WriteTo() error: %s\n", wrerr)
} else {
fmt.Printf("Wrote %d bytes to socket\n", rlen)
}
}
}