From 678965f6776a9d3a1fff70d4ad99ad5910b7e220 Mon Sep 17 00:00:00 2001 From: Javidx9 <25419386+OneLoneCoder@users.noreply.github.com> Date: Sun, 14 Feb 2021 17:46:28 +0000 Subject: [PATCH] Added Networking Part 4 Video --- Videos/Networking/Parts3&4/MMO_Client.cpp | 278 +++++ Videos/Networking/Parts3&4/MMO_Common.h | 39 + Videos/Networking/Parts3&4/MMO_Server.cpp | 128 ++ Videos/Networking/Parts3&4/olcPGEX_Network.h | 1038 +++++++++++++++++ .../Parts3&4/olcPGEX_TransformedView.h | 658 +++++++++++ 5 files changed, 2141 insertions(+) create mode 100644 Videos/Networking/Parts3&4/MMO_Client.cpp create mode 100644 Videos/Networking/Parts3&4/MMO_Common.h create mode 100644 Videos/Networking/Parts3&4/MMO_Server.cpp create mode 100644 Videos/Networking/Parts3&4/olcPGEX_Network.h create mode 100644 Videos/Networking/Parts3&4/olcPGEX_TransformedView.h diff --git a/Videos/Networking/Parts3&4/MMO_Client.cpp b/Videos/Networking/Parts3&4/MMO_Client.cpp new file mode 100644 index 0000000..cd47177 --- /dev/null +++ b/Videos/Networking/Parts3&4/MMO_Client.cpp @@ -0,0 +1,278 @@ + +#include "../MMO_Server/MMO_Common.h" + +#define OLC_PGEX_TRANSFORMEDVIEW +#include "olcPGEX_TransformedView.h" + +#include + +class MMOGame : public olc::PixelGameEngine, olc::net::client_interface +{ +public: + MMOGame() + { + sAppName = "MMO Client"; + } + +private: + olc::TileTransformedView tv; + + std::string sWorldMap = + "################################" + "#..............................#" + "#..............................#" + "#..............................#" + "#..............................#" + "#..............................#" + "#..............................#" + "#..............................#" + "#..............................#" + "#..............................#" + "#..............................#" + "#..............................#" + "#..........####...####.........#" + "#..........#.........#.........#" + "#..........#.........#.........#" + "#..........#.........#.........#" + "#..........##############......#" + "#..............................#" + "#..................#.#.#.#.....#" + "#..............................#" + "#..................#.#.#.#.....#" + "#..............................#" + "#..............................#" + "#..............................#" + "#..............................#" + "#..............................#" + "#..............................#" + "#..............................#" + "#..............................#" + "#..............................#" + "#..............................#" + "################################"; + + olc::vi2d vWorldSize = { 32, 32 }; + +private: + std::unordered_map mapObjects; + uint32_t nPlayerID = 0; + sPlayerDescription descPlayer; + + bool bWaitingForConnection = true; + +public: + bool OnUserCreate() override + { + tv = olc::TileTransformedView({ ScreenWidth(), ScreenHeight() }, { 8, 8 }); + + //mapObjects[0].nUniqueID = 0; + //mapObjects[0].vPos = { 3.0f, 3.0f }; + + if (Connect("127.0.0.1", 60000)) + { + return true; + } + + return false; + } + + bool OnUserUpdate(float fElapsedTime) override + { + // Check for incoming network messages + if (IsConnected()) + { + while (!Incoming().empty()) + { + auto msg = Incoming().pop_front().msg; + + switch (msg.header.id) + { + case(GameMsg::Client_Accepted): + { + std::cout << "Server accepted client - you're in!\n"; + olc::net::message msg; + msg.header.id = GameMsg::Client_RegisterWithServer; + descPlayer.vPos = { 3.0f, 3.0f }; + msg << descPlayer; + Send(msg); + break; + } + + case(GameMsg::Client_AssignID): + { + // Server is assigning us OUR id + msg >> nPlayerID; + std::cout << "Assigned Client ID = " << nPlayerID << "\n"; + break; + } + + case(GameMsg::Game_AddPlayer): + { + sPlayerDescription desc; + msg >> desc; + mapObjects.insert_or_assign(desc.nUniqueID, desc); + + if (desc.nUniqueID == nPlayerID) + { + // Now we exist in game world + bWaitingForConnection = false; + } + break; + } + + case(GameMsg::Game_RemovePlayer): + { + uint32_t nRemovalID = 0; + msg >> nRemovalID; + mapObjects.erase(nRemovalID); + break; + } + + case(GameMsg::Game_UpdatePlayer): + { + sPlayerDescription desc; + msg >> desc; + mapObjects.insert_or_assign(desc.nUniqueID, desc); + break; + } + + + } + } + } + + if (bWaitingForConnection) + { + Clear(olc::DARK_BLUE); + DrawString({ 10,10 }, "Waiting To Connect...", olc::WHITE); + return true; + } + + + + + + + // Control of Player Object + mapObjects[nPlayerID].vVel = { 0.0f, 0.0f }; + if (GetKey(olc::Key::W).bHeld) mapObjects[nPlayerID].vVel += { 0.0f, -1.0f }; + if (GetKey(olc::Key::S).bHeld) mapObjects[nPlayerID].vVel += { 0.0f, +1.0f }; + if (GetKey(olc::Key::A).bHeld) mapObjects[nPlayerID].vVel += { -1.0f, 0.0f }; + if (GetKey(olc::Key::D).bHeld) mapObjects[nPlayerID].vVel += { +1.0f, 0.0f }; + + if (mapObjects[nPlayerID].vVel.mag2() > 0) + mapObjects[nPlayerID].vVel = mapObjects[nPlayerID].vVel.norm() * 4.0f; + + // Update objects locally + for (auto& object : mapObjects) + { + // Where will object be worst case? + olc::vf2d vPotentialPosition = object.second.vPos + object.second.vVel * fElapsedTime; + + // Extract region of world cells that could have collision this frame + olc::vi2d vCurrentCell = object.second.vPos.floor(); + olc::vi2d vTargetCell = vPotentialPosition; + olc::vi2d vAreaTL = (vCurrentCell.min(vTargetCell) - olc::vi2d(1, 1)).max({ 0,0 }); + olc::vi2d vAreaBR = (vCurrentCell.max(vTargetCell) + olc::vi2d(1, 1)).min(vWorldSize); + + // Iterate through each cell in test area + olc::vi2d vCell; + for (vCell.y = vAreaTL.y; vCell.y <= vAreaBR.y; vCell.y++) + { + for (vCell.x = vAreaTL.x; vCell.x <= vAreaBR.x; vCell.x++) + { + // Check if the cell is actually solid... + // olc::vf2d vCellMiddle = vCell.floor(); + if (sWorldMap[vCell.y * vWorldSize.x + vCell.x] == '#') + { + // ...it is! So work out nearest point to future player position, around perimeter + // of cell rectangle. We can test the distance to this point to see if we have + // collided. + + olc::vf2d vNearestPoint; + // Inspired by this (very clever btw) + // https://stackoverflow.com/questions/45370692/circle-rectangle-collision-response + vNearestPoint.x = std::max(float(vCell.x), std::min(vPotentialPosition.x, float(vCell.x + 1))); + vNearestPoint.y = std::max(float(vCell.y), std::min(vPotentialPosition.y, float(vCell.y + 1))); + + // But modified to work :P + olc::vf2d vRayToNearest = vNearestPoint - vPotentialPosition; + float fOverlap = object.second.fRadius - vRayToNearest.mag(); + if (std::isnan(fOverlap)) fOverlap = 0;// Thanks Dandistine! + + // If overlap is positive, then a collision has occurred, so we displace backwards by the + // overlap amount. The potential position is then tested against other tiles in the area + // therefore "statically" resolving the collision + if (fOverlap > 0) + { + // Statically resolve the collision + vPotentialPosition = vPotentialPosition - vRayToNearest.norm() * fOverlap; + } + } + } + } + + // Set the objects new position to the allowed potential position + object.second.vPos = vPotentialPosition; + } + + + + + + + // Handle Pan & Zoom + if (GetMouse(2).bPressed) tv.StartPan(GetMousePos()); + if (GetMouse(2).bHeld) tv.UpdatePan(GetMousePos()); + if (GetMouse(2).bReleased) tv.EndPan(GetMousePos()); + if (GetMouseWheel() > 0) tv.ZoomAtScreenPos(1.5f, GetMousePos()); + if (GetMouseWheel() < 0) tv.ZoomAtScreenPos(0.75f, GetMousePos()); + + // Clear World + Clear(olc::BLACK); + + // Draw World + olc::vi2d vTL = tv.GetTopLeftTile().max({ 0,0 }); + olc::vi2d vBR = tv.GetBottomRightTile().min(vWorldSize); + olc::vi2d vTile; + for (vTile.y = vTL.y; vTile.y < vBR.y; vTile.y++) + for (vTile.x = vTL.x; vTile.x < vBR.x; vTile.x++) + { + if (sWorldMap[vTile.y * vWorldSize.x + vTile.x] == '#') + { + tv.DrawRect(vTile, { 1.0f, 1.0f }); + tv.DrawRect(olc::vf2d(vTile) + olc::vf2d(0.1f, 0.1f), { 0.8f, 0.8f }); + } + } + + // Draw World Objects + for (auto& object : mapObjects) + { + // Draw Boundary + tv.DrawCircle(object.second.vPos, object.second.fRadius); + + // Draw Velocity + if (object.second.vVel.mag2() > 0) + tv.DrawLine(object.second.vPos, object.second.vPos + object.second.vVel.norm() * object.second.fRadius, olc::MAGENTA); + + // Draw Name + olc::vi2d vNameSize = GetTextSizeProp("ID: " + std::to_string(object.first)); + tv.DrawStringPropDecal(object.second.vPos - olc::vf2d{ vNameSize.x * 0.5f * 0.25f * 0.125f, -object.second.fRadius * 1.25f }, "ID: " + std::to_string(object.first), olc::BLUE, { 0.25f, 0.25f }); + } + + // Send player description + olc::net::message msg; + msg.header.id = GameMsg::Game_UpdatePlayer; + msg << mapObjects[nPlayerID]; + Send(msg); + return true; + } +}; + +int main() +{ + MMOGame demo; + if (demo.Construct(480, 480, 1, 1)) + demo.Start(); + return 0; +} \ No newline at end of file diff --git a/Videos/Networking/Parts3&4/MMO_Common.h b/Videos/Networking/Parts3&4/MMO_Common.h new file mode 100644 index 0000000..c7c7f2e --- /dev/null +++ b/Videos/Networking/Parts3&4/MMO_Common.h @@ -0,0 +1,39 @@ +#pragma once +#include + +#define OLC_PGE_APPLICATION +#include "olcPixelGameEngine.h" + +#define OLC_PGEX_NETWORK +#include "olcPGEX_Network.h" + +enum class GameMsg : uint32_t +{ + Server_GetStatus, + Server_GetPing, + + Client_Accepted, + Client_AssignID, + Client_RegisterWithServer, + Client_UnregisterWithServer, + + Game_AddPlayer, + Game_RemovePlayer, + Game_UpdatePlayer, +}; + +struct sPlayerDescription +{ + uint32_t nUniqueID = 0; + uint32_t nAvatarID = 0; + + uint32_t nHealth = 100; + uint32_t nAmmo = 20; + uint32_t nKills = 0; + uint32_t nDeaths = 0; + + float fRadius = 0.5f; + + olc::vf2d vPos; + olc::vf2d vVel; +}; \ No newline at end of file diff --git a/Videos/Networking/Parts3&4/MMO_Server.cpp b/Videos/Networking/Parts3&4/MMO_Server.cpp new file mode 100644 index 0000000..3390b82 --- /dev/null +++ b/Videos/Networking/Parts3&4/MMO_Server.cpp @@ -0,0 +1,128 @@ +#include +#include + +#include "MMO_Common.h" + +class GameServer : public olc::net::server_interface +{ +public: + GameServer(uint16_t nPort) : olc::net::server_interface(nPort) + { + } + + std::unordered_map m_mapPlayerRoster; + std::vector m_vGarbageIDs; + +protected: + bool OnClientConnect(std::shared_ptr> client) override + { + // For now we will allow all + return true; + } + + void OnClientValidated(std::shared_ptr> client) override + { + // Client passed validation check, so send them a message informing + // them they can continue to communicate + olc::net::message msg; + msg.header.id = GameMsg::Client_Accepted; + client->Send(msg); + } + + void OnClientDisconnect(std::shared_ptr> client) override + { + if (client) + { + if (m_mapPlayerRoster.find(client->GetID()) == m_mapPlayerRoster.end()) + { + // client never added to roster, so just let it disappear + } + else + { + auto& pd = m_mapPlayerRoster[client->GetID()]; + std::cout << "[UNGRACEFUL REMOVAL]:" + std::to_string(pd.nUniqueID) + "\n"; + m_mapPlayerRoster.erase(client->GetID()); + m_vGarbageIDs.push_back(client->GetID()); + } + } + + } + + void OnMessage(std::shared_ptr> client, olc::net::message& msg) override + { + if (!m_vGarbageIDs.empty()) + { + for (auto pid : m_vGarbageIDs) + { + olc::net::message m; + m.header.id = GameMsg::Game_RemovePlayer; + m << pid; + std::cout << "Removing " << pid << "\n"; + MessageAllClients(m); + } + m_vGarbageIDs.clear(); + } + + + + switch (msg.header.id) + { + case GameMsg::Client_RegisterWithServer: + { + sPlayerDescription desc; + msg >> desc; + desc.nUniqueID = client->GetID(); + m_mapPlayerRoster.insert_or_assign(desc.nUniqueID, desc); + + olc::net::message msgSendID; + msgSendID.header.id = GameMsg::Client_AssignID; + msgSendID << desc.nUniqueID; + MessageClient(client, msgSendID); + + olc::net::message msgAddPlayer; + msgAddPlayer.header.id = GameMsg::Game_AddPlayer; + msgAddPlayer << desc; + MessageAllClients(msgAddPlayer); + + for (const auto& player : m_mapPlayerRoster) + { + olc::net::message msgAddOtherPlayers; + msgAddOtherPlayers.header.id = GameMsg::Game_AddPlayer; + msgAddOtherPlayers << player.second; + MessageClient(client, msgAddOtherPlayers); + } + + break; + } + + case GameMsg::Client_UnregisterWithServer: + { + break; + } + + case GameMsg::Game_UpdatePlayer: + { + // Simply bounce update to everyone except incoming client + MessageAllClients(msg, client); + break; + } + + } + + } + +}; + + + +int main() +{ + GameServer server(60000); + server.Start(); + + while (1) + { + server.Update(-1, true); + } + return 0; +} \ No newline at end of file diff --git a/Videos/Networking/Parts3&4/olcPGEX_Network.h b/Videos/Networking/Parts3&4/olcPGEX_Network.h new file mode 100644 index 0000000..02fd6f0 --- /dev/null +++ b/Videos/Networking/Parts3&4/olcPGEX_Network.h @@ -0,0 +1,1038 @@ +/* + ASIO Based Networking olcPixelGameEngine Extension v1.0 + + Videos: + Part #1: https://youtu.be/2hNdkYInj4g + Part #2: https://youtu.be/UbjxGvrDrbw + Part #3: https://youtu.be/hHowZ3bWsio + Part #4: https://youtu.be/f_1lt9pfaEo + + License (OLC-3) + ~~~~~~~~~~~~~~~ + + Copyright 2018 - 2021 OneLoneCoder.com + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + + 1. Redistributions or derivations of source code must retain the above + copyright notice, this list of conditions and the following disclaimer. + + 2. Redistributions or derivative works in binary form must reproduce + the above copyright notice. This list of conditions and the following + disclaimer must be reproduced in the documentation and/or other + materials provided with the distribution. + + 3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + Links + ~~~~~ + YouTube: https://www.youtube.com/javidx9 + Discord: https://discord.gg/WhwHUMV + Twitter: https://www.twitter.com/javidx9 + Twitch: https://www.twitch.tv/javidx9 + GitHub: https://www.github.com/onelonecoder + Homepage: https://www.onelonecoder.com + + Author + ~~~~~~ + David Barr, aka javidx9, ŠOneLoneCoder 2019, 2020, 2021 + +*/ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef _WIN32 +#ifndef _WIN32_WINNT +#define _WIN32_WINNT 0x0A00 +#endif +#endif + +#define _WINSOCK_DEPRECATED_NO_WARNINGS +#define ASIO_STANDALONE +#include +#include +#include + +namespace olc +{ + namespace net + { + // Message + + // Message Header is sent at start of all messages. The template allows us + // to use "enum class" to ensure that the messages are valid at compile time + template + struct message_header + { + T id{}; + uint32_t size = 0; + }; + + // Message Body contains a header and a std::vector, containing raw bytes + // of infomation. This way the message can be variable length, but the size + // in the header must be updated. + template + struct message + { + // Header & Body vector + message_header header{}; + std::vector body; + + // returns size of entire message packet in bytes + size_t size() const + { + return body.size(); + } + + // Override for std::cout compatibility - produces friendly description of message + friend std::ostream& operator << (std::ostream& os, const message& msg) + { + os << "ID:" << int(msg.header.id) << " Size:" << msg.header.size; + return os; + } + + // Convenience Operator overloads - These allow us to add and remove stuff from + // the body vector as if it were a stack, so First in, Last Out. These are a + // template in itself, because we dont know what data type the user is pushing or + // popping, so lets allow them all. NOTE: It assumes the data type is fundamentally + // Plain Old Data (POD). TLDR: Serialise & Deserialise into/from a vector + + // Pushes any POD-like data into the message buffer + template + friend message& operator << (message& msg, const DataType& data) + { + // Check that the type of the data being pushed is trivially copyable + static_assert(std::is_standard_layout::value, "Data is too complex to be pushed into vector"); + + // Cache current size of vector, as this will be the point we insert the data + size_t i = msg.body.size(); + + // Resize the vector by the size of the data being pushed + msg.body.resize(msg.body.size() + sizeof(DataType)); + + // Physically copy the data into the newly allocated vector space + std::memcpy(msg.body.data() + i, &data, sizeof(DataType)); + + // Recalculate the message size + msg.header.size = msg.size(); + + // Return the target message so it can be "chained" + return msg; + } + + // Pulls any POD-like data form the message buffer + template + friend message& operator >> (message& msg, DataType& data) + { + // Check that the type of the data being pushed is trivially copyable + static_assert(std::is_standard_layout::value, "Data is too complex to be pulled from vector"); + + // Cache the location towards the end of the vector where the pulled data starts + size_t i = msg.body.size() - sizeof(DataType); + + // Physically copy the data from the vector into the user variable + std::memcpy(&data, msg.body.data() + i, sizeof(DataType)); + + // Shrink the vector to remove read bytes, and reset end position + msg.body.resize(i); + + // Recalculate the message size + msg.header.size = msg.size(); + + // Return the target message so it can be "chained" + return msg; + } + }; + + + // An "owned" message is identical to a regular message, but it is associated with + // a connection. On a server, the owner would be the client that sent the message, + // on a client the owner would be the server. + + // Forward declare the connection + template + class connection; + + template + struct owned_message + { + std::shared_ptr> remote = nullptr; + message msg; + + // Again, a friendly string maker + friend std::ostream& operator<<(std::ostream& os, const owned_message& msg) + { + os << msg.msg; + return os; + } + }; + + + // Queue + template + class tsqueue + { + public: + tsqueue() = default; + tsqueue(const tsqueue&) = delete; + virtual ~tsqueue() { clear(); } + + public: + // Returns and maintains item at front of Queue + const T& front() + { + std::scoped_lock lock(muxQueue); + return deqQueue.front(); + } + + // Returns and maintains item at back of Queue + const T& back() + { + std::scoped_lock lock(muxQueue); + return deqQueue.back(); + } + + // Removes and returns item from front of Queue + T pop_front() + { + std::scoped_lock lock(muxQueue); + auto t = std::move(deqQueue.front()); + deqQueue.pop_front(); + return t; + } + + // Removes and returns item from back of Queue + T pop_back() + { + std::scoped_lock lock(muxQueue); + auto t = std::move(deqQueue.back()); + deqQueue.pop_back(); + return t; + } + + // Adds an item to back of Queue + void push_back(const T& item) + { + std::scoped_lock lock(muxQueue); + deqQueue.emplace_back(std::move(item)); + + std::unique_lock ul(muxBlocking); + cvBlocking.notify_one(); + } + + // Adds an item to front of Queue + void push_front(const T& item) + { + std::scoped_lock lock(muxQueue); + deqQueue.emplace_front(std::move(item)); + + std::unique_lock ul(muxBlocking); + cvBlocking.notify_one(); + } + + // Returns true if Queue has no items + bool empty() + { + std::scoped_lock lock(muxQueue); + return deqQueue.empty(); + } + + // Returns number of items in Queue + size_t count() + { + std::scoped_lock lock(muxQueue); + return deqQueue.size(); + } + + // Clears Queue + void clear() + { + std::scoped_lock lock(muxQueue); + deqQueue.clear(); + } + + void wait() + { + while (empty()) + { + std::unique_lock ul(muxBlocking); + cvBlocking.wait(ul); + } + } + + protected: + std::mutex muxQueue; + std::deque deqQueue; + std::condition_variable cvBlocking; + std::mutex muxBlocking; + }; + + // Connection + // Forward declare + template + class server_interface; + + template + class connection : public std::enable_shared_from_this> + { + public: + // A connection is "owned" by either a server or a client, and its + // behaviour is slightly different bewteen the two. + enum class owner + { + server, + client + }; + + public: + // Constructor: Specify Owner, connect to context, transfer the socket + // Provide reference to incoming message queue + connection(owner parent, asio::io_context& asioContext, asio::ip::tcp::socket socket, tsqueue>& qIn) + : m_asioContext(asioContext), m_socket(std::move(socket)), m_qMessagesIn(qIn) + { + m_nOwnerType = parent; + + // Construct validation check data + if (m_nOwnerType == owner::server) + { + // Connection is Server -> Client, construct random data for the client + // to transform and send back for validation + m_nHandshakeOut = uint64_t(std::chrono::system_clock::now().time_since_epoch().count()); + + // Pre-calculate the result for checking when the client responds + m_nHandshakeCheck = scramble(m_nHandshakeOut); + } + else + { + // Connection is Client -> Server, so we have nothing to define, + m_nHandshakeIn = 0; + m_nHandshakeOut = 0; + } + } + + virtual ~connection() + {} + + // This ID is used system wide - its how clients will understand other clients + // exist across the whole system. + uint32_t GetID() const + { + return id; + } + + public: + void ConnectToClient(olc::net::server_interface* server, uint32_t uid = 0) + { + if (m_nOwnerType == owner::server) + { + if (m_socket.is_open()) + { + id = uid; + + // Was: ReadHeader(); + + // A client has attempted to connect to the server, but we wish + // the client to first validate itself, so first write out the + // handshake data to be validated + WriteValidation(); + + // Next, issue a task to sit and wait asynchronously for precisely + // the validation data sent back from the client + ReadValidation(server); + } + } + } + + void ConnectToServer(const asio::ip::tcp::resolver::results_type& endpoints) + { + // Only clients can connect to servers + if (m_nOwnerType == owner::client) + { + // Request asio attempts to connect to an endpoint + asio::async_connect(m_socket, endpoints, + [this](std::error_code ec, asio::ip::tcp::endpoint endpoint) + { + if (!ec) + { + // Was: ReadHeader(); + + // First thing server will do is send packet to be validated + // so wait for that and respond + ReadValidation(); + } + }); + } + } + + + void Disconnect() + { + if (IsConnected()) + asio::post(m_asioContext, [this]() { m_socket.close(); }); + } + + bool IsConnected() const + { + return m_socket.is_open(); + } + + // Prime the connection to wait for incoming messages + void StartListening() + { + + } + + public: + // ASYNC - Send a message, connections are one-to-one so no need to specifiy + // the target, for a client, the target is the server and vice versa + void Send(const message& msg) + { + asio::post(m_asioContext, + [this, msg]() + { + // If the queue has a message in it, then we must + // assume that it is in the process of asynchronously being written. + // Either way add the message to the queue to be output. If no messages + // were available to be written, then start the process of writing the + // message at the front of the queue. + bool bWritingMessage = !m_qMessagesOut.empty(); + m_qMessagesOut.push_back(msg); + if (!bWritingMessage) + { + WriteHeader(); + } + }); + } + + + + private: + // ASYNC - Prime context to write a message header + void WriteHeader() + { + // If this function is called, we know the outgoing message queue must have + // at least one message to send. So allocate a transmission buffer to hold + // the message, and issue the work - asio, send these bytes + asio::async_write(m_socket, asio::buffer(&m_qMessagesOut.front().header, sizeof(message_header)), + [this](std::error_code ec, std::size_t length) + { + // asio has now sent the bytes - if there was a problem + // an error would be available... + if (!ec) + { + // ... no error, so check if the message header just sent also + // has a message body... + if (m_qMessagesOut.front().body.size() > 0) + { + // ...it does, so issue the task to write the body bytes + WriteBody(); + } + else + { + // ...it didnt, so we are done with this message. Remove it from + // the outgoing message queue + m_qMessagesOut.pop_front(); + + // If the queue is not empty, there are more messages to send, so + // make this happen by issuing the task to send the next header. + if (!m_qMessagesOut.empty()) + { + WriteHeader(); + } + } + } + else + { + // ...asio failed to write the message, we could analyse why but + // for now simply assume the connection has died by closing the + // socket. When a future attempt to write to this client fails due + // to the closed socket, it will be tidied up. + std::cout << "[" << id << "] Write Header Fail.\n"; + m_socket.close(); + } + }); + } + + // ASYNC - Prime context to write a message body + void WriteBody() + { + // If this function is called, a header has just been sent, and that header + // indicated a body existed for this message. Fill a transmission buffer + // with the body data, and send it! + asio::async_write(m_socket, asio::buffer(m_qMessagesOut.front().body.data(), m_qMessagesOut.front().body.size()), + [this](std::error_code ec, std::size_t length) + { + if (!ec) + { + // Sending was successful, so we are done with the message + // and remove it from the queue + m_qMessagesOut.pop_front(); + + // If the queue still has messages in it, then issue the task to + // send the next messages' header. + if (!m_qMessagesOut.empty()) + { + WriteHeader(); + } + } + else + { + // Sending failed, see WriteHeader() equivalent for description :P + std::cout << "[" << id << "] Write Body Fail.\n"; + m_socket.close(); + } + }); + } + + // ASYNC - Prime context ready to read a message header + void ReadHeader() + { + // If this function is called, we are expecting asio to wait until it receives + // enough bytes to form a header of a message. We know the headers are a fixed + // size, so allocate a transmission buffer large enough to store it. In fact, + // we will construct the message in a "temporary" message object as it's + // convenient to work with. + asio::async_read(m_socket, asio::buffer(&m_msgTemporaryIn.header, sizeof(message_header)), + [this](std::error_code ec, std::size_t length) + { + if (!ec) + { + // A complete message header has been read, check if this message + // has a body to follow... + if (m_msgTemporaryIn.header.size > 0) + { + // ...it does, so allocate enough space in the messages' body + // vector, and issue asio with the task to read the body. + m_msgTemporaryIn.body.resize(m_msgTemporaryIn.header.size); + ReadBody(); + } + else + { + // it doesn't, so add this bodyless message to the connections + // incoming message queue + AddToIncomingMessageQueue(); + } + } + else + { + // Reading form the client went wrong, most likely a disconnect + // has occurred. Close the socket and let the system tidy it up later. + std::cout << "[" << id << "] Read Header Fail.\n"; + m_socket.close(); + } + }); + } + + // ASYNC - Prime context ready to read a message body + void ReadBody() + { + // If this function is called, a header has already been read, and that header + // request we read a body, The space for that body has already been allocated + // in the temporary message object, so just wait for the bytes to arrive... + asio::async_read(m_socket, asio::buffer(m_msgTemporaryIn.body.data(), m_msgTemporaryIn.body.size()), + [this](std::error_code ec, std::size_t length) + { + if (!ec) + { + // ...and they have! The message is now complete, so add + // the whole message to incoming queue + AddToIncomingMessageQueue(); + } + else + { + // As above! + std::cout << "[" << id << "] Read Body Fail.\n"; + m_socket.close(); + } + }); + } + + // "Encrypt" data + uint64_t scramble(uint64_t nInput) + { + uint64_t out = nInput ^ 0xDEADBEEFC0DECAFE; + out = (out & 0xF0F0F0F0F0F0F0) >> 4 | (out & 0x0F0F0F0F0F0F0F) << 4; + return out ^ 0xC0DEFACE12345678; + } + + // ASYNC - Used by both client and server to write validation packet + void WriteValidation() + { + asio::async_write(m_socket, asio::buffer(&m_nHandshakeOut, sizeof(uint64_t)), + [this](std::error_code ec, std::size_t length) + { + if (!ec) + { + // Validation data sent, clients should sit and wait + // for a response (or a closure) + if (m_nOwnerType == owner::client) + ReadHeader(); + } + else + { + m_socket.close(); + } + }); + } + + void ReadValidation(olc::net::server_interface* server = nullptr) + { + asio::async_read(m_socket, asio::buffer(&m_nHandshakeIn, sizeof(uint64_t)), + [this, server](std::error_code ec, std::size_t length) + { + if (!ec) + { + if (m_nOwnerType == owner::server) + { + // Connection is a server, so check response from client + + // Compare sent data to actual solution + if (m_nHandshakeIn == m_nHandshakeCheck) + { + // Client has provided valid solution, so allow it to connect properly + std::cout << "Client Validated" << std::endl; + server->OnClientValidated(this->shared_from_this()); + + // Sit waiting to receive data now + ReadHeader(); + } + else + { + // Client gave incorrect data, so disconnect + std::cout << "Client Disconnected (Fail Validation)" << std::endl; + m_socket.close(); + } + } + else + { + // Connection is a client, so solve puzzle + m_nHandshakeOut = scramble(m_nHandshakeIn); + + // Write the result + WriteValidation(); + } + } + else + { + // Some biggerfailure occured + std::cout << "Client Disconnected (ReadValidation)" << std::endl; + m_socket.close(); + } + }); + } + + // Once a full message is received, add it to the incoming queue + void AddToIncomingMessageQueue() + { + // Shove it in queue, converting it to an "owned message", by initialising + // with the a shared pointer from this connection object + if(m_nOwnerType == owner::server) + m_qMessagesIn.push_back({ this->shared_from_this(), m_msgTemporaryIn }); + else + m_qMessagesIn.push_back({ nullptr, m_msgTemporaryIn }); + + // We must now prime the asio context to receive the next message. It + // wil just sit and wait for bytes to arrive, and the message construction + // process repeats itself. Clever huh? + ReadHeader(); + } + + protected: + // Each connection has a unique socket to a remote + asio::ip::tcp::socket m_socket; + + // This context is shared with the whole asio instance + asio::io_context& m_asioContext; + + // This queue holds all messages to be sent to the remote side + // of this connection + tsqueue> m_qMessagesOut; + + // This references the incoming queue of the parent object + tsqueue>& m_qMessagesIn; + + // Incoming messages are constructed asynchronously, so we will + // store the part assembled message here, until it is ready + message m_msgTemporaryIn; + + // The "owner" decides how some of the connection behaves + owner m_nOwnerType = owner::server; + + // Handshake Validation + uint64_t m_nHandshakeOut = 0; + uint64_t m_nHandshakeIn = 0; + uint64_t m_nHandshakeCheck = 0; + + + bool m_bValidHandshake = false; + bool m_bConnectionEstablished = false; + + uint32_t id = 0; + + }; + + // Client + template + class client_interface + { + public: + client_interface() + {} + + virtual ~client_interface() + { + // If the client is destroyed, always try and disconnect from server + Disconnect(); + } + + public: + // Connect to server with hostname/ip-address and port + bool Connect(const std::string& host, const uint16_t port) + { + try + { + // Resolve hostname/ip-address into tangiable physical address + asio::ip::tcp::resolver resolver(m_context); + asio::ip::tcp::resolver::results_type endpoints = resolver.resolve(host, std::to_string(port)); + + // Create connection + m_connection = std::make_unique>(connection::owner::client, m_context, asio::ip::tcp::socket(m_context), m_qMessagesIn); + + // Tell the connection object to connect to server + m_connection->ConnectToServer(endpoints); + + // Start Context Thread + thrContext = std::thread([this]() { m_context.run(); }); + } + catch (std::exception& e) + { + std::cerr << "Client Exception: " << e.what() << "\n"; + return false; + } + return true; + } + + // Disconnect from server + void Disconnect() + { + // If connection exists, and it's connected then... + if(IsConnected()) + { + // ...disconnect from server gracefully + m_connection->Disconnect(); + } + + // Either way, we're also done with the asio context... + m_context.stop(); + // ...and its thread + if (thrContext.joinable()) + thrContext.join(); + + // Destroy the connection object + m_connection.release(); + } + + // Check if client is actually connected to a server + bool IsConnected() + { + if (m_connection) + return m_connection->IsConnected(); + else + return false; + } + + public: + // Send message to server + void Send(const message& msg) + { + if (IsConnected()) + m_connection->Send(msg); + } + + // Retrieve queue of messages from server + tsqueue>& Incoming() + { + return m_qMessagesIn; + } + + protected: + // asio context handles the data transfer... + asio::io_context m_context; + // ...but needs a thread of its own to execute its work commands + std::thread thrContext; + // The client has a single instance of a "connection" object, which handles data transfer + std::unique_ptr> m_connection; + + private: + // This is the thread safe queue of incoming messages from server + tsqueue> m_qMessagesIn; + }; + + // Server + template + class server_interface + { + public: + // Create a server, ready to listen on specified port + server_interface(uint16_t port) + : m_asioAcceptor(m_asioContext, asio::ip::tcp::endpoint(asio::ip::tcp::v4(), port)) + { + + } + + virtual ~server_interface() + { + // May as well try and tidy up + Stop(); + } + + // Starts the server! + bool Start() + { + try + { + // Issue a task to the asio context - This is important + // as it will prime the context with "work", and stop it + // from exiting immediately. Since this is a server, we + // want it primed ready to handle clients trying to + // connect. + WaitForClientConnection(); + + // Launch the asio context in its own thread + m_threadContext = std::thread([this]() { m_asioContext.run(); }); + } + catch (std::exception& e) + { + // Something prohibited the server from listening + std::cerr << "[SERVER] Exception: " << e.what() << "\n"; + return false; + } + + std::cout << "[SERVER] Started!\n"; + return true; + } + + // Stops the server! + void Stop() + { + // Request the context to close + m_asioContext.stop(); + + // Tidy up the context thread + if (m_threadContext.joinable()) m_threadContext.join(); + + // Inform someone, anybody, if they care... + std::cout << "[SERVER] Stopped!\n"; + } + + // ASYNC - Instruct asio to wait for connection + void WaitForClientConnection() + { + // Prime context with an instruction to wait until a socket connects. This + // is the purpose of an "acceptor" object. It will provide a unique socket + // for each incoming connection attempt + m_asioAcceptor.async_accept( + [this](std::error_code ec, asio::ip::tcp::socket socket) + { + // Triggered by incoming connection request + if (!ec) + { + // Display some useful(?) information + std::cout << "[SERVER] New Connection: " << socket.remote_endpoint() << "\n"; + + // Create a new connection to handle this client + std::shared_ptr> newconn = + std::make_shared>(connection::owner::server, + m_asioContext, std::move(socket), m_qMessagesIn); + + + + // Give the user server a chance to deny connection + if (OnClientConnect(newconn)) + { + // Connection allowed, so add to container of new connections + m_deqConnections.push_back(std::move(newconn)); + + // And very important! Issue a task to the connection's + // asio context to sit and wait for bytes to arrive! + m_deqConnections.back()->ConnectToClient(this, nIDCounter++); + + std::cout << "[" << m_deqConnections.back()->GetID() << "] Connection Approved\n"; + } + else + { + std::cout << "[-----] Connection Denied\n"; + + // Connection will go out of scope with no pending tasks, so will + // get destroyed automagically due to the wonder of smart pointers + } + } + else + { + // Error has occurred during acceptance + std::cout << "[SERVER] New Connection Error: " << ec.message() << "\n"; + } + + // Prime the asio context with more work - again simply wait for + // another connection... + WaitForClientConnection(); + }); + } + + // Send a message to a specific client + void MessageClient(std::shared_ptr> client, const message& msg) + { + // Check client is legitimate... + if (client && client->IsConnected()) + { + // ...and post the message via the connection + client->Send(msg); + } + else + { + // If we cant communicate with client then we may as + // well remove the client - let the server know, it may + // be tracking it somehow + OnClientDisconnect(client); + + // Off you go now, bye bye! + client.reset(); + + // Then physically remove it from the container + m_deqConnections.erase( + std::remove(m_deqConnections.begin(), m_deqConnections.end(), client), m_deqConnections.end()); + } + } + + // Send message to all clients + void MessageAllClients(const message& msg, std::shared_ptr> pIgnoreClient = nullptr) + { + bool bInvalidClientExists = false; + + // Iterate through all clients in container + for (auto& client : m_deqConnections) + { + // Check client is connected... + if (client && client->IsConnected()) + { + // ..it is! + if(client != pIgnoreClient) + client->Send(msg); + } + else + { + // The client couldnt be contacted, so assume it has + // disconnected. + OnClientDisconnect(client); + client.reset(); + + // Set this flag to then remove dead clients from container + bInvalidClientExists = true; + } + } + + // Remove dead clients, all in one go - this way, we dont invalidate the + // container as we iterated through it. + if (bInvalidClientExists) + m_deqConnections.erase( + std::remove(m_deqConnections.begin(), m_deqConnections.end(), nullptr), m_deqConnections.end()); + } + + // Force server to respond to incoming messages + void Update(size_t nMaxMessages = -1, bool bWait = false) + { + if (bWait) m_qMessagesIn.wait(); + + // Process as many messages as you can up to the value + // specified + size_t nMessageCount = 0; + while (nMessageCount < nMaxMessages && !m_qMessagesIn.empty()) + { + // Grab the front message + auto msg = m_qMessagesIn.pop_front(); + + // Pass to message handler + OnMessage(msg.remote, msg.msg); + + nMessageCount++; + } + } + + protected: + // This server class should override thse functions to implement + // customised functionality + + // Called when a client connects, you can veto the connection by returning false + virtual bool OnClientConnect(std::shared_ptr> client) + { + return false; + } + + // Called when a client appears to have disconnected + virtual void OnClientDisconnect(std::shared_ptr> client) + { + + } + + // Called when a message arrives + virtual void OnMessage(std::shared_ptr> client, message& msg) + { + + } + + public: + // Called when a client is validated + virtual void OnClientValidated(std::shared_ptr> client) + { + + } + + + protected: + // Thread Safe Queue for incoming message packets + tsqueue> m_qMessagesIn; + + // Container of active validated connections + std::deque>> m_deqConnections; + + // Order of declaration is important - it is also the order of initialisation + asio::io_context m_asioContext; + std::thread m_threadContext; + + // These things need an asio context + asio::ip::tcp::acceptor m_asioAcceptor; // Handles new incoming connection attempts... + + // Clients will be identified in the "wider system" via an ID + uint32_t nIDCounter = 10000; + }; + } +} + + diff --git a/Videos/Networking/Parts3&4/olcPGEX_TransformedView.h b/Videos/Networking/Parts3&4/olcPGEX_TransformedView.h new file mode 100644 index 0000000..e1e2f17 --- /dev/null +++ b/Videos/Networking/Parts3&4/olcPGEX_TransformedView.h @@ -0,0 +1,658 @@ +/* + olcPGEX_TransformedView.h + + +-------------------------------------------------------------+ + | OneLoneCoder Pixel Game Engine Extension | + | Transformed View v1.00 | + +-------------------------------------------------------------+ + + NOTE: UNDER ACTIVE DEVELOPMENT - THERE ARE BUGS/GLITCHES + + What is this? + ~~~~~~~~~~~~~ + This extension provides drawing routines that are compatible with + changeable world and screen spaces. For example you can pan and + zoom, and all PGE drawing routines will automatically adopt the current + world scales and offsets. + + License (OLC-3) + ~~~~~~~~~~~~~~~ + + Copyright 2018 - 2021 OneLoneCoder.com + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + + 1. Redistributions or derivations of source code must retain the above + copyright notice, this list of conditions and the following disclaimer. + + 2. Redistributions or derivative works in binary form must reproduce + the above copyright notice. This list of conditions and the following + disclaimer must be reproduced in the documentation and/or other + materials provided with the distribution. + + 3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + Links + ~~~~~ + YouTube: https://www.youtube.com/javidx9 + Discord: https://discord.gg/WhwHUMV + Twitter: https://www.twitter.com/javidx9 + Twitch: https://www.twitch.tv/javidx9 + GitHub: https://www.github.com/onelonecoder + Homepage: https://www.onelonecoder.com + + Author + ~~~~~~ + David Barr, aka javidx9, ŠOneLoneCoder 2019, 2020, 2021 + + Revisions: + 1.00: Initial Release +*/ + +#pragma once +#ifndef OLC_PGEX_TRANSFORMEDVIEW_H +#define OLC_PGEX_TRANSFORMEDVIEW_H + +#include "olcPixelGameEngine.h" + + + +namespace olc +{ + class TransformedView : public olc::PGEX + { + public: + TransformedView() = default; + virtual void Initialise(const olc::vi2d& vViewArea, const olc::vf2d& vPixelScale = { 1.0f, 1.0f }); + + public: + void SetWorldOffset(const olc::vf2d& vOffset); + void MoveWorldOffset(const olc::vf2d& vDeltaOffset); + void SetWorldScale(const olc::vf2d& vScale); + void SetViewArea(const olc::vi2d& vViewArea); + olc::vf2d GetWorldTL() const; + olc::vf2d GetWorldBR() const; + olc::vf2d GetWorldVisibleArea() const; + void ZoomAtScreenPos(const float fDeltaZoom, const olc::vi2d& vPos); + void SetZoom(const float fZoom, const olc::vi2d& vPos); + void StartPan(const olc::vi2d& vPos); + void UpdatePan(const olc::vi2d& vPos); + void EndPan(const olc::vi2d& vPos); + const olc::vf2d& GetWorldOffset() const; + const olc::vf2d& GetWorldScale() const; + virtual olc::vi2d WorldToScreen(const olc::vf2d& vWorldPos) const; + virtual olc::vf2d ScreenToWorld(const olc::vi2d& vScreenPos) const; + virtual olc::vf2d ScaleToWorld(const olc::vi2d& vScreenSize) const; + virtual olc::vi2d ScaleToScreen(const olc::vf2d& vWorldSize) const; + virtual bool IsPointVisible(const olc::vf2d& vPos) const; + virtual bool IsRectVisible(const olc::vf2d& vPos, const olc::vf2d& vSize) const; + + protected: + olc::vf2d m_vWorldOffset = { 0.0f, 0.0f }; + olc::vf2d m_vWorldScale = { 1.0f, 1.0f }; + olc::vf2d m_vRecipPixel = { 1.0f, 1.0f }; + olc::vf2d m_vPixelScale = { 1.0f, 1.0f }; + bool m_bPanning = false; + olc::vf2d m_vStartPan = { 0.0f, 0.0f }; + olc::vi2d m_vViewArea; + + public: // Hopefully, these should look familiar! + // Plots a single point + virtual bool Draw(float x, float y, olc::Pixel p = olc::WHITE); + bool Draw(const olc::vf2d& pos, olc::Pixel p = olc::WHITE); + // Draws a line from (x1,y1) to (x2,y2) + void DrawLine(float x1, float y1, float x2, float y2, olc::Pixel p = olc::WHITE, uint32_t pattern = 0xFFFFFFFF); + void DrawLine(const olc::vf2d& pos1, const olc::vf2d& pos2, olc::Pixel p = olc::WHITE, uint32_t pattern = 0xFFFFFFFF); + // Draws a circle located at (x,y) with radius + void DrawCircle(float x, float y, float radius, olc::Pixel p = olc::WHITE, uint8_t mask = 0xFF); + void DrawCircle(const olc::vf2d& pos, float radius, olc::Pixel p = olc::WHITE, uint8_t mask = 0xFF); + // Fills a circle located at (x,y) with radius + void FillCircle(float x, float y, float radius, olc::Pixel p = olc::WHITE); + void FillCircle(const olc::vf2d& pos, float radius, olc::Pixel p = olc::WHITE); + // Draws a rectangle at (x,y) to (x+w,y+h) + void DrawRect(float x, float y, float w, float h, olc::Pixel p = olc::WHITE); + void DrawRect(const olc::vf2d& pos, const olc::vf2d& size, olc::Pixel p = olc::WHITE); + // Fills a rectangle at (x,y) to (x+w,y+h) + void FillRect(float x, float y, float w, float h, olc::Pixel p = olc::WHITE); + void FillRect(const olc::vf2d& pos, const olc::vf2d& size, olc::Pixel p = olc::WHITE); + // Draws a triangle between points (x1,y1), (x2,y2) and (x3,y3) + void DrawTriangle(float x1, float y1, float x2, float y2, float x3, float y3, olc::Pixel p = olc::WHITE); + void DrawTriangle(const olc::vf2d& pos1, const olc::vf2d& pos2, const olc::vf2d& pos3, olc::Pixel p = olc::WHITE); + // Flat fills a triangle between points (x1,y1), (x2,y2) and (x3,y3) + void FillTriangle(float x1, float y1, float x2, float y2, float x3, float y3, olc::Pixel p = olc::WHITE); + void FillTriangle(const olc::vf2d& pos1, const olc::vf2d& pos2, const olc::vf2d& pos3, olc::Pixel p = olc::WHITE); + // Draws an entire sprite at location (x,y) + void DrawSprite(float x, float y, olc::Sprite* sprite, float scalex = 1, float scaley = 1, uint8_t flip = olc::Sprite::NONE); + void DrawSprite(const olc::vf2d& pos, olc::Sprite* sprite, const olc::vf2d& scale = { 1.0f, 1.0f }, uint8_t flip = olc::Sprite::NONE); + // Draws an area of a sprite at location (x,y), where the + // selected area is (ox,oy) to (ox+w,oy+h) + void DrawPartialSprite(float x, float y, Sprite* sprite, int32_t ox, int32_t oy, int32_t w, int32_t h, float scalex = 1, float scaley = 1, uint8_t flip = olc::Sprite::NONE); + void DrawPartialSprite(const olc::vf2d& pos, Sprite* sprite, const olc::vi2d& sourcepos, const olc::vi2d& size, const olc::vf2d& scale = { 1.0f, 1.0f }, uint8_t flip = olc::Sprite::NONE); + void DrawString(float x, float y, const std::string& sText, Pixel col, const olc::vf2d& scale); + void DrawString(const olc::vf2d& pos, const std::string& sText, const Pixel col, const olc::vf2d& scale); + + + // Draws a whole decal, with optional scale and tinting + void DrawDecal(const olc::vf2d& pos, olc::Decal* decal, const olc::vf2d& scale = { 1.0f,1.0f }, const olc::Pixel& tint = olc::WHITE); + // Draws a region of a decal, with optional scale and tinting + void DrawPartialDecal(const olc::vf2d& pos, olc::Decal* decal, const olc::vf2d& source_pos, const olc::vf2d& source_size, const olc::vf2d& scale = { 1.0f,1.0f }, const olc::Pixel& tint = olc::WHITE); + void DrawPartialDecal(const olc::vf2d& pos, const olc::vf2d& size, olc::Decal* decal, const olc::vf2d& source_pos, const olc::vf2d& source_size, const olc::Pixel& tint = olc::WHITE); + // Draws fully user controlled 4 vertices, pos(pixels), uv(pixels), colours + void DrawExplicitDecal(olc::Decal* decal, const olc::vf2d* pos, const olc::vf2d* uv, const olc::Pixel* col, uint32_t elements = 4); + //// Draws a decal with 4 arbitrary points, warping the texture to look "correct" + void DrawWarpedDecal(olc::Decal* decal, const olc::vf2d(&pos)[4], const olc::Pixel& tint = olc::WHITE); + void DrawWarpedDecal(olc::Decal* decal, const olc::vf2d* pos, const olc::Pixel& tint = olc::WHITE); + void DrawWarpedDecal(olc::Decal* decal, const std::array& pos, const olc::Pixel& tint = olc::WHITE); + //// As above, but you can specify a region of a decal source sprite + void DrawPartialWarpedDecal(olc::Decal* decal, const olc::vf2d(&pos)[4], const olc::vf2d& source_pos, const olc::vf2d& source_size, const olc::Pixel& tint = olc::WHITE); + void DrawPartialWarpedDecal(olc::Decal* decal, const olc::vf2d* pos, const olc::vf2d& source_pos, const olc::vf2d& source_size, const olc::Pixel& tint = olc::WHITE); + void DrawPartialWarpedDecal(olc::Decal* decal, const std::array& pos, const olc::vf2d& source_pos, const olc::vf2d& source_size, const olc::Pixel& tint = olc::WHITE); + //// Draws a decal rotated to specified angle, wit point of rotation offset + void DrawRotatedDecal(const olc::vf2d& pos, olc::Decal* decal, const float fAngle, const olc::vf2d& center = { 0.0f, 0.0f }, const olc::vf2d& scale = { 1.0f,1.0f }, const olc::Pixel& tint = olc::WHITE); + void DrawPartialRotatedDecal(const olc::vf2d& pos, olc::Decal* decal, const float fAngle, const olc::vf2d& center, const olc::vf2d& source_pos, const olc::vf2d& source_size, const olc::vf2d& scale = { 1.0f, 1.0f }, const olc::Pixel& tint = olc::WHITE); + // Draws a multiline string as a decal, with tiniting and scaling + void DrawStringDecal(const olc::vf2d& pos, const std::string& sText, const olc::Pixel col = olc::WHITE, const olc::vf2d& scale = { 1.0f, 1.0f }); + void DrawStringPropDecal(const olc::vf2d& pos, const std::string& sText, const olc::Pixel col = olc::WHITE, const olc::vf2d& scale = { 1.0f, 1.0f }); + // Draws a single shaded filled rectangle as a decal + void FillRectDecal(const olc::vf2d& pos, const olc::vf2d& size, const olc::Pixel col = olc::WHITE); + // Draws a corner shaded rectangle as a decal + void GradientFillRectDecal(const olc::vf2d& pos, const olc::vf2d& size, const olc::Pixel colTL, const olc::Pixel colBL, const olc::Pixel colBR, const olc::Pixel colTR); + // Draws an arbitrary convex textured polygon using GPU + void DrawPolygonDecal(olc::Decal* decal, const std::vector& pos, const std::vector& uv, const olc::Pixel tint = olc::WHITE); + + }; + + class TileTransformedView : public TransformedView + { + public: + TileTransformedView() = default; + TileTransformedView(const olc::vi2d& vViewArea, const olc::vi2d& vTileSize); + + public: + void SetRangeX(const bool bRanged, const int32_t nMin = 0, const int32_t nMax = 0); + void SetRangeY(const bool bRanged, const int32_t nMin = 0, const int32_t nMax = 0); + olc::vi2d GetTopLeftTile() const; + olc::vi2d GetBottomRightTile() const; + olc::vi2d GetVisibleTiles() const; + olc::vi2d GetTileUnderScreenPos(const olc::vi2d& vPos) const; + const olc::vi2d GetTileOffset() const; + + private: + bool m_bRangedX = false; + int32_t m_nMinRangeX = 0; + int32_t m_nMaxRangeX = 0; + bool m_bRangedY = false; + int32_t m_nMinRangeY = 0; + int32_t m_nMaxRangeY = 0; + }; +} + +#ifdef OLC_PGEX_TRANSFORMEDVIEW +#undef OLC_PGEX_TRANSFORMEDVIEW + +namespace olc +{ + + void TransformedView::Initialise(const olc::vi2d& vViewArea, const olc::vf2d& vPixelScale) + { + SetViewArea(vViewArea); + SetWorldScale(vPixelScale); + m_vPixelScale = vPixelScale; + m_vRecipPixel = 1.0f / m_vPixelScale; + } + + void TransformedView::SetWorldOffset(const olc::vf2d& vOffset) + { + m_vWorldOffset = vOffset; + } + + void TransformedView::MoveWorldOffset(const olc::vf2d& vDeltaOffset) + { + m_vWorldOffset += vDeltaOffset; + } + + void TransformedView::SetWorldScale(const olc::vf2d& vScale) + { + m_vWorldScale = vScale; + } + + void TransformedView::SetViewArea(const olc::vi2d& vViewArea) + { + m_vViewArea = vViewArea; + } + + olc::vf2d TransformedView::GetWorldTL() const + { + return ScreenToWorld({ 0,0 }); + } + + olc::vf2d TransformedView::GetWorldBR() const + { + return TransformedView::ScreenToWorld(m_vViewArea); + } + + olc::vf2d TransformedView::GetWorldVisibleArea() const + { + return GetWorldBR() - GetWorldTL(); + } + + void TransformedView::ZoomAtScreenPos(const float fDeltaZoom, const olc::vi2d& vPos) + { + olc::vf2d vOffsetBeforeZoom = ScreenToWorld(vPos); + m_vWorldScale *= fDeltaZoom; + olc::vf2d vOffsetAfterZoom = ScreenToWorld(vPos); + m_vWorldOffset += vOffsetBeforeZoom - vOffsetAfterZoom; + } + + void TransformedView::SetZoom(const float fZoom, const olc::vi2d& vPos) + { + olc::vf2d vOffsetBeforeZoom = ScreenToWorld(vPos); + m_vWorldScale = { fZoom, fZoom }; + olc::vf2d vOffsetAfterZoom = ScreenToWorld(vPos); + m_vWorldOffset += vOffsetBeforeZoom - vOffsetAfterZoom; + } + + void TransformedView::StartPan(const olc::vi2d& vPos) + { + m_bPanning = true; + m_vStartPan = olc::vf2d(vPos); + } + + void TransformedView::UpdatePan(const olc::vi2d& vPos) + { + if (m_bPanning) + { + m_vWorldOffset -= (olc::vf2d(vPos) - m_vStartPan) / m_vWorldScale; + m_vStartPan = olc::vf2d(vPos); + } + } + + void TransformedView::EndPan(const olc::vi2d& vPos) + { + UpdatePan(vPos); + m_bPanning = false; + } + + const olc::vf2d& TransformedView::GetWorldOffset() const + { + return m_vWorldOffset; + } + + const olc::vf2d& TransformedView::GetWorldScale() const + { + return m_vWorldScale; + } + + olc::vi2d TransformedView::WorldToScreen(const olc::vf2d& vWorldPos) const + { + olc::vf2d vFloat = ((vWorldPos - m_vWorldOffset) * m_vWorldScale); + vFloat = { std::floor(vFloat.x), std::floor(vFloat.y) }; + return vFloat; + } + + olc::vf2d TransformedView::ScreenToWorld(const olc::vi2d& vScreenPos) const + { + return (olc::vf2d(vScreenPos) / m_vWorldScale) + m_vWorldOffset; + } + + olc::vf2d TransformedView::ScaleToWorld(const olc::vi2d& vScreenSize) const + { + return (olc::vf2d(vScreenSize) / m_vWorldScale); + } + + olc::vi2d TransformedView::ScaleToScreen(const olc::vf2d& vWorldSize) const + { + olc::vf2d vFloat = vWorldSize * m_vWorldScale; + return vFloat.floor(); + } + + bool TransformedView::IsPointVisible(const olc::vf2d & vPos) const + { + olc::vi2d vScreen = WorldToScreen(vPos); + return vScreen.x >= 0 && vScreen.x < m_vViewArea.x&& vScreen.y >= 0 && vScreen.y < m_vViewArea.y; + } + + bool TransformedView::IsRectVisible(const olc::vf2d& vPos, const olc::vf2d& vSize) const + { + olc::vi2d vScreenPos = WorldToScreen(vPos); + olc::vi2d vScreenSize = vSize * m_vWorldScale; + return (vScreenPos.x < 0 + m_vViewArea.x && vScreenPos.x + vScreenSize.x > 0 && vScreenPos.y < m_vViewArea.y&& vScreenPos.y + vScreenSize.y > 0); + } + + bool TransformedView::Draw(float x, float y, olc::Pixel p) + { + return Draw({ x, y }, p); + } + + bool TransformedView::Draw(const olc::vf2d & pos, olc::Pixel p) + { + return pge->Draw(WorldToScreen(pos), p); + } + + void TransformedView::DrawLine(float x1, float y1, float x2, float y2, olc::Pixel p, uint32_t pattern) + { + DrawLine({ x1, y2 }, { x2, y2 }, p, pattern); + } + + void TransformedView::DrawLine(const olc::vf2d & pos1, const olc::vf2d & pos2, olc::Pixel p, uint32_t pattern) + { + pge->DrawLine(WorldToScreen(pos1), WorldToScreen(pos2), p, pattern); + } + + void TransformedView::DrawCircle(float x, float y, float radius, olc::Pixel p, uint8_t mask) + { + DrawCircle({ x,y }, radius, p, mask); + } + + void TransformedView::DrawCircle(const olc::vf2d & pos, float radius, olc::Pixel p, uint8_t mask) + { + pge->DrawCircle(WorldToScreen(pos), int32_t(radius * m_vWorldScale.x), p, mask); + } + + void TransformedView::FillCircle(float x, float y, float radius, olc::Pixel p) + { + FillCircle({ x,y }, radius, p); + } + + void TransformedView::FillCircle(const olc::vf2d & pos, float radius, olc::Pixel p) + { + pge->FillCircle(WorldToScreen(pos), int32_t(radius * m_vWorldScale.x), p); + } + + void TransformedView::DrawRect(float x, float y, float w, float h, olc::Pixel p) + { + DrawRect({ x, y }, { w, h }, p); + } + + void TransformedView::DrawRect(const olc::vf2d & pos, const olc::vf2d & size, olc::Pixel p) + { + pge->DrawRect(WorldToScreen(pos), ((size * m_vWorldScale) + olc::vf2d(0.5f, 0.5f)).floor(), p); + } + + void TransformedView::FillRect(float x, float y, float w, float h, olc::Pixel p) + { + FillRect({ x, y }, { w, h }, p); + } + + void TransformedView::FillRect(const olc::vf2d & pos, const olc::vf2d & size, olc::Pixel p) + { + pge->FillRect(WorldToScreen(pos), size * m_vWorldScale, p); + } + + void TransformedView::DrawTriangle(float x1, float y1, float x2, float y2, float x3, float y3, olc::Pixel p) + { + DrawTriangle({ x1, y1 }, { x2, y2 }, { x3, y3 }, p); + } + + void TransformedView::DrawTriangle(const olc::vf2d & pos1, const olc::vf2d & pos2, const olc::vf2d & pos3, olc::Pixel p) + { + pge->DrawTriangle(WorldToScreen(pos1), WorldToScreen(pos2), WorldToScreen(pos3), p); + } + + void TransformedView::FillTriangle(float x1, float y1, float x2, float y2, float x3, float y3, olc::Pixel p) + { + FillTriangle({ x1, y1 }, { x2, y2 }, { x3, y3 }, p); + } + + void TransformedView::FillTriangle(const olc::vf2d & pos1, const olc::vf2d & pos2, const olc::vf2d & pos3, olc::Pixel p) + { + pge->FillTriangle(WorldToScreen(pos1), WorldToScreen(pos2), WorldToScreen(pos3), p); + } + + void TransformedView::DrawSprite(float x, float y, olc::Sprite* sprite, float scalex, float scaley, uint8_t flip) + { + DrawSprite({ x, y }, sprite, { scalex, scaley }, flip); + } + + void TransformedView::DrawSprite(const olc::vf2d & pos, olc::Sprite * sprite, const olc::vf2d & scale, uint8_t flip) + { + olc::vf2d vSpriteSize = olc::vf2d(float(sprite->width), float(sprite->height)); + if (IsRectVisible(pos, vSpriteSize * scale)) + { + olc::vf2d vSpriteScaledSize = vSpriteSize * m_vRecipPixel * m_vWorldScale * scale; + olc::vi2d vPixel, vStart = WorldToScreen(pos), vEnd = vSpriteScaledSize + vStart; + olc::vf2d vPixelStep = 1.0f / vSpriteScaledSize; + for (vPixel.y = vStart.y; vPixel.y < vEnd.y; vPixel.y++) + { + for (vPixel.x = vStart.x; vPixel.x < vEnd.x; vPixel.x++) + { + olc::vf2d vSample = olc::vf2d(vPixel - vStart) * vPixelStep; + pge->Draw(vPixel, sprite->Sample(vSample.x, vSample.y)); + } + } + } + } + + + void TransformedView::DrawPartialSprite(float x, float y, Sprite* sprite, int32_t ox, int32_t oy, int32_t w, int32_t h, float scalex, float scaley, uint8_t flip) + { + DrawPartialSprite({ x,y }, sprite, { ox,oy }, { w, h }, { scalex, scaley }, flip); + } + + void TransformedView::DrawPartialSprite(const olc::vf2d& pos, Sprite* sprite, const olc::vi2d& sourcepos, const olc::vi2d& size, const olc::vf2d& scale, uint8_t flip) + { + olc::vf2d vSpriteSize = size; + if (IsRectVisible(pos, size * scale)) + { + olc::vf2d vSpriteScaledSize = olc::vf2d(size) * m_vRecipPixel * m_vWorldScale * scale; + olc::vf2d vSpritePixelStep = 1.0f / olc::vf2d(float(sprite->width), float(sprite->height)); + olc::vi2d vPixel, vStart = WorldToScreen(pos), vEnd = vSpriteScaledSize + vStart; + olc::vf2d vScreenPixelStep = 1.0f / vSpriteScaledSize; + + for (vPixel.y = vStart.y; vPixel.y < vEnd.y; vPixel.y++) + { + for (vPixel.x = vStart.x; vPixel.x < vEnd.x; vPixel.x++) + { + olc::vf2d vSample = ((olc::vf2d(vPixel - vStart) * vScreenPixelStep) * size * vSpritePixelStep) + olc::vf2d(sourcepos) * vSpritePixelStep; + pge->Draw(vPixel, sprite->Sample(vSample.x, vSample.y)); + } + } + } + } + + void TransformedView::DrawString(float x, float y, const std::string& sText, Pixel col, const olc::vf2d& scale) + { + DrawString({ x, y }, sText, col, scale); + } + + void TransformedView::DrawString(const olc::vf2d& pos, const std::string& sText, const Pixel col, const olc::vf2d& scale) + { + olc::vf2d vOffset = { 0.0f, 0.0f }; + Pixel::Mode m = pge->GetPixelMode(); + + auto StringPlot = [&col](const int x, const int y, const olc::Pixel& pSource, const olc::Pixel& pDest) + { + return pSource.r > 1 ? col : pDest; + }; + + pge->SetPixelMode(StringPlot); + + for (auto c : sText) + { + if (c == '\n') + { + vOffset.x = 0.0f; vOffset.y += 8.0f * m_vRecipPixel.y * scale.y; + } + else + { + int32_t ox = ((c - 32) % 16) * 8; + int32_t oy = ((c - 32) / 16) * 8; + DrawPartialSprite(pos + vOffset, pge->GetFontSprite(), { ox, oy }, { 8, 8 }, scale); + vOffset.x += 8.0f * m_vRecipPixel.x * scale.x; + } + } + pge->SetPixelMode(m); + } + + + void TransformedView::DrawDecal(const olc::vf2d & pos, olc::Decal * decal, const olc::vf2d & scale, const olc::Pixel & tint) + { + pge->DrawDecal(WorldToScreen(pos), decal, scale * m_vWorldScale * m_vRecipPixel, tint); + } + + void TransformedView::DrawPartialDecal(const olc::vf2d & pos, olc::Decal * decal, const olc::vf2d & source_pos, const olc::vf2d & source_size, const olc::vf2d & scale, const olc::Pixel & tint) + { + pge->DrawPartialDecal(WorldToScreen(pos), decal, source_pos, source_size, scale * m_vWorldScale * m_vRecipPixel, tint); + } + + void TransformedView::DrawPartialDecal(const olc::vf2d & pos, const olc::vf2d & size, olc::Decal * decal, const olc::vf2d & source_pos, const olc::vf2d & source_size, const olc::Pixel & tint) + { + pge->DrawPartialDecal(WorldToScreen(pos), size * m_vWorldScale * m_vRecipPixel, decal, source_pos, source_size, tint); + } + + void TransformedView::DrawExplicitDecal(olc::Decal* decal, const olc::vf2d* pos, const olc::vf2d* uv, const olc::Pixel* col, uint32_t elements) + { + std::vector vTransformed(elements); + for (uint32_t n = 0; n < elements; n++) + vTransformed[n] = WorldToScreen(pos[n]); + pge->DrawExplicitDecal(decal, vTransformed.data(), uv, col, elements); + } + + void TransformedView::DrawWarpedDecal(olc::Decal* decal, const olc::vf2d* pos, const olc::Pixel& tint) + { + std::array vTransformed = + { { + WorldToScreen(pos[0]), WorldToScreen(pos[1]), + WorldToScreen(pos[2]), WorldToScreen(pos[3]), + } }; + + pge->DrawWarpedDecal(decal, vTransformed, tint); + } + + void TransformedView::DrawWarpedDecal(olc::Decal* decal, const olc::vf2d(&pos)[4], const olc::Pixel& tint) + { + DrawWarpedDecal(decal, &pos[0], tint); + } + + void TransformedView::DrawWarpedDecal(olc::Decal* decal, const std::array& pos, const olc::Pixel& tint) + { + DrawWarpedDecal(decal, pos.data(), tint); + } + + void TransformedView::DrawPartialWarpedDecal(olc::Decal* decal, const olc::vf2d(&pos)[4], const olc::vf2d& source_pos, const olc::vf2d& source_size, const olc::Pixel& tint) + { + DrawPartialWarpedDecal(decal, &pos[0], source_pos, source_size, tint); + } + + void TransformedView::DrawPartialWarpedDecal(olc::Decal* decal, const olc::vf2d* pos, const olc::vf2d& source_pos, const olc::vf2d& source_size, const olc::Pixel& tint) + { + std::array vTransformed = + { { + WorldToScreen(pos[0]), WorldToScreen(pos[1]), + WorldToScreen(pos[2]), WorldToScreen(pos[3]), + } }; + + pge->DrawPartialWarpedDecal(decal, vTransformed, source_pos, source_size, tint); + } + + void TransformedView::DrawPartialWarpedDecal(olc::Decal* decal, const std::array& pos, const olc::vf2d& source_pos, const olc::vf2d& source_size, const olc::Pixel& tint) + { + DrawPartialWarpedDecal(decal, pos.data(), source_pos, source_size, tint); + } + + void TransformedView::DrawRotatedDecal(const olc::vf2d & pos, olc::Decal * decal, const float fAngle, const olc::vf2d & center, const olc::vf2d & scale, const olc::Pixel & tint) + { + pge->DrawRotatedDecal(WorldToScreen(pos), decal, fAngle, center, scale * m_vWorldScale * m_vRecipPixel, tint); + } + + void TransformedView::DrawPartialRotatedDecal(const olc::vf2d & pos, olc::Decal * decal, const float fAngle, const olc::vf2d & center, const olc::vf2d & source_pos, const olc::vf2d & source_size, const olc::vf2d & scale, const olc::Pixel & tint) + { + pge->DrawPartialRotatedDecal(WorldToScreen(pos), decal, fAngle, center, source_pos, source_size, scale * m_vWorldScale * m_vRecipPixel, tint); + } + + void TransformedView::DrawStringDecal(const olc::vf2d & pos, const std::string & sText, const olc::Pixel col, const olc::vf2d & scale) + { + pge->DrawStringDecal(WorldToScreen(pos), sText, col, scale * m_vWorldScale * m_vRecipPixel); + } + + void TransformedView::DrawStringPropDecal(const olc::vf2d & pos, const std::string & sText, const olc::Pixel col, const olc::vf2d & scale ) + { + pge->DrawStringPropDecal(WorldToScreen(pos), sText, col, scale * m_vWorldScale * m_vRecipPixel); + } + + void TransformedView::FillRectDecal(const olc::vf2d & pos, const olc::vf2d & size, const olc::Pixel col) + { + pge->FillRectDecal(WorldToScreen(pos), (size * m_vWorldScale).ceil(), col); + } + + void TransformedView::GradientFillRectDecal(const olc::vf2d & pos, const olc::vf2d & size, const olc::Pixel colTL, const olc::Pixel colBL, const olc::Pixel colBR, const olc::Pixel colTR) + { + pge->GradientFillRectDecal(WorldToScreen(pos), size * m_vWorldScale, colTL, colBL, colBR, colTR); + } + + void TransformedView::DrawPolygonDecal(olc::Decal* decal, const std::vector& pos, const std::vector& uv, const olc::Pixel tint) + { + std::vector vTransformed(pos.size()); + for (uint32_t n = 0; n < pos.size(); n++) + vTransformed[n] = WorldToScreen(pos[n]); + pge->DrawPolygonDecal(decal, vTransformed, uv, tint); + } + + ///////////////////////////////////////////////////////// + ///////////////////////////////////////////////////////// + + + TileTransformedView::TileTransformedView(const olc::vi2d& vViewArea, const olc::vi2d& vTileSize) + { + Initialise(vViewArea, vTileSize); + } + + void TileTransformedView::SetRangeX(const bool bRanged, const int32_t nMin, const int32_t nMax) + { + m_bRangedX = bRanged; + m_nMinRangeX = nMin; + m_nMaxRangeX = nMax; + } + + void TileTransformedView::SetRangeY(const bool bRanged, const int32_t nMin, const int32_t nMax) + { + m_bRangedY = bRanged; + m_nMinRangeY = nMin; + m_nMaxRangeY = nMax; + } + + olc::vi2d TileTransformedView::GetTopLeftTile() const + { + return ScreenToWorld({ 0,0 }).floor(); + } + + olc::vi2d TileTransformedView::GetBottomRightTile() const + { + return ScreenToWorld(m_vViewArea).ceil(); + } + + olc::vi2d TileTransformedView::GetVisibleTiles() const + { + return GetBottomRightTile() - GetTopLeftTile(); + } + + olc::vi2d TileTransformedView::GetTileUnderScreenPos(const olc::vi2d& vPos) const + { + return ScreenToWorld(vPos).floor(); + } + + const olc::vi2d TileTransformedView::GetTileOffset() const + { + return { int32_t((m_vWorldOffset.x - std::floor(m_vWorldOffset.x)) * m_vWorldScale.x), + int32_t((m_vWorldOffset.y - std::floor(m_vWorldOffset.y)) * m_vWorldScale.y) }; + } +} + +#endif +#endif