From 91ecfe0adb02b9a1ed91c8980f93732f7ff19363 Mon Sep 17 00:00:00 2001 From: XProger Date: Mon, 26 Nov 2018 11:00:34 +0300 Subject: [PATCH] #142 network multiplayer basic protocol and routine (WIP) --- src/core.h | 10 +- src/game.h | 1 + src/lara.h | 11 +- src/level.h | 12 + src/napi_dummy.h | 17 ++ src/napi_socket.h | 103 +++++++ src/network.h | 354 ++++++++++++++++++++++ src/platform/win/OpenLara.vcxproj | 15 +- src/platform/win/OpenLara.vcxproj.filters | 3 + src/utils.h | 24 +- 10 files changed, 540 insertions(+), 10 deletions(-) create mode 100644 src/napi_dummy.h create mode 100644 src/napi_socket.h create mode 100644 src/network.h diff --git a/src/core.h b/src/core.h index bf3b996..7720019 100644 --- a/src/core.h +++ b/src/core.h @@ -15,6 +15,7 @@ #define _GAPI_GL 1 //#define _GAPI_D3D9 1 //#define _GAPI_VULKAN 1 + //#define _NAPI_SOCKET #include @@ -324,6 +325,12 @@ namespace Core { #include "input.h" #include "sound.h" +#if defined(_NAPI_SOCKET) + #include "napi_socket.h" +#else + #include "napi_dummy.h" +#endif + #define MAX_LIGHTS 4 #define MAX_RENDER_BUFFERS 32 #define MAX_CONTACTS 15 @@ -622,6 +629,7 @@ namespace Core { Input::init(); Sound::init(); + NAPI::init(); GAPI::init(); @@ -770,7 +778,7 @@ namespace Core { delete ditherTex; GAPI::deinit(); - + NAPI::deinit(); Sound::deinit(); } diff --git a/src/game.h b/src/game.h index f6cb605..523e6ad 100644 --- a/src/game.h +++ b/src/game.h @@ -173,6 +173,7 @@ namespace Game { void updateTick() { Input::update(); + Network::update(); cheatControl(Input::lastState[0]); diff --git a/src/lara.h b/src/lara.h index a5aca58..e278b22 100644 --- a/src/lara.h +++ b/src/lara.h @@ -320,6 +320,8 @@ struct Lara : Character { float hitTimer; + int32 networkInput; + #ifdef _DEBUG //uint16 *dbgBoxes; //int dbgBoxesCount; @@ -494,8 +496,9 @@ struct Lara : Character { Lara(IGame *game, int entity) : Character(game, entity, LARA_MAX_HEALTH), dozy(false), wpnCurrent(TR::Entity::NONE), wpnNext(TR::Entity::NONE), braid(NULL) { camera = new Camera(game, this); - itemHolster = TR::Entity::NONE; - hitTimer = 0.0f; + itemHolster = TR::Entity::NONE; + hitTimer = 0.0f; + networkInput = -1; if (level->extra.laraSkin > -1) level->entities[entity].modelIndex = level->extra.laraSkin + 1; @@ -2893,6 +2896,10 @@ struct Lara : Character { virtual int getInput() { // TODO: updateInput if (level->isCutsceneLevel()) return 0; + + if (networkInput != -1) + return networkInput; + input = 0; int pid = camera->cameraIndex; diff --git a/src/level.h b/src/level.h index 03a03a5..d9cf967 100644 --- a/src/level.h +++ b/src/level.h @@ -11,6 +11,7 @@ #include "trigger.h" #include "inventory.h" #include "savegame.h" +#include "network.h" #if defined(_DEBUG) && defined(_GAPI_GL) && !defined(_GAPI_GLES) #define DEBUG_RENDER @@ -872,10 +873,14 @@ struct Level : IGame { loadSlot = -1; } + Network::start(this); + Core::resetTime(); } virtual ~Level() { + Network::stop(); + for (int i = 0; i < level.entitiesCount; i++) delete (Controller*)level.entities[i].controller; @@ -1812,6 +1817,13 @@ struct Level : IGame { sndWater->setVolume(volWater, 0.2f); if (sndTrack && sndTrack->volumeTarget != volTrack) sndTrack->setVolume(volTrack, 0.2f); + + #ifdef _DEBUG + if (Input::down[ikJ]) { + Network::sayHello(); + Input::down[ikJ] = false; + } + #endif } void updateEffect() { diff --git a/src/napi_dummy.h b/src/napi_dummy.h new file mode 100644 index 0000000..23d424d --- /dev/null +++ b/src/napi_dummy.h @@ -0,0 +1,17 @@ +#ifndef H_NAPI_DUMMY +#define H_NAPI_DUMMYT + +#include "utils.h" + +namespace NAPI { + typedef int Peer; + + void init() {} + void deinit() {} + void listen(uint16 port) {} + int send(const Peer &to, const void *data, int size) { return 0; } + int recv(Peer &from, void *data, int size) { return 0; } + void broadcast(const void *data, int size) {} +} + +#endif \ No newline at end of file diff --git a/src/napi_socket.h b/src/napi_socket.h new file mode 100644 index 0000000..f523ca2 --- /dev/null +++ b/src/napi_socket.h @@ -0,0 +1,103 @@ +#ifndef H_NAPI_SOCKET +#define H_NAPI_SOCKET + +#include "utils.h" + +#ifdef _OS_WIN + #include "winsock.h" +#endif + +namespace NAPI { + + struct Peer { + uint16 port; + uint32 ip; + + inline bool operator == (const Peer &peer) const { + return port == peer.port && ip == peer.ip; + } + }; + + SOCKET sock; + sockaddr_in addr; + uint16 port; + + void init() { + sock = INVALID_SOCKET; + + WSAData wData; + WSAStartup(0x0101, &wData); + } + + void deinit() { + if (sock != INVALID_SOCKET) { + shutdown(sock, 1); + #ifdef _OS_WIN + closesocket(sock); + #else + close(sock); + #endif + } + WSACleanup(); + } + + void listen(uint16 port) { + NAPI::port = port; + + if (sock != INVALID_SOCKET) return; + + sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); + if (sock == INVALID_SOCKET) { + LOG("! network: failed to create socket\n"); + sock = INVALID_SOCKET; + return; + } + + u_long on = 1; + if (ioctlsocket(sock, FIONBIO, &on) < 0) { + LOG("! network: failed to set non-blocking mode\n"); + closesocket(sock); + sock = INVALID_SOCKET; + return; + } + + memset(&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; + addr.sin_port = htons(port); + if (bind(sock, (sockaddr*)&addr, sizeof(addr))) + LOG("! network: unable to bind socket on port (%d)\n", (int)port); + + on = 1; + if (setsockopt(sock, SOL_SOCKET, SO_BROADCAST, (const char*)&on, sizeof(on))) + LOG("! network: unable to enable broadcasting\n"); + } + + int send(const Peer &to, const void *data, int size) { + if (sock == INVALID_SOCKET) return false; + + addr.sin_addr.s_addr = to.ip; + addr.sin_port = to.port; + return sendto(sock, (const char*)data, size, 0, (sockaddr*)&addr, sizeof(addr)); + } + + int recv(Peer &from, void *data, int size) { + if (sock == INVALID_SOCKET) return false; + + int i = sizeof(addr); + int count = recvfrom(sock, (char*)data, size, 0, (sockaddr*)&addr, &i); + if (count > 0) { + from.ip = addr.sin_addr.s_addr; + from.port = addr.sin_port; + } + return count; + } + + void broadcast(const void *data, int size) { + Peer peer; + peer.ip = INADDR_BROADCAST; + peer.port = htons(port); + send(peer, data, size); + } +} + +#endif \ No newline at end of file diff --git a/src/network.h b/src/network.h new file mode 100644 index 0000000..55728f2 --- /dev/null +++ b/src/network.h @@ -0,0 +1,354 @@ +#ifndef H_NET +#define H_NET + +#include "core.h" +#include "utils.h" +#include "format.h" +#include "controller.h" +#include "ui.h" + +#define NET_PROTOCOL 1 +#define NET_PORT 21468 + +#define NET_PING_TIMEOUT ( 1000 * 10 ) +#define NET_PING_PERIOD ( 1000 * 3 ) +#define NET_SYMC_INPUT_PERIOD ( 1000 / 25 ) +#define NET_SYMC_STATE_PERIOD ( 1000 / 1000 ) + +namespace Network { + + struct Packet { + enum Type { + HELLO, INFO, PING, PONG, JOIN, ACCEPT, REJECT, INPUT, STATE, + }; + + uint16 type; + uint16 id; + + union { + struct { + uint8 protocol; + uint8 game; + } hello; + + struct { + str16 name; + uint8 level; + uint8 players; + struct { + uint16 secure:1; + } flags; + } info; + + struct { + str16 nick; + str16 pass; + } join; + + struct { + uint16 id; + uint8 level; + uint8 roomIndex; + int16 posX; + int16 posY; + int16 posZ; + int16 angle; + } accept; + + struct { + uint16 reason; + } reject; + + struct { + uint16 mask; + } input; + + struct { + uint8 roomIndex; + uint8 reserved; + int16 pos[3]; + int16 angle[2]; + uint8 frame; + uint8 stand; + uint16 animIndex; + } state; + }; + + int getSize() const { + const int sizes[] = { + sizeof(hello), + sizeof(info), + 0, + 0, + sizeof(join), + sizeof(accept), + sizeof(reject), + sizeof(input), + sizeof(state), + }; + + if (type >= 0 && type < COUNT(sizes)) + return 2 + 2 + sizes[type]; + ASSERT(false); + return 0; + } + }; + + IGame *game; + + struct Player { + NAPI::Peer peer; + int pingTime; + int pingIndex; + Controller *controller; + }; + + Array players; + + int syncInputTime; + int syncStateTime; + + void start(IGame *game) { + Network::game = game; + NAPI::listen(NET_PORT); + syncInputTime = syncStateTime = osGetTime(); + } + + void stop() { + players.clear(); + } + + bool sendPacket(const NAPI::Peer &to, const Packet &packet) { + return NAPI::send(to, &packet, packet.getSize()) > 0; + } + + bool recvPacket(NAPI::Peer &from, Packet &packet) { + int count = NAPI::recv(from, &packet, sizeof(packet)); + if (count > 0) { + if (count != packet.getSize()) { + ASSERT(false); + return false; + } + return true; + } + return false; + } + + void sayHello() { + Packet packet; + packet.type = Packet::HELLO; + packet.hello.protocol = NET_PROTOCOL; + packet.hello.game = game->getLevel()->version & TR::VER_VERSION; + + NAPI::broadcast(&packet, packet.getSize()); + } + + void joinGame(const NAPI::Peer &peer) { + Packet packet; + packet.type = Packet::JOIN; + packet.join.nick = "Player_2"; + packet.join.pass = ""; + LOG("join game\n"); + sendPacket(peer, packet); + } + + void pingPlayers(int time) { + int i = 0; + while (i < players.length) { + int delta = time - players[i].pingTime; + + if (delta > NET_PING_TIMEOUT) { + players.removeFast(i); + continue; + } + + if (delta > NET_PING_PERIOD) { + Packet packet; + packet.type = Packet::PING; + sendPacket(players[i].peer, packet); + } + + i++; + } + } + + void syncInput(int time) { + Lara *lara = (Lara*)game->getLara(); + if (!lara) return; + + if ((time - syncInputTime) < NET_SYMC_INPUT_PERIOD) + return; + + Packet packet; + packet.type = Packet::INPUT; + packet.input.mask = lara->getInput(); + + for (int i = 0; i < players.length; i++) + sendPacket(players[i].peer, packet); + + syncInputTime = time; + } + + void syncState(int time) { + if ((time - syncStateTime) < NET_SYMC_STATE_PERIOD) + return; + // TODO + syncStateTime = time; + } + + Player* getPlayerByPeer(const NAPI::Peer &peer) { + for (int i = 0; i < players.length; i++) + if (players[i].peer == peer) { + return &players[i]; + } + return NULL; + } + + void getSpawnPoint(uint8 &roomIndex, vec3 &pos, float &angle) { + Controller *lara = game->getLara(); + roomIndex = lara->getRoomIndex(); + pos = lara->getPos(); + angle = normalizeAngle(lara->angle.y); // 0..2PI + } + + void update() { + int count; + NAPI::Peer from; + Packet packet, response; + + int time = osGetTime(); + + while ( (count = recvPacket(from, packet)) > 0 ) { + Player *player = getPlayerByPeer(from); + if (player) + player->pingTime = time; + + switch (packet.type) { + case Packet::HELLO : + if (game->getLevel()->isTitle()) + break; + + LOG("recv HELLO\n"); + if (packet.hello.game != (game->getLevel()->version & TR::VER_VERSION)) + break; + if (packet.hello.protocol != NET_PROTOCOL) + break; + LOG("send INFO\n"); + response.type = Packet::INFO; + response.info.name = "MultiOpenLara"; + response.info.level = game->getLevel()->id; + response.info.players = players.length + 1; + response.info.flags.secure = false; + + sendPacket(from, response); + + break; + + case Packet::INFO : { + LOG("recv INFO\n"); + char buf[sizeof(packet.info.name) + 1]; + packet.info.name.get(buf); + LOG("name: %s\n", buf); + joinGame(from); + break; + } + + case Packet::PING : + if (player) { + response.type = Packet::PONG; + sendPacket(from, response); + } + break; + + case Packet::PONG : + break; + + case Packet::JOIN : + if (!player) { + uint8 roomIndex; + vec3 pos; + float angle; + + getSpawnPoint(roomIndex, pos, angle); + + Player newPlayer; + newPlayer.peer = from; + newPlayer.pingIndex = 0; + newPlayer.pingTime = time; + newPlayer.controller = game->addEntity(TR::Entity::LARA, roomIndex, pos, angle); + players.push(newPlayer); + + ((Lara*)newPlayer.controller)->networkInput = 0; + + char buf[32]; + packet.join.nick.get(buf); + LOG("Player %s joined\n", buf); + + ASSERT(newPlayer.controller); + + TR::Room &room = game->getLevel()->rooms[roomIndex]; + vec3 offset = pos - room.getOffset(); + + response.type = Packet::ACCEPT; + response.accept.id = 0; + response.accept.level = game->getLevel()->id; + response.accept.roomIndex = roomIndex; + response.accept.posX = int16(offset.x); + response.accept.posY = int16(offset.y); + response.accept.posZ = int16(offset.z); + response.accept.angle = int16(angle * RAD2DEG); + + sendPacket(from, response); + } + break; + + case Packet::ACCEPT : { + LOG("accept!\n"); + game->loadLevel(TR::LevelID(packet.accept.level)); + inventory->toggle(); + break; + } + + case Packet::REJECT : + break; + + case Packet::INPUT : + if (game->getLevel()->isTitle()) + break; + + if (!player) { + uint8 roomIndex; + vec3 pos; + float angle; + + getSpawnPoint(roomIndex, pos, angle); + + Player newPlayer; + newPlayer.peer = from; + newPlayer.pingIndex = 0; + newPlayer.pingTime = time; + newPlayer.controller = game->addEntity(TR::Entity::LARA, roomIndex, pos, angle); + players.push(newPlayer); + + ((Lara*)newPlayer.controller)->networkInput = 0; + + player = getPlayerByPeer(from); + } + + if (player) { + ((Lara*)player->controller)->networkInput = packet.input.mask; + } + break; + + case Packet::STATE : + break; + } + } + + pingPlayers(time); + syncInput(time); + syncState(time); + } +} + +#endif diff --git a/src/platform/win/OpenLara.vcxproj b/src/platform/win/OpenLara.vcxproj index 513f052..0be254e 100644 --- a/src/platform/win/OpenLara.vcxproj +++ b/src/platform/win/OpenLara.vcxproj @@ -106,7 +106,7 @@ Console true - openvr_api.lib;d3d9.lib;opengl32.lib;winmm.lib;kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;%(AdditionalDependencies) + wsock32.lib;openvr_api.lib;d3d9.lib;opengl32.lib;winmm.lib;kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;%(AdditionalDependencies) @@ -149,7 +149,7 @@ true true true - wcrt.lib;openvr_api.lib;d3d9.lib;opengl32.lib;winmm.lib;kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;%(AdditionalDependencies) + wcrt.lib;wsock32.lib;openvr_api.lib;d3d9.lib;opengl32.lib;winmm.lib;kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;%(AdditionalDependencies) true false false @@ -179,7 +179,7 @@ false true true - wcrt.lib;openvr_api.lib;opengl32.lib;winmm.lib;kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;%(AdditionalDependencies) + wcrt.lib;wsock32.lib;openvr_api.lib;opengl32.lib;winmm.lib;kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;%(AdditionalDependencies) true false false @@ -199,6 +199,7 @@ + @@ -209,12 +210,14 @@ - - + + + + @@ -223,9 +226,9 @@ - + diff --git a/src/platform/win/OpenLara.vcxproj.filters b/src/platform/win/OpenLara.vcxproj.filters index 4f69173..2b14242 100644 --- a/src/platform/win/OpenLara.vcxproj.filters +++ b/src/platform/win/OpenLara.vcxproj.filters @@ -54,6 +54,9 @@ + + + diff --git a/src/utils.h b/src/utils.h index 905ef15..2932a4d 100644 --- a/src/utils.h +++ b/src/utils.h @@ -1638,6 +1638,27 @@ namespace String { } + +template +struct FixedStr { + char data[N]; + + void get(char *dst) { + memcpy(dst, data, sizeof(data)); + dst[sizeof(data)] = 0; + } + + FixedStr& operator = (const char *str) { + int len = min(sizeof(data), strlen(str)); + memset(data, 0, sizeof(data)); + memcpy(data, str, len); + return *this; + } +}; + +typedef FixedStr<16> str16; + + template struct Array { int capacity; @@ -1675,7 +1696,8 @@ struct Array { } void removeFast(int index) { - (*this)[index] = (*this)[--length]; + (*this)[index] = (*this)[length - 1]; + length--; } void remove(int index) {