Jump to content

Recommended Posts

 The problem

   Server time calculation in Metin2 client uses only one packet to synchronize it. Server sends current timestamp once, client adjusts time according to it. If packet retreival is late so is calculated offset. This results in desynchronized in game interface, ex. affects are visible in the affect shower 2 seconds longer than they are actually applied. 

The fix

   Solution is fairly simple: by periodically measuring the Round Trip Time (RTT) and factoring in half of that delay to calculate the exact network offset, the client can continuously synchronize its clock with the server.

Spoiler

1. Open service.h and add anywhere:

#define ENABLE_TIME_SYNC

2. Open input.h and search for:

void Refine(LPCHARACTER ch, const char* c_pData);

3. Paste below:

#ifdef ENABLE_TIME_SYNC
  void TimeSyncRequest(LPCHARACTER ch, TPacketCGTimeSyncRequest* packet);
#endif

4. Open input_main.cpp and search for:

#include "dungeon.h"

5. Paste below:

#ifdef ENABLE_TIME_SYNC
#include <chrono>
#endif

6. Search for:

    case HEADER_CG_REFINE:
      Refine(ch, c_pData);
      break;

7. Paste below:

#ifdef ENABLE_TIME_SYNC
    case HEADER_CG_TIME_SYNC_REQUEST:
      TimeSyncRequest(ch, (TPacketCGTimeSyncRequest*)c_pData);
      break;
#endif

8. Add at the end of the file:

#ifdef ENABLE_TIME_SYNC
int64_t GetMonotonicTimeNs()
{
  return std::chrono::duration_cast<std::chrono::nanoseconds>(
           std::chrono::system_clock::now().time_since_epoch())
    .count();
}

void CInputMain::TimeSyncRequest(LPCHARACTER ch, TPacketCGTimeSyncRequest* packet)
{
  if (!ch || !ch->GetDesc())
    return;

  TPacketGCTimeSyncReply header{ .header = HEADER_GC_TIME_SYNC_REPLY,
                                 .clientTimeNs = packet->clientTimeNs,
                                 .serverTimeNs = GetMonotonicTimeNs() };

  ch->GetDesc()->Packet(&header, sizeof(TPacketGCTimeSyncReply));
}
#endif

9. Open packet.h and search for:

HEADER_CG_TIME_SYNC = 0xfc,

10. Paste ABOVE:

#ifdef ENABLE_TIME_SYNC
  HEADER_CG_TIME_SYNC_REQUEST = 0xfb,
#endif

11. Search for:

HEADER_GC_TIME_SYNC = 0xfc,

12. Paste ABOVE:

#ifdef ENABLE_TIME_SYNC
  HEADER_GC_TIME_SYNC_REPLY = 0xfb,
#endif

13. Search for:

#pragma pack()

14. Paste ABOVE:

#ifdef ENABLE_TIME_SYNC
typedef struct SPacketCGTimeSyncRequest
{
  uint8_t header;
  int64_t clientTimeNs;
} TPacketCGTimeSyncRequest;

typedef struct SPacketGCTimeSyncReply
{
  uint8_t header;
  int64_t clientTimeNs;
  int64_t serverTimeNs;
} TPacketGCTimeSyncReply;
#endif

15. Repeat steps 9-14 in client source.

16. Open packet_info.cpp and search for:

Set(HEADER_CG_STATE_CHECKER, sizeof(BYTE), "ServerStateCheck", false);

17. Paste below:

#ifdef ENABLE_TIME_SYNC
  Set(HEADER_CG_TIME_SYNC_REQUEST,
      sizeof(TPacketCGTimeSyncRequest),
      "TimeSyncRequest",
     false);
#endif

18. Open Control.h or UserInterface/Locale_Inc.h and add anywhere:

#define ENABLE_TIME_SYNC

19. Open UserInterface/GameType.cpp and paste at the and of the file:

#ifdef ENABLE_TIME_SYNC
int64_t GetMonotonicTimeNs()
{
  return std::chrono::duration_cast<std::chrono::nanoseconds>(
           std::chrono::system_clock::now().time_since_epoch())
    .count();
}
#endif

 20. Open UserInterface/GameType.h and paste at the and of the file:

#ifdef ENABLE_TIME_SYNC
int64_t
GetMonotonicTimeNs();
#endif

21. Open UserInterface/PythonApplication.cpp and search for:

#include "PythonCharacterManager.h"

22. Paste below:

#ifdef ENABLE_TIME_SYNC
#include <chrono>
#endif

23. Search for:

  , m_dwBufSleepSkipTime(0)

24. Paste below:

#ifdef ENABLE_TIME_SYNC
  , m_timeOffsetNs(0)
  , m_timeSync(false)
#endif

25. Search for function:

time_t CPythonApplication::GetServerTimeStamp()
{
    [...]
}

26. Replace with:

time_t CPythonApplication::GetServerTimeStamp()
{
#ifdef ENABLE_TIME_SYNC
    if (m_timeSync) {
        int64_t nowNs = std::chrono::duration_cast<std::chrono::nanoseconds>(
                          std::chrono::system_clock::now().time_since_epoch())
                          .count();

        int64_t syncedNs = nowNs + m_timeOffsetNs;

        return syncedNs / 1'000'000'000;
    }

    return (time(0) - m_tLocalStartTime) + m_tServerTime;
#else
    return (time(0) - m_tLocalStartTime) + m_tServerTime;
#endif
}

27. Paste at the end of the file:

#ifdef ENABLE_TIME_SYNC
void CPythonApplication::SyncTime(int64_t clientTime, int64_t serverTimeNs)
{
    m_timeOffsetNs =
        ((serverTimeNs - clientTime) + (serverTimeNs - GetMonotonicTimeNs())) / 2;

    m_timeSync = true;
}
#endif

28. Open UserInterface/PythonApplication.h and search for:

int m_iContinuousCursorNum;

29. Paste below:

#ifdef ENABLE_TIME_SYNC
public:
  void SyncTime(int64_t clientTime, int64_t serverTimeNs);

private:
  int64_t m_timeOffsetNs;
  bool m_timeSync;
#endif

30. Open UserInterface/PythonNetworkStream.cpp and search for:

    Set(HEADER_GC_DRAGON_SOUL_REFINE,
        TPacketType(sizeof(TPacketGCDragonSoulRefine), STATIC_SIZE_PACKET));

31. Paste below:

#ifdef ENABLE_TIME_SYNC
    Set(HEADER_GC_TIME_SYNC_REPLY,
        CNetworkPacketHeaderMap::TPacketType(sizeof(TPacketGCTimeSyncReply),
                                             STATIC_SIZE_PACKET));
#endif

32. Open UserInterface/PythonNetworkStream.h and search for:

void __BettingGuildWar_SetBettingMoney(UINT uBettingMoney);

33. Paste below:

#ifdef ENABLE_TIME_SYNC
public:
  void StartTimeSync();
  void UpdateTimeSync();
  void OnTimeSyncResponse();

  bool SendTimeSyncRequestPacket();

private:
  bool RecvTimeSyncReplyPacket();

  bool m_timeSyncRunning = false;
  bool m_waitingResponse = false;
  int m_burstCount = 0;
  int64_t m_nextActionTimeNs = 0;
#endif

34. Open UserInterface/PythonNetworkStreamPhaseGame.cpp and search for:

DWORD dwRecvCount = 0;

35. Paste below:

#ifdef ENABLE_TIME_SYNC
  UpdateTimeSync();
#endif

36. Paste at the end of the file:

#ifdef ENABLE_TIME_SYNC
void CPythonNetworkStream::StartTimeSync()
{
  m_timeSyncRunning = true;
  m_waitingResponse = false;
  m_burstCount = 0;

  m_nextActionTimeNs = GetMonotonicTimeNs();
}

void CPythonNetworkStream::UpdateTimeSync()
{
  if (!m_timeSyncRunning)
    return;

  if (m_waitingResponse)
    return;

  uint64_t now = GetMonotonicTimeNs();

  if (now < m_nextActionTimeNs)
    return;

  SendTimeSyncRequestPacket();
  m_waitingResponse = true;
}

void CPythonNetworkStream::OnTimeSyncResponse()
{
  m_waitingResponse = false;

  uint64_t now = GetMonotonicTimeNs();

  if (m_burstCount < 4) {
    ++m_burstCount;
    m_nextActionTimeNs = now + 200ULL * 1000ULL * 1000ULL;
  } else {
    m_timeSyncRunning = true;
    m_nextActionTimeNs = now + 5ULL * 1000ULL * 1000ULL * 1000ULL;
  }
}

bool CPythonNetworkStream::SendTimeSyncRequestPacket()
{
  TPacketCGTimeSyncRequest packet{ .header = HEADER_CG_TIME_SYNC_REQUEST,
                                   .clientTimeNs = GetMonotonicTimeNs() };

  return Send(sizeof(TPacketCGTimeSyncRequest), &packet);
}

bool CPythonNetworkStream::RecvTimeSyncReplyPacket()
{
  TPacketGCTimeSyncReply packet{};

  if (!Recv(sizeof(TPacketGCTimeSyncReply), &packet))
    return false;

  CPythonApplication::Instance().SyncTime(packet.clientTimeNs,
                                          packet.serverTimeNs);

  OnTimeSyncResponse();

  return true;
}
#endif

 

 

  • Metin2 Dev 2
  • Good 1
  • Love 1
Link to comment
https://metin2.dev/topic/34718-proper-time-synchronization/
Share on other sites

Don't use any images from : imgur, turkmmop, freakgamers, inforge, hizliresim... Or your content will be deleted without notice...
Use : https://metin2.download/media/add/

Please use https://metin2.download/ when uploading files smaller than 100MB, otherwise the approval will take longer due to manual upload.

Please sign in to comment

You will be able to leave a comment after signing in



Sign In Now
×
×
  • Create New...

Important Information

Terms of Use / Privacy Policy / Guidelines / We have placed cookies on your device to help make this website better. You can adjust your cookie settings, otherwise we'll assume you're okay to continue.