Working distributed connection using python server and C++ client

This is the code for the C++ client.cpp

#include "pandaFramework.h"
#include "windowFramework.h"
#include "nodePath.h"
#include "queuedConnectionManager.h"
#include "queuedConnectionReader.h"
#include "connectionWriter.h"
#include "netDatagram.h"
#include "datagram.h"
#include "datagramIterator.h"
#include "genericAsyncTask.h"
#include "asyncTaskManager.h"
#include "keyboardButton.h"
#include "mouseWatcher.h"
#include "clockObject.h"
#include "ambientLight.h"
#include "directionalLight.h"
#include "cardMaker.h"
#include "lineSegs.h"
#include "animControlCollection.h"
#include "auto_bind.h"
#include "partBundle.h"
#include "partBundleNode.h"
#include "load_prc_file.h"
#include "pandaSystem.h"
#include "pnotify.h"

#include <cmath>
#include <map>
#include <string>
#include <memory>

static NotifyCategory* crCat() {
    static NotifyCategory* c = Notify::ptr()->get_category(":ClientRepository");
    return c;
}
static NotifyCategory* actorCat() {
    static NotifyCategory* c = Notify::ptr()->get_category(":Actor");
    return c;
}
static NotifyCategory* appCat() {
    static NotifyCategory* c = Notify::ptr()->get_category(":App");
    return c;
}

// Values from direct/distributed/MsgTypesCMU.py
static const uint16_t CLIENT_DISCONNECT_CMU       = 9008;
static const uint16_t CLIENT_OBJECT_GENERATE_CMU  = 9002;
static const uint16_t CLIENT_OBJECT_UPDATE_FIELD  =  120;
static const uint16_t CLIENT_SET_INTEREST_CMU     = 9009;
static const uint16_t SET_DOID_RANGE_CMU          = 9001;
static const uint16_t OBJECT_GENERATE_CMU         = 9003;
static const uint16_t OBJECT_DISABLE_CMU          = 9005;
static const uint16_t OBJECT_DELETE_CMU           = 9006;
static const uint16_t OBJECT_UPDATE_FIELD_CMU     = 9004;
static const uint16_t OBJECT_SET_ZONE_CMU         = 9010;
static const uint16_t REQUEST_GENERATES_CMU       = 9007;

static const uint32_t ZONE_ID = 1;

static const std::string RALPH_MODEL = "ralph";
static const std::string RALPH_WALK  = "ralph-walk";

static const uint16_t FIELD_SET_X       =  8;
static const uint16_t FIELD_SET_Y       =  9;
static const uint16_t FIELD_SET_Z       = 10;
static const uint16_t FIELD_SET_H       = 11;
static const uint16_t FIELD_SET_P       = 12;
static const uint16_t FIELD_SET_R       = 13;
static const uint16_t FIELD_SET_SM_POS  = 34;
static const uint16_t FIELD_SET_SM_HPR  = 35;
static const uint16_t FIELD_SET_SM_XYzh = 37;
static const uint16_t FIELD_LOOP        = 43;
static const uint16_t FIELD_POSE        = 44;

// Position encoding: int16 = float * 10
static const float POS_DIVISOR = 10.0f;

// Movement
static const float MOVE_SPEED = 5.0f;
static const float TURN_SPEED = 60.0f;

class ClientRepository;

class Actor {
public:
    Actor() = default;

    ~Actor() {
        if (!_cleaned) cleanup();
    }

    Actor(const Actor&)            = delete;
    Actor& operator=(const Actor&) = delete;

    bool load(WindowFramework* window, PandaFramework* framework,
              const std::string& modelPath,
              const std::string& animPath,
              const std::string& animAlias = "walk") {
        _window    = window;
        _framework = framework;
        _animAlias = animAlias;
        _cleaned   = false;

        _modelNP = window->load_model(framework->get_models(), modelPath);
        if (_modelNP.is_empty()) {
            actorCat()->error() << "model '" << modelPath << "' not found on model-path\n";
            return false;
        }

        _animNP = window->load_model(_modelNP, animPath);
        if (_animNP.is_empty()) {
            actorCat()->warning() << "anim '" << animPath << "' not found — actor will be static\n";
            return true;
        }

        auto_bind(_modelNP.node(), _anims, ~0);

        if (_anims.get_num_anims() > 0 && !_anims.find_anim(animAlias))
            _anims.store_anim(_anims.get_anim(0), animAlias);

        actorCat()->info() << "loaded '" << modelPath << "' with "
                           << _anims.get_num_anims() << " animation(s)\n";
        return true;
    }

    void loop(const std::string& animName, bool restart = true) {
        if (_anims.get_num_anims() > 0) _anims.loop(animName, restart);
    }

    void stop(const std::string& animName = "") {
        if (_anims.get_num_anims() == 0) return;
        animName.empty() ? _anims.stop_all() : _anims.stop(animName);
    }

    void pose(const std::string& animName, int frame) {
        if (_anims.get_num_anims() > 0) _anims.pose(animName, frame);
    }

    // mirrors Actor.cleanup()
    void cleanup() {
        if (_cleaned) return;
        _cleaned = true;

        stop();                                     // 1. stop(None)
        _anims = AnimControlCollection();           // 2. clearPythonData()
        if (!_animNP.is_empty()) {                  // 3. flush()
            _animNP.detach_node();
            _animNP = NodePath();
        }
        if (!_modelNP.is_empty()) {                 // 4. removeNode()
            _modelNP.remove_node();
            _modelNP = NodePath();
        }
    }

    void reparentTo(NodePath parent)       { if (!_modelNP.is_empty()) _modelNP.reparent_to(parent); }
    void setPos(float x, float y, float z) { if (!_modelNP.is_empty()) _modelNP.set_pos(x, y, z); }
    void setH(float h)                     { if (!_modelNP.is_empty()) _modelNP.set_h(h); }
    void setColorScale(const LColor& c)    { if (!_modelNP.is_empty()) _modelNP.set_color_scale(c); }

    NodePath getNodePath()   const { return _modelNP; }
    bool     isEmpty()       const { return _modelNP.is_empty(); }
    bool     hasBoundAnims() const { return _anims.get_num_anims() > 0; }

private:
    WindowFramework*      _window    = nullptr;
    PandaFramework*       _framework = nullptr;
    NodePath              _modelNP;
    NodePath              _animNP;
    AnimControlCollection _anims;
    std::string           _animAlias;
    bool                  _cleaned = false;
};

class DistributedSmoothActor {
public:
    uint32_t          doId          = 0;
    float             x = 0, y = 0, h = 0;
    bool              isLocalAvatar = false;
    ClientRepository* cr            = nullptr;

    DistributedSmoothActor() = default;

    NodePath getNode() const { return _actor.getNodePath(); }

    void generate(WindowFramework* window, PandaFramework* framework);
    void announceGenerate();
    void disable();

    void setX(float v) { x = v; NodePath np = _actor.getNodePath(); if (!np.is_empty()) np.set_x(v); }
    void setY(float v) { y = v; NodePath np = _actor.getNodePath(); if (!np.is_empty()) np.set_y(v); }
    void setH(float v) { h = v; NodePath np = _actor.getNodePath(); if (!np.is_empty()) np.set_h(v); }

    void dSetX(float v);
    void dSetY(float v);
    void dSetH(float v);

    void startWalk();
    void stopWalk();

private:
    WindowFramework* _window    = nullptr;
    PandaFramework*  _framework = nullptr;
    Actor         _actor;
    bool             _isMoving  = false;

    NodePath buildFallbackGeometry();
};

class ClientRepository {
public:
    static const uint16_t CLASS_ID = 5;   // DistributedSmoothActor class number

    DistributedSmoothActor* myAvatar = nullptr;

    ClientRepository(WindowFramework* window, PandaFramework* framework)
        : _window(window), _framework(framework) {
        _reader = new QueuedConnectionReader(&_manager, 0);
        _writer = new ConnectionWriter(&_manager, 1);
        // CMU ServerRepository uses 2-byte TCP framing
        _reader->set_tcp_header_size(2);
        _writer->set_tcp_header_size(2);
    }

    ~ClientRepository() {
        delete _reader;
        delete _writer;
    }

    void connect(const std::string& host, int port) {
        _conn = _manager.open_TCP_client_connection(host, port, 3000);
        if (_conn) {
            _reader->add_connection(_conn);
            _connected = true;
            crCat()->info() << "connected to " << host << ":" << port << "\n";
        } else {
            crCat()->error() << "failed to connect to " << host << ":" << port
                             << " — is server.py running?\n";
        }
    }

    bool     isConnected() const { return _connected; }
    uint32_t getMyDoId()   const { return _myDoId; }

    // DC: setSmXYZH(int16/10 x, int16/10 y, int16/10 z, int16/10 h, uint16 t)
    void sendPosUpdate(float x, float y, float h) {
        if (!_connected || _myDoId == 0) return;
        uint16_t ts = static_cast<uint16_t>(
            static_cast<uint32_t>(
                ClockObject::get_global_clock()->get_real_time() * 100.0) & 0xFFFFu);
        Datagram dg;
        dg.add_uint16(CLIENT_OBJECT_UPDATE_FIELD);
        dg.add_uint32(_myDoId);
        dg.add_uint16(FIELD_SET_SM_XYzh);
        _packPos(dg, x); _packPos(dg, y); _packPos(dg, 0.0f); _packPos(dg, h);
        dg.add_uint16(ts);
        _writer->send(dg, _conn);
    }

    void sendLoop(const std::string& animName) {
        if (!_connected || _myDoId == 0) return;
        Datagram dg;
        dg.add_uint16(CLIENT_OBJECT_UPDATE_FIELD);
        dg.add_uint32(_myDoId);
        dg.add_uint16(FIELD_LOOP);
        uint16_t len = static_cast<uint16_t>(animName.size());
        dg.add_uint16(len);
        dg.append_data(animName.data(), len);
        _writer->send(dg, _conn);
    }

    void sendPose(const std::string& animName, int frame) {
        if (!_connected || _myDoId == 0) return;
        Datagram dg;
        dg.add_uint16(CLIENT_OBJECT_UPDATE_FIELD);
        dg.add_uint32(_myDoId);
        dg.add_uint16(FIELD_POSE);
        uint16_t len = static_cast<uint16_t>(animName.size());
        dg.add_uint16(len);
        dg.append_data(animName.data(), len);
        dg.add_int16(static_cast<int16_t>(frame));
        _writer->send(dg, _conn);
    }

    void sendSetLocation(uint32_t doId, uint32_t zoneId) {
        Datagram dg;
        dg.add_uint16(OBJECT_SET_ZONE_CMU);
        dg.add_uint32(doId);
        dg.add_uint32(zoneId);
        _writer->send(dg, _conn);
    }

    void poll() {
        if (!_connected) return;

        while (_manager.reset_connection_available()) {
            PT(Connection) lost;
            _manager.get_reset_connection(lost);
            if (lost == _conn) lostConnection();
            _manager.close_connection(lost);
        }

        while (_reader->data_available()) {
            NetDatagram ndg;
            if (!_reader->get_data(ndg)) continue;
            if (ndg.get_length() < 2) continue;
            DatagramIterator dgi(ndg);
            handleDatagram(dgi);
        }
    }

    void sendDisconnect() {
        if (!_connected) 
            return;

        if (_myDoId != 0) {
            Datagram dgDis;
            dgDis.add_uint16(OBJECT_DISABLE_CMU);
            dgDis.add_uint32(_myDoId);
            _writer->send(dgDis, _conn);
        }
        Datagram dg;
        dg.add_uint16(CLIENT_DISCONNECT_CMU);
        _writer->send(dg, _conn);
        _manager.close_connection(_conn);
        _connected = false;
    }

private:
    WindowFramework*        _window;
    PandaFramework*         _framework;
    QueuedConnectionManager _manager;
    QueuedConnectionReader* _reader;
    ConnectionWriter*       _writer;
    PT(Connection)          _conn;
    bool                    _connected = false;

    uint32_t _doIdBase = 0;   // first id in our assigned range
    uint32_t _doIdLast = 0;   // one past the last id (exclusive)
    uint32_t _myDoId   = 0;   // doId of our spawned avatar

    std::map<uint32_t, std::unique_ptr<DistributedSmoothActor>> _avatars;

    bool isLocalId(uint32_t doId) const {
        return _doIdLast > 0 && doId >= _doIdBase && doId < _doIdLast;
    }

    static void  _packPos(Datagram& dg, float v)   { dg.add_int16(static_cast<int16_t>(v * POS_DIVISOR)); }
    static float _unpackPos(DatagramIterator& dgi) { return dgi.get_int16() / POS_DIVISOR; }

    void onConnected() {
        crCat()->info() << "doIdBase=" << _doIdBase << " doIdLast=" << _doIdLast << "\n";
        sendSetInterest(ZONE_ID);
        createDistributedObject();
    }

    void sendSetInterest(uint32_t zoneId) {
        Datagram dg;
        dg.add_uint16(CLIENT_SET_INTEREST_CMU);
        dg.add_uint32(zoneId);
        _writer->send(dg, _conn);
    }

    // Send CLIENT_OBJECT_GENERATE_CMU and spawn our avatar locally.
    // Wire: [uint32 zoneId][uint16 classId][uint32 doId][uint16 numOther=0]
    // The server broadcasts OBJECT_GENERATE_CMU to other clients but NOT back
    void createDistributedObject() {
        if (!_connected) return;
        _myDoId = _doIdBase;

        Datagram dg;
        dg.add_uint16(CLIENT_OBJECT_GENERATE_CMU);
        dg.add_uint32(ZONE_ID);
        dg.add_uint16(CLASS_ID);
        dg.add_uint32(_myDoId);
        dg.add_uint16(0);   // numOther = 0
        _writer->send(dg, _conn);

        if (_avatars.count(_myDoId) == 0) {
            auto actor = std::make_unique<DistributedSmoothActor>();
            actor->doId          = _myDoId;
            actor->cr            = this;
            actor->isLocalAvatar = true;
            actor->generate(_window, _framework);
            actor->announceGenerate();
            myAvatar = actor.get();
            _avatars[_myDoId] = std::move(actor);
            crCat()->info() << "local avatar spawned doId=" << _myDoId << "\n";
        }
    }

    void lostConnection() {
        crCat()->warning() << "lost connection to server\n";
        _connected = false;
    }

    void handleDatagram(DatagramIterator& dgi) {
        const uint16_t msgType = dgi.get_uint16();
        switch (msgType) {
            case SET_DOID_RANGE_CMU:      _onSetDoIdRange(dgi);   break;
            case OBJECT_GENERATE_CMU:     _onObjectGenerate(dgi); break;
            case OBJECT_UPDATE_FIELD_CMU: _onObjectUpdate(dgi);   break;
            case OBJECT_DISABLE_CMU:      _onObjectDisable(dgi);  break;
            case OBJECT_DELETE_CMU:       _onObjectDisable(dgi);  break;
            case REQUEST_GENERATES_CMU:   _resendGenerates();      break;
            default: break;
        }
    }

    // Wire: [uint32 doIdBase] [uint32 rangeCount]
    void _onSetDoIdRange(DatagramIterator& dgi) {
        _doIdBase = dgi.get_uint32();
        _doIdLast = _doIdBase + dgi.get_uint32();   // second field is a COUNT
        onConnected();
    }

    // Wire: [uint32 ownerBase] [uint32 zoneId] [uint16 classId] [uint32 doId]
    //       [uint16 numOther]  [fieldId + value] × numOther
    // DistributedSmoothActor has zero required fields; all arrive in numOther.
    void _onObjectGenerate(DatagramIterator& dgi) {
        uint32_t ownerBase = dgi.get_uint32();
        uint32_t zoneId    = dgi.get_uint32();
        uint16_t classId   = dgi.get_uint16();
        uint32_t doId      = dgi.get_uint32();

        if (classId != CLASS_ID) return;
        if (_avatars.count(doId)) return;

        float x = 0.0f, y = 0.0f, h = 0.0f;
        if (dgi.get_remaining_size() >= 2) {
            uint16_t numOther = dgi.get_uint16();
            for (uint16_t i = 0; i < numOther && dgi.get_remaining_size() >= 4; ++i) {
                uint16_t fid = dgi.get_uint16();
                if      (fid == FIELD_SET_X) { x = _unpackPos(dgi); }
                else if (fid == FIELD_SET_Y) { y = _unpackPos(dgi); }
                else if (fid == FIELD_SET_H) { h = _unpackPos(dgi); }
                else if (fid == FIELD_SET_Z ||
                         fid == FIELD_SET_P ||
                         fid == FIELD_SET_R) { _unpackPos(dgi); }
                else break;
            }
        }

        bool isOurs = isLocalId(doId) || (ownerBase == _doIdBase);
        if (isOurs) _myDoId = doId;

        auto actor = std::make_unique<DistributedSmoothActor>();
        actor->doId          = doId;
        actor->cr            = this;
        actor->isLocalAvatar = isOurs;
        actor->generate(_window, _framework);
        actor->setX(x); actor->setY(y); actor->setH(h);
        actor->announceGenerate();

        if (isOurs) {
            myAvatar = actor.get();
            crCat()->info() << "our avatar spawned doId=" << doId << "\n";
        } else {
            crCat()->info() << "remote avatar spawned doId=" << doId << "\n";
        }
        _avatars[doId] = std::move(actor);
    }

    // Wire: [uint32 doId] repeated until end of datagram
    void _onObjectDisable(DatagramIterator& dgi) {
        while (dgi.get_remaining_size() >= 4) {
            uint32_t doId = dgi.get_uint32();
            auto it = _avatars.find(doId);
            if (it != _avatars.end()) {
                it->second->disable();
                if (it->second.get() == myAvatar) myAvatar = nullptr;
                _avatars.erase(it);
                crCat()->info() << "avatar removed doId=" << doId << "\n";
            }
        }
    }

    // Wire: [uint32 senderId] [uint32 doId] [uint16 fieldId] [payload]
    void _onObjectUpdate(DatagramIterator& dgi) {
        (void)dgi.get_uint32();    // senderId — CMU prefix, must consume
        uint32_t doId    = dgi.get_uint32();
        uint16_t fieldId = dgi.get_uint16();

        if (doId == _myDoId) return;   // ignore echoes of our own updates

        auto it = _avatars.find(doId);
        if (it == _avatars.end()) return;
        DistributedSmoothActor* actor = it->second.get();

        if (fieldId == FIELD_SET_X) {
            actor->setX(_unpackPos(dgi));
        } else if (fieldId == FIELD_SET_Y) {
            actor->setY(_unpackPos(dgi));
        } else if (fieldId == FIELD_SET_H) {
            actor->setH(_unpackPos(dgi));
        } else if (fieldId == FIELD_SET_SM_XYzh) {
            // x, y, z, h (int16/10 each) + t (uint16 timestamp)
            float x = _unpackPos(dgi), y = _unpackPos(dgi);
            _unpackPos(dgi);          // z — discard
            float h = _unpackPos(dgi);
            dgi.get_uint16();         // timestamp — discard
            actor->setX(x); actor->setY(y); actor->setH(h);
        } else if (fieldId == FIELD_SET_SM_POS) {
            // x, y, z (int16/10 each) + t (uint16 timestamp)
            float x = _unpackPos(dgi), y = _unpackPos(dgi);
            _unpackPos(dgi); dgi.get_uint16();
            actor->setX(x); actor->setY(y);
        } else if (fieldId == FIELD_SET_SM_HPR) {
            // h, p, r (int16/10 each) + t (uint16 timestamp)
            float h = _unpackPos(dgi);
            _unpackPos(dgi); _unpackPos(dgi); dgi.get_uint16();
            actor->setH(h);
        } else if (fieldId == FIELD_LOOP) {
            uint16_t len = dgi.get_uint16(); dgi.skip_bytes(len);
            actor->startWalk();
        } else if (fieldId == FIELD_POSE) {
            uint16_t len = dgi.get_uint16(); dgi.skip_bytes(len);
            dgi.get_int16();   // frame
            actor->stopWalk();
        }
    }

    // Re-broadcast our objects so a newly joined client can discover us.
    // Called in response to REQUEST_GENERATES_CMU (9007).
    void _resendGenerates() {
        for (auto& kv : _avatars) {
            if (!isLocalId(kv.first)) continue;
            Datagram dg;
            dg.add_uint16(CLIENT_OBJECT_GENERATE_CMU);
            dg.add_uint32(ZONE_ID);
            dg.add_uint16(CLASS_ID);
            dg.add_uint32(kv.first);
            dg.add_uint16(0);
            _writer->send(dg, _conn);
        }
    }
};

NodePath DistributedSmoothActor::buildFallbackGeometry() {
    LColor col = isLocalAvatar ? LColor(0.4f, 0.6f, 1.0f, 1.0f)
                               : LColor(1.0f, 0.6f, 0.4f, 1.0f);
    LineSegs ls("fallback");
    ls.set_color(col); ls.set_thickness(4.0f);
    ls.move_to(0, 0, 0);    ls.draw_to(0, 0, 3);
    ls.move_to(-1, 0, 1.5f); ls.draw_to(1, 0, 1.5f);
    ls.move_to(0, -1, 1.5f); ls.draw_to(0, 1, 1.5f);
    ls.move_to(0, 0, 3);    ls.draw_to( 0.3f, 0.5f, 2.5f);
    ls.move_to(0, 0, 3);    ls.draw_to(-0.3f, 0.5f, 2.5f);
    return NodePath(ls.create());
}

void DistributedSmoothActor::generate(WindowFramework* window, PandaFramework* framework) {
    _window    = window;
    _framework = framework;
}

void DistributedSmoothActor::announceGenerate() {
    LColor col = isLocalAvatar ? LColor(0.4f, 0.6f, 1.0f, 1.0f)
                               : LColor(1.0f, 0.6f, 0.4f, 1.0f);

    bool loaded = _actor.load(_window, _framework, RALPH_MODEL, RALPH_WALK, "walk");

    NodePath np;
    if (loaded && !_actor.isEmpty()) {
        _actor.setColorScale(col);
        if (_actor.hasBoundAnims()) _actor.pose("walk", 5);
        np = _actor.getNodePath();
    } else {
        actorCat()->warning() << "using fallback geometry for doId=" << doId << "\n";
        np = buildFallbackGeometry();
    }

    np.reparent_to(_window->get_render());
    np.set_pos(x, y, 0.0f);
    np.set_h(h);
}

void DistributedSmoothActor::disable() {
    _actor.cleanup();
}

void DistributedSmoothActor::dSetX(float v) { x = v; NodePath np = _actor.getNodePath(); if (!np.is_empty()) np.set_x(v); }
void DistributedSmoothActor::dSetY(float v) { y = v; NodePath np = _actor.getNodePath(); if (!np.is_empty()) np.set_y(v); }
void DistributedSmoothActor::dSetH(float v) { h = v; NodePath np = _actor.getNodePath(); if (!np.is_empty()) np.set_h(v); }

void DistributedSmoothActor::startWalk() {
    if (_isMoving) return;
    _isMoving = true;
    _actor.loop("walk");
    if (isLocalAvatar && cr) cr->sendLoop("walk");
}

void DistributedSmoothActor::stopWalk() {
    if (!_isMoving) return;
    _isMoving = false;
    _actor.stop("walk");
    _actor.pose("walk", 5);
    if (isLocalAvatar && cr) cr->sendPose("walk", 5);
}

static PandaFramework*   gFramework    = nullptr;
static WindowFramework*  gWindow       = nullptr;
static ClientRepository* gCR           = nullptr;
static MouseWatcher*     gMouseWatcher = nullptr;

static void setupScene() {
    NodePath mouseNP = gWindow->get_mouse();
    if (!mouseNP.is_empty())
        gMouseWatcher = DCAST(MouseWatcher, mouseNP.node());

    NodePath cam = gWindow->get_camera_group();
    cam.wrt_reparent_to(gWindow->get_render());
    cam.set_pos(0.0f, -50.0f, 35.0f);
    cam.look_at(LPoint3f(0.0f, 0.0f, 0.0f));

    PT(AmbientLight) al = new AmbientLight("al");
    al->set_color(LColor(0.5f, 0.5f, 0.5f, 1.0f));
    gWindow->get_render().set_light(gWindow->get_render().attach_new_node(al));

    PT(DirectionalLight) dl = new DirectionalLight("dl");
    dl->set_color(LColor(0.8f, 0.8f, 0.5f, 1.0f));
    NodePath dlnp = gWindow->get_render().attach_new_node(dl);
    dlnp.set_hpr(45.0f, -45.0f, 0.0f);
    gWindow->get_render().set_light(dlnp);

    CardMaker cm("ground");
    cm.set_frame(-50.0f, 50.0f, -50.0f, 50.0f);
    NodePath ground = gWindow->get_render().attach_new_node(cm.generate());
    ground.set_p(-90.0f);
    ground.set_color(0.25f, 0.45f, 0.25f, 1.0f);
}

AsyncTask::DoneStatus taskNetworkPoll(GenericAsyncTask*, void*) {
    gCR->poll();
    return AsyncTask::DS_cont;
}

AsyncTask::DoneStatus taskMovement(GenericAsyncTask*, void*) {
    DistributedSmoothActor* av = gCR->myAvatar;
    if (!av || !gMouseWatcher) return AsyncTask::DS_cont;

    float dt     = static_cast<float>(ClockObject::get_global_clock()->get_dt());
    bool  moving = false;

    if (gMouseWatcher->is_button_down(KeyboardButton::left()) ||
        gMouseWatcher->is_button_down(KeyboardButton::ascii_key('a'))) {
        av->dSetH(av->h + TURN_SPEED * dt); moving = true;
    }
    if (gMouseWatcher->is_button_down(KeyboardButton::right()) ||
        gMouseWatcher->is_button_down(KeyboardButton::ascii_key('d'))) {
        av->dSetH(av->h - TURN_SPEED * dt); moving = true;
    }
    if (gMouseWatcher->is_button_down(KeyboardButton::up()) ||
        gMouseWatcher->is_button_down(KeyboardButton::ascii_key('w'))) {
        float rad = av->h * static_cast<float>(MathNumbers::pi) / 180.0f;
        av->dSetX(av->x - sinf(rad) * MOVE_SPEED * dt);
        av->dSetY(av->y + cosf(rad) * MOVE_SPEED * dt);
        moving = true;
    }
    if (gMouseWatcher->is_button_down(KeyboardButton::down()) ||
        gMouseWatcher->is_button_down(KeyboardButton::ascii_key('s'))) {
        float rad = av->h * static_cast<float>(MathNumbers::pi) / 180.0f;
        av->dSetX(av->x + sinf(rad) * MOVE_SPEED * dt);
        av->dSetY(av->y - cosf(rad) * MOVE_SPEED * dt);
        moving = true;
    }

    if (moving) {
        av->startWalk();
        gCR->sendPosUpdate(av->x, av->y, av->h);
    } else {
        av->stopWalk();
    }

    return AsyncTask::DS_cont;
}

int main(int argc, char* argv[]) {
    const std::string host = "127.0.0.1";
    const int port = 4400;

    load_prc_file_data("", "model-path /c/PATH_TO_YOUR_MODELS/models");

    PandaFramework framework;
    framework.open_framework(argc, argv);
    framework.set_window_title("SimpleAvatar C++ Client");
    gFramework = &framework;

    gWindow = framework.open_window();
    if (!gWindow) {
        appCat()->error() << "could not open window\n";
        return 1;
    }
    gWindow->enable_keyboard();
    setupScene();

    ClientRepository cr(gWindow, gFramework);
    gCR = &cr;
    cr.connect(host, port);

    PT(GenericAsyncTask) netTask = new GenericAsyncTask("netPoll", taskNetworkPoll, nullptr);
    netTask->set_sort(-40);
    AsyncTaskManager::get_global_ptr()->add(netTask);

    PT(GenericAsyncTask) moveTask = new GenericAsyncTask("moveTask", taskMovement, nullptr);
    moveTask->set_sort(0);
    AsyncTaskManager::get_global_ptr()->add(moveTask);

    appCat()->info() << "Panda3D " << PandaSystem::get_version_string() << " — WASD / arrow keys to move\n";

    framework.main_loop();

    cr.sendDisconnect();
    framework.close_framework();
    return 0;
}

Obviously this is POC that distributed network can be used in C++ with a python host server.
YOU HAVE TO USE YOUR OWN PATHs FOR THE MODELS
For the server i used 06-simple-avatar