Compare commits

..

3 Commits

Author SHA1 Message Date
genxium
365177a3af Renamed CPP files. 2023-02-10 11:38:21 +08:00
genxium
b79e2dc935 Enhanced UDP message callback handling. 2023-02-09 23:34:00 +08:00
genxium
7b0c807496 Drafted enhancement of UDP message callback. 2023-02-09 10:18:23 +08:00
17 changed files with 262 additions and 87 deletions

View File

@@ -6,9 +6,13 @@ This project is a demo for a websocket-based rollback netcode inspired by [GGPO]
![Merged_cut_annotated_spedup](./charts/Merged_cut_annotated_spedup.gif)
(battle between 2 celluar 4G users using Android phones, [original video here](https://pan.baidu.com/s/1RL-9M-cK8cFS_Q8afMTrJA?pwd=ryzv))
![Phone4g_battle_spedup](./charts/Phone4g_battle_spedup.gif)
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. This video shows how the UDP holepunched p2p performs for [Phone-Wifi v.s. PC-Wifi (viewed by PC side)](https://pan.baidu.com/s/1K6704bJKlrSBTVqGcXhajA?pwd=l7ok).
- 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 [Phone-4G v.s. PC-Wifi (viewed by PC side)](https://pan.baidu.com/s/1IZVa5wVgAdeH6D-xsZYFUw?pwd=dgkj).
- 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 [Phone-4G v.s. PC-Wifi (merged view@v0.9.34, excellent synchronization)](https://pan.baidu.com/s/1yeIrN5TSf6_av_8-N3vdVg?pwd=7tzw).
- Browser vs `native app` is possible but in that case only websocket is used.

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 MiB

View File

@@ -1084,6 +1084,9 @@ fromUDP=${fromUDP}`);
}
try {
let st = performance.now();
if (cc.sys.isNative) {
DelayNoMore.UdpSession.pollUdpRecvRingBuff();
}
const noDelayInputFrameId = gopkgs.ConvertToNoDelayInputFrameId(self.renderFrameId);
let prevSelfInput = null,
currSelfInput = null;

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,7 @@
{
"engine_version": "2.2.1",
"has_native": true,
"project_type": "js",
"projectName": "DelayNoMore",
"packageName": "org.genxium.delaynomore"
}

View File

@@ -18,11 +18,11 @@
"from": "cocos/scripting/js-bindings/manual/jsb_module_register.cpp",
"to": "frameworks/runtime-src/Classes/jsb_module_register.cpp"
}, {
"from": "frameworks/runtime-src/Classes/send_ring_buff.hpp",
"to": "frameworks/runtime-src/Classes/send_ring_buff.hpp"
"from": "frameworks/runtime-src/Classes/ring_buff.hpp",
"to": "frameworks/runtime-src/Classes/ring_buff.hpp"
}, {
"from": "frameworks/runtime-src/Classes/send_ring_buff.cpp",
"to": "frameworks/runtime-src/Classes/send_ring_buff.cpp"
"from": "frameworks/runtime-src/Classes/ring_buff.cpp",
"to": "frameworks/runtime-src/Classes/ring_buff.cpp"
}, {
"from": "frameworks/runtime-src/Classes/udp_session.hpp",
"to": "frameworks/runtime-src/Classes/udp_session.hpp"

View File

@@ -0,0 +1,102 @@
#include <string.h>
#include "ring_buff.hpp"
// Sending
void SendRingBuff::put(BYTEC* const newBytes, size_t newBytesLen, PeerAddr* pNewPeerAddr) {
while (0 < cnt && cnt >= n) {
// Make room for the new element
this->pop();
}
eles[ed].bytesLen = newBytesLen;
memset(eles[ed].bytes, 0, sizeof eles[ed].bytes);
memcpy(eles[ed].bytes, newBytes, newBytesLen);
eles[ed].peerAddr = *(pNewPeerAddr);
ed++;
cnt++;
if (ed >= n) {
ed -= n; // Deliberately not using "%" operator for performance concern
}
}
SendWork* SendRingBuff::pop() {
if (0 == cnt) {
return NULL;
}
SendWork* ret = &(eles[st]);
cnt--;
st++;
if (st >= n) {
st -= n;
}
return ret;
}
// Recving
void RecvRingBuff::put(char* newBytes, size_t newBytesLen) {
RecvWork* slotEle = (&eles[ed.load()]); // Save for later update
int oldCnt = cnt.load();
int oldSt = st.load(); // Used to guard against "cnt decremented in pop(...), but st not yet incremented and thus return value not yet copied to avoid contamination"
int tried = 0;
while (n <= oldCnt && !ed.compare_exchange_weak(oldSt, oldSt) && 3 > tried) {
// Make room for the new element
this->pop(NULL);
oldCnt = cnt.load(); // If "pop()" above failed, it'd only be due to concurrent calls to "pop()", either way the updated "cnt" should be good to go
oldSt = st.load();
++tried;
}
if (n <= oldCnt && !ed.compare_exchange_weak(oldSt, oldSt) && 3 == tried) {
// Failed silently, UDP packet can be dropped.
return;
}
slotEle->bytesLen = newBytesLen;
memset(slotEle->ui8Arr, 0, sizeof slotEle->ui8Arr);
for (int i = 0; i < newBytesLen; i++) {
*(slotEle->ui8Arr + i) = *(newBytes + i);
}
// No need to compare-and-swap, only "UvRecvThread" will access "RecvRingBuff.ed".
ed++;
if (ed >= n) {
ed -= n; // Deliberately not using "%" operator for performance concern
}
// Only increment cnt when the putting of new element is fully done.
cnt++;
}
bool RecvRingBuff::pop(RecvWork* out) {
int oldCnt = std::atomic_fetch_sub(&cnt, 1);
/*
[WARNING]
After here, two cases should be taken care of.
1. If "n == oldCnt", we need guard against "put" to avoid contaminating "ret" by the "putting".
2. If "0 >= oldCnt", we need guard against another "pop" to avoid over-popping.
*/
if (0 >= oldCnt) {
// "pop" could be accessed by either "GameThread/pollUdpRecvRingBuff" or "UvRecvThread/put", thus we should be proactively guard against concurrent popping while "1 == cnt"
++cnt;
return false;
}
// When concurrent "pop"s reach here, over-popping is definitely avoided.
int oldSt = st.load();
if (out) {
RecvWork* src = (&eles[oldSt]);
memset(out->ui8Arr, 0, sizeof out->ui8Arr);
memcpy(out->ui8Arr, src, src->bytesLen);
out->bytesLen = src->bytesLen;
}
int newSt = oldSt + 1;
if (newSt >= n) {
newSt -= n;
}
if (st.compare_exchange_weak(oldSt, newSt)) {
return true;
} else {
// Failed concurrent access should recover the "cnt"
++cnt;
return false;
}
}

View File

@@ -4,6 +4,8 @@
#include "uv/uv.h"
#define __SSIZE_T // Otherwise "ssize_t" would have conflicting macros error that stops compiling
#include <atomic>
int const RING_BUFF_CONSECUTIVE_SET = 0;
int const RING_BUFF_NON_CONSECUTIVE_SET = 1;
int const RING_BUFF_FAILED_TO_SET = 2;
@@ -41,4 +43,32 @@ public:
SendWork* pop();
};
// TODO: Move "RecvXxxx" to a dedicated class.
class RecvWork {
public:
uint8_t ui8Arr[maxUdpPayloadBytes]; // Wasting some RAM here thus no need for explicit recursive destruction
size_t bytesLen;
};
/*
[WARNING] This class is specific to "RecvWork"; its "put" and "pop" methods are designed to be thread-safe & lock-free for our particular case, i.e. only concurrent access from "UvRecvThread" & "GameThread", in a sense more sophisticated than the Golang or JavaScript versions.
There's yet no plan to support thread-safe & lock-free "getByFrameId/setByFrameId" -- being thread-safe is easy by use of mutex, which is very SLOWWWWW when used in 60fps race-conditions.
The generic "thread-safe, lock-free ring buffer or circular buffer" is a big problem, widely discussed over the internet and in literatures, search "lock-free circular buffer" for more information.
*/
class RecvRingBuff {
public:
int n;
std::atomic_int ed, st, cnt;
RecvWork eles[maxBuffedMsgs]; // preallocated on stack to save heap alloc/dealloc time
RecvRingBuff(int newN) {
this->n = newN;
this->st = this->ed = this->cnt = 0;
}
void put(char* newBytes, size_t newBytesLen);
bool pop(RecvWork* out);
};
#endif

View File

@@ -1,31 +0,0 @@
#include <string.h>
#include "send_ring_buff.hpp"
void SendRingBuff::put(BYTEC* const newBytes, size_t newBytesLen, PeerAddr* pNewPeerAddr) {
while (0 < cnt && cnt >= n) {
// Make room for the new element
this->pop();
}
eles[ed].bytesLen = newBytesLen;
memset(eles[ed].bytes, 0, sizeof eles[ed].bytes);
memcpy(eles[ed].bytes, newBytes, newBytesLen);
eles[ed].peerAddr = *(pNewPeerAddr);
ed++;
cnt++;
if (ed >= n) {
ed -= n; // Deliberately not using "%" operator for performance concern
}
}
SendWork* SendRingBuff::pop() {
if (0 == cnt) {
return NULL;
}
SendWork* ret = &(eles[st]);
cnt--;
st++;
if (st >= n) {
st -= n;
}
return ret;
}

View File

@@ -16,11 +16,15 @@ uv_loop_t *recvLoop = NULL, *sendLoop = NULL;
uv_mutex_t sendRingBuffLock; // used along with "uvSendLoopTriggerSig" as a "uv_cond_t"
SendRingBuff* sendRingBuff = NULL;
uv_mutex_t recvRingBuffLock;
RecvRingBuff* recvRingBuff = NULL;
char SRV_IP[256];
int SRV_PORT = 0;
int UDP_TUNNEL_SRV_PORT = 0;
struct PeerAddr udpPunchingServerAddr, udpTunnelAddr;
struct PeerAddr peerAddrList[maxPeerCnt];
bool peerPunchedMarks[maxPeerCnt];
void _onRead(uv_udp_t* req, ssize_t nread, uv_buf_t const* buf, struct sockaddr const* addr, unsigned flags) {
if (nread < 0) {
@@ -29,6 +33,8 @@ void _onRead(uv_udp_t* req, ssize_t nread, uv_buf_t const* buf, struct sockaddr
free(buf->base);
return;
}
struct sockaddr_in const* sockAddr = (struct sockaddr_in const*)addr;
#if defined(COCOS2D_DEBUG) && (COCOS2D_DEBUG > 0)
char ip[INET_ADDRSTRLEN];
memset(ip, 0, sizeof ip);
@@ -38,10 +44,8 @@ void _onRead(uv_udp_t* req, ssize_t nread, uv_buf_t const* buf, struct sockaddr
// 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 %u bytes from %s:%d", nread, ip, port);
break;
}
default:
@@ -53,37 +57,24 @@ void _onRead(uv_udp_t* req, ssize_t nread, uv_buf_t const* buf, struct sockaddr
#endif
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);
// Peer holepunching
for (int i = 0; i < maxPeerCnt; i++) {
if (peerAddrList[i].sockAddrIn.sin_addr.s_addr != sockAddr->sin_addr.s_addr) continue;
if (peerAddrList[i].sockAddrIn.sin_port != sockAddr->sin_port) continue;
peerPunchedMarks[i] = true;
#if defined(COCOS2D_DEBUG) && (COCOS2D_DEBUG > 0)
CCLOG("UDP received peer-holepunching from %s:%d", ip, port);
#endif
break;
}
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);
});
} else if (0 < nread) {
// Non-holepunching; the previously used "cocos2d::Application::getInstance()->getScheduler()->performFunctionInCocosThread(...)" approach was so non-deterministic in terms of the lag till GameThread actually recognizes this latest received packet due to scheduler uncertainty -- and was also heavier in RAM due to lambda usage
#if defined(COCOS2D_DEBUG) && (COCOS2D_DEBUG > 0)
CCLOG("UDP received %u bytes inputFrameUpsync from %s:%d", nread, ip, port);
#endif
//uv_mutex_lock(&recvRingBuffLock);
recvRingBuff->put(buf->base, nread);
//uv_mutex_unlock(&recvRingBuffLock);
}
free(buf->base);
@@ -169,6 +160,7 @@ void startRecvLoop(void* arg) {
int uvCloseRet = uv_loop_close(l);
CCLOG("UDP recv loop is closed in UvRecvThread, uvCloseRet=%d", uvCloseRet);
uv_mutex_destroy(&recvRingBuffLock);
}
void startSendLoop(void* arg) {
@@ -196,6 +188,10 @@ int initSendLoop(struct sockaddr const* pUdpAddr) {
}
uv_mutex_init(&sendRingBuffLock);
sendRingBuff = new SendRingBuff(maxBuffedMsgs);
uv_mutex_init(&recvRingBuffLock);
recvRingBuff = new RecvRingBuff(maxBuffedMsgs);
uv_async_init(sendLoop, &uvSendLoopStopSig, _onUvStopSig);
uv_async_init(sendLoop, &uvSendLoopTriggerSig, _onUvSthNewToSend);
@@ -222,6 +218,12 @@ bool DelayNoMore::UdpSession::openUdpSession(int port) {
struct sockaddr_in udpAddr;
uv_ip4_addr("0.0.0.0", port, &udpAddr);
struct sockaddr const* pUdpAddr = (struct sockaddr const*)&udpAddr;
memset(peerPunchedMarks, false, sizeof(peerPunchedMarks));
for (int i = 0; i < maxPeerCnt; i++) {
peerAddrList[i].authKey = -1; // hardcoded for now
memset((char*)&peerAddrList[i].sockAddrIn, 0, sizeof(peerAddrList[i].sockAddrIn));
}
/*
[WARNING] On Android, the libuv documentation of "UV_UDP_REUSEADDR" is true, i.e. only the socket that binds later on the same port will be triggered the recv callback; however on Windows, experiment shows that the exact reverse is true instead.
@@ -259,11 +261,8 @@ bool DelayNoMore::UdpSession::closeUdpSession() {
uv_thread_join(&recvTid);
free(udpRecvSocket);
free(recvLoop);
delete recvRingBuff;
for (int i = 0; i < maxPeerCnt; i++) {
peerAddrList[i].authKey = -1; // hardcoded for now
memset((char*)&peerAddrList[i].sockAddrIn, 0, sizeof(peerAddrList[i].sockAddrIn));
}
CCLOG("Closed udp session and dealloc all resources in GameThread...");
return true;
@@ -321,8 +320,7 @@ bool DelayNoMore::UdpSession::broadcastInputFrameUpsync(BYTEC* const bytes, size
uv_mutex_lock(&sendRingBuffLock);
// Might want to send several times for better arrival rate
for (int j = 0; j < broadcastUpsyncCnt; j++) {
// Send to room udp tunnel in case of hole punching failure
sendRingBuff->put(bytes, bytesLen, &udpTunnelAddr);
int peerPunchedCnt = 0;
for (int i = 0; i < roomCapacity; i++) {
if (i + 1 == selfJoinIndex) {
continue;
@@ -331,8 +329,17 @@ bool DelayNoMore::UdpSession::broadcastInputFrameUpsync(BYTEC* const bytes, size
// Peer addr not initialized
continue;
}
if (false == peerPunchedMarks[i]) {
// Not punched yet, save some bandwidth
continue;
}
sendRingBuff->put(bytes, bytesLen, &(peerAddrList[i]));
++peerPunchedCnt;
}
sendRingBuff->put(bytes, bytesLen, &(peerAddrList[i])); // Content hardcoded for now
if (peerPunchedCnt + 1 < roomCapacity) {
// Send to room udp tunnel in case of ANY hole punching failure
sendRingBuff->put(bytes, bytesLen, &udpTunnelAddr);
}
}
@@ -341,3 +348,37 @@ bool DelayNoMore::UdpSession::broadcastInputFrameUpsync(BYTEC* const bytes, size
return true;
}
bool DelayNoMore::UdpSession::pollUdpRecvRingBuff() {
// This function is called by GameThread 60 fps.
//uv_mutex_lock(&recvRingBuffLock);
while (true) {
RecvWork f;
bool res = recvRingBuff->pop(&f);
if (!res) return false;
// [WARNING] Declaring "AutoHandleScope" is critical here, otherwise "onUdpMessageCb.toObject()" wouldn't be recognized as a function of the ScriptEngine!
se::AutoHandleScope hs;
// [WARNING] Use of the "ScriptEngine" is only allowed in "GameThread a.k.a. CocosThread"!
se::Value onUdpMessageCb;
se::ScriptEngine::getInstance()->getGlobalObject()->getProperty("onUdpMessage", &onUdpMessageCb);
if (onUdpMessageCb.isObject() && onUdpMessageCb.toObject()->isFunction()) {
//CCLOG("UDP received %d bytes upsync -- 1", nread);
se::Object* const gameThreadMsg = se::Object::createTypedArray(se::Object::TypedArrayType::UINT8, f.ui8Arr, f.bytesLen);
//CCLOG("UDP received %d bytes upsync -- 2", nread);
se::ValueArray args = { se::Value(gameThreadMsg) };
// 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);
}
}
//uv_mutex_unlock(&recvRingBuffLock);
return true;
}

View File

@@ -1,7 +1,7 @@
#ifndef udp_session_hpp
#define udp_session_hpp
#include "send_ring_buff.hpp"
#include "ring_buff.hpp"
int const maxPeerCnt = 10;
@@ -14,6 +14,7 @@ namespace DelayNoMore {
//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);
static bool pollUdpRecvRingBuff();
};
}
#endif

View File

@@ -126,6 +126,20 @@ bool upsertPeerUdpAddr(se::State& s) {
}
SE_BIND_FUNC(upsertPeerUdpAddr)
bool pollUdpRecvRingBuff(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, "upsertPeerUdpAddr: Error processing arguments");
return DelayNoMore::UdpSession::pollUdpRecvRingBuff();
}
SE_REPORT_ERROR("wrong number of arguments: %d, was expecting %d; or wrong arg type!", (int)argc, 0);
return false;
}
SE_BIND_FUNC(pollUdpRecvRingBuff)
static bool udpSessionFinalize(se::State& s) {
CCLOGINFO("jsbindings: finalizing JS object %p (DelayNoMore::UdpSession)", s.nativeThisObject());
auto iter = se::NonRefNativePtrCreatedByCtorMap::find(s.nativeThisObject());
@@ -158,6 +172,7 @@ bool registerUdpSession(se::Object* obj) {
cls->defineStaticFunction("broadcastInputFrameUpsync", _SE(broadcastInputFrameUpsync));
cls->defineStaticFunction("closeUdpSession", _SE(closeUdpSession));
cls->defineStaticFunction("upsertPeerUdpAddr", _SE(upsertPeerUdpAddr));
cls->defineStaticFunction("pollUdpRecvRingBuff", _SE(pollUdpRecvRingBuff));
cls->defineFinalizeFunction(_SE(udpSessionFinalize));
cls->install();

View File

@@ -15,5 +15,5 @@ SE_DECLARE_FUNC(punchToServer);
SE_DECLARE_FUNC(broadcastInputFrameUpsync);
SE_DECLARE_FUNC(closeUdpSession);
SE_DECLARE_FUNC(upsertPeerUdpAddr);
SE_DECLARE_FUNC(pollUdpRecvRingBuff);
#endif

View File

@@ -15,7 +15,7 @@ LOCAL_SRC_FILES := hellojavascript/main.cpp \
../../../Classes/jsb_module_register.cpp \
../../../Classes/udp_session.cpp \
../../../Classes/udp_session_bridge.cpp \
../../../Classes/send_ring_buff.cpp
../../../Classes/ring_buff.cpp
LOCAL_C_INCLUDES := $(LOCAL_PATH)/../../../Classes

View File

@@ -190,11 +190,11 @@ copy "$(ProjectDir)..\..\..\project.json" "$(OutDir)\" /Y</Command>
<ClCompile Include="..\Classes\AppDelegate.cpp" />
<ClCompile Include="..\Classes\udp_session.cpp" />
<ClCompile Include="..\Classes\udp_session_bridge.cpp" />
<ClCompile Include="..\Classes\send_ring_buff.cpp" />
<ClCompile Include="..\Classes\ring_buff.cpp" />
</ItemGroup>
<ItemGroup>
<ClInclude Include="main.h" />
<ClInclude Include="..\Classes\send_ring_buff.hpp" />
<ClInclude Include="..\Classes\ring_buff.hpp" />
<ClInclude Include="..\Classes\udp_session.hpp" />
<ClInclude Include="..\Classes\udp_session_bridge.hpp" />
<ClInclude Include="..\Classes\AppDelegate.h" />

View File

@@ -22,7 +22,7 @@
<ClCompile Include="..\Classes\jsb_module_register.cpp">
<Filter>Classes</Filter>
</ClCompile>
<ClCompile Include="..\Classes\send_ring_buff.cpp">
<ClCompile Include="..\Classes\ring_buff.cpp">
<Filter>Classes</Filter>
</ClCompile>
<ClCompile Include="..\Classes\udp_session.cpp">
@@ -40,7 +40,7 @@
<Filter>win32</Filter>
</ClInclude>
<ClInclude Include="resource.h" />
<ClInclude Include="..\Classes\send_ring_buff.hpp">
<ClInclude Include="..\Classes\ring_buff.hpp">
<Filter>Classes</Filter>
</ClInclude>
<ClInclude Include="..\Classes\udp_session.hpp">

View File

@@ -755,7 +755,7 @@ func ApplyInputFrameDownsyncDynamicsOnSingleRenderFrame(inputsBuffer *RingBuffer
// Reset playerCollider position from the "virtual grid position"
newVx, newVy := currPlayerDownsync.VirtualGridX+currPlayerDownsync.VelX, currPlayerDownsync.VirtualGridY+currPlayerDownsync.VelY
if 0 >= thatPlayerInNextFrame.Hp && 0 == thatPlayerInNextFrame.FramesToRecover {
// Revive
// Revive from Dying
newVx, newVy = currPlayerDownsync.RevivalVirtualGridX, currPlayerDownsync.RevivalVirtualGridY
thatPlayerInNextFrame.CharacterState = ATK_CHARACTER_STATE_IDLE1
thatPlayerInNextFrame.Hp = currPlayerDownsync.MaxHp
@@ -945,7 +945,8 @@ func ApplyInputFrameDownsyncDynamicsOnSingleRenderFrame(inputsBuffer *RingBuffer
if fallStopping {
thatPlayerInNextFrame.VelY = 0
thatPlayerInNextFrame.VelX = 0
if ATK_CHARACTER_STATE_BLOWN_UP1 == thatPlayerInNextFrame.CharacterState {
if ATK_CHARACTER_STATE_DYING == thatPlayerInNextFrame.CharacterState {
} else if ATK_CHARACTER_STATE_BLOWN_UP1 == thatPlayerInNextFrame.CharacterState {
thatPlayerInNextFrame.CharacterState = ATK_CHARACTER_STATE_LAY_DOWN1
thatPlayerInNextFrame.FramesToRecover = chConfig.LayDownFramesToRecover
} else {
@@ -960,9 +961,11 @@ func ApplyInputFrameDownsyncDynamicsOnSingleRenderFrame(inputsBuffer *RingBuffer
thatPlayerInNextFrame.FramesToRecover = 0
}
} else {
// landedOnGravityPushback not fallStopping, could be in LayDown or GetUp
// landedOnGravityPushback not fallStopping, could be in LayDown or GetUp or Dying
if _, existent := nonAttackingSet[thatPlayerInNextFrame.CharacterState]; existent {
if ATK_CHARACTER_STATE_LAY_DOWN1 == thatPlayerInNextFrame.CharacterState {
if ATK_CHARACTER_STATE_DYING == thatPlayerInNextFrame.CharacterState {
// No update needed for Dying
} else if ATK_CHARACTER_STATE_LAY_DOWN1 == thatPlayerInNextFrame.CharacterState {
if 0 == thatPlayerInNextFrame.FramesToRecover {
thatPlayerInNextFrame.CharacterState = ATK_CHARACTER_STATE_GET_UP1
thatPlayerInNextFrame.FramesToRecover = chConfig.GetUpFramesToRecover