├── .gitattributes ├── src ├── sdk.h ├── boost_json.cpp ├── json_loader.h ├── application │ ├── application_listener.h │ ├── player.cpp │ ├── application.h │ ├── application.cpp │ └── player.h ├── cli_helper.h ├── database │ ├── postgres.h │ └── postgres.cpp ├── logger_helper.h ├── model │ ├── loot_generator.cpp │ ├── collision_detector.h │ ├── geom.h │ ├── loot_generator.h │ ├── tagged.h │ ├── collision_detector.cpp │ ├── model_properties.h │ ├── model_serialization.h │ └── model.h ├── logger_helper.cpp ├── infrastructure │ ├── serializing_listener.h │ ├── serializing_listener.cpp │ └── application_serialization.h ├── cli_helper.cpp ├── ticker.h ├── http_server │ ├── http_server.cpp │ └── http_server.h ├── request_handler │ ├── request_handler_helper.h │ ├── request_handler_helper.cpp │ ├── api_request_handler.h │ ├── request_handler.h │ ├── api_request_handler.cpp │ └── request_handler.cpp ├── main.cpp └── json_loader.cpp ├── assets ├── pug.png └── gameplay.gif ├── static ├── favicon.ico ├── assets │ └── pug.fbx ├── favicon-16x16.png ├── favicon-32x32.png ├── apple-touch-icon.png ├── android-chrome-192x192.png ├── android-chrome-512x512.png ├── site.webmanifest ├── abc..html ├── about.html ├── file with spaces.html ├── js │ ├── helper.js │ ├── js.cookie.min.js │ ├── game_map.js │ ├── loaders │ │ └── MTLLoader.js │ └── utils │ │ └── SkeletonUtils.js ├── images │ └── cube.svg ├── load_simulator.html ├── game.html ├── game_old.html ├── hall_of_fame.html └── index.html ├── README.md ├── tests ├── model-tests.cpp ├── state-serialization-tests.cpp ├── loot_generator_tests.cpp └── collision-detector-tests.cpp ├── data └── config.json └── postman └── DetectivePugs.postman_collection.json /.gitattributes: -------------------------------------------------------------------------------- 1 | static/** linguist-vendored -------------------------------------------------------------------------------- /src/sdk.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #ifdef WIN32 3 | #include 4 | #endif 5 | -------------------------------------------------------------------------------- /assets/pug.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/z-beslaneev/Detective-Pugs/HEAD/assets/pug.png -------------------------------------------------------------------------------- /static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/z-beslaneev/Detective-Pugs/HEAD/static/favicon.ico -------------------------------------------------------------------------------- /assets/gameplay.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/z-beslaneev/Detective-Pugs/HEAD/assets/gameplay.gif -------------------------------------------------------------------------------- /static/assets/pug.fbx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/z-beslaneev/Detective-Pugs/HEAD/static/assets/pug.fbx -------------------------------------------------------------------------------- /static/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/z-beslaneev/Detective-Pugs/HEAD/static/favicon-16x16.png -------------------------------------------------------------------------------- /static/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/z-beslaneev/Detective-Pugs/HEAD/static/favicon-32x32.png -------------------------------------------------------------------------------- /static/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/z-beslaneev/Detective-Pugs/HEAD/static/apple-touch-icon.png -------------------------------------------------------------------------------- /static/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/z-beslaneev/Detective-Pugs/HEAD/static/android-chrome-192x192.png -------------------------------------------------------------------------------- /static/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/z-beslaneev/Detective-Pugs/HEAD/static/android-chrome-512x512.png -------------------------------------------------------------------------------- /src/boost_json.cpp: -------------------------------------------------------------------------------- 1 | // Этот файл служит для подключения реализации библиотеки Boost.Json 2 | #include 3 | 4 | -------------------------------------------------------------------------------- /src/json_loader.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include "model.h" 6 | 7 | namespace json_loader { 8 | 9 | model::Game LoadGame(const std::filesystem::path& json_path); 10 | 11 | } // namespace json_loader 12 | -------------------------------------------------------------------------------- /src/application/application_listener.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | class IApplicationlListener { 6 | public: 7 | virtual void OnUpdate(const std::chrono::milliseconds& delta) = 0; 8 | virtual ~IApplicationlListener() = default; 9 | }; -------------------------------------------------------------------------------- /static/site.webmanifest: -------------------------------------------------------------------------------- 1 | {"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"} -------------------------------------------------------------------------------- /static/abc..html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | abc..html 6 | 7 | 8 |

This is a file with two dots in its name.

9 |

Go to Main page

10 | 11 | 12 | -------------------------------------------------------------------------------- /static/about.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | About 6 | 7 | 8 | 9 |

This is About page.

10 |

Go to Main page

11 | 12 | 13 | -------------------------------------------------------------------------------- /static/file with spaces.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Page with spaces in file name 6 | 7 | 8 |

This is file with spaces

9 |

Go to Main page

10 | 11 | 12 | -------------------------------------------------------------------------------- /static/js/helper.js: -------------------------------------------------------------------------------- 1 | function dumpObject(obj, lines = [], isLast = true, prefix = '') { 2 | const localPrefix = isLast ? '└─' : '├─'; 3 | lines.push(`${prefix}${prefix ? localPrefix : ''}${obj.name || '*no-name*'} [${obj.type}]`); 4 | const newPrefix = prefix + (isLast ? ' ' : '│ '); 5 | const lastNdx = obj.children.length - 1; 6 | obj.children.forEach((child, ndx) => { 7 | const isLast = ndx === lastNdx; 8 | dumpObject(child, lines, isLast, newPrefix); 9 | }); 10 | return lines; 11 | } 12 | -------------------------------------------------------------------------------- /src/cli_helper.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include 7 | 8 | namespace cli_helpers { 9 | 10 | struct Arguments { 11 | std::uint64_t tick_period {0}; 12 | std::string config_file_path; 13 | std::string www_root; 14 | bool randomize_spawn_dog { false }; 15 | std::string state_file_path; 16 | std::uint64_t save_state_period {0}; 17 | }; 18 | 19 | std::optional ParseCommandLine(int argc, const char* const argv[]); 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/application/player.cpp: -------------------------------------------------------------------------------- 1 | #include "player.h" 2 | 3 | namespace application { 4 | 5 | Player::Player(model::GameSession* session, std::shared_ptr dog) 6 | : session_{session} 7 | , dog_{dog} {} 8 | 9 | model::GameSession* Player::GetSession() { 10 | return session_; 11 | } 12 | 13 | std::shared_ptr Player::GetDog() { 14 | return dog_; 15 | } 16 | 17 | model::GameSession Player::GetSession() const { 18 | return *session_; 19 | } 20 | std::shared_ptr Player::GetDog() const { 21 | return dog_; 22 | } 23 | 24 | } -------------------------------------------------------------------------------- /src/database/postgres.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | 7 | namespace postgres { 8 | 9 | struct PlayerInfo { 10 | std::string name; 11 | int score; 12 | double play_time; 13 | }; 14 | 15 | class Database { 16 | public: 17 | explicit Database(const std::string& conn); 18 | 19 | void AddRecord(const std::string& name, int score, double play_time); 20 | 21 | std::vector GetRecords(std::optional start, std::optional maxItems); 22 | 23 | void AddRecords(const std::vector& infos); 24 | 25 | private: 26 | pqxx::connection conn_; 27 | }; 28 | 29 | } -------------------------------------------------------------------------------- /static/images/cube.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/logger_helper.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #define _WIN32_WINNT 0x0601 //TODO delete 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | namespace logging = boost::log; 13 | namespace keywords = boost::log::keywords; 14 | namespace json = boost::json; 15 | 16 | BOOST_LOG_ATTRIBUTE_KEYWORD(timestamp, "TimeStamp", boost::posix_time::ptime) 17 | BOOST_LOG_ATTRIBUTE_KEYWORD(additional_value, "AdditionalValue", json::value) 18 | BOOST_LOG_ATTRIBUTE_KEYWORD(additional_data, "AdditionalData", json::object) 19 | 20 | namespace logger_helper 21 | { 22 | void InitLogger(); 23 | } -------------------------------------------------------------------------------- /src/model/loot_generator.cpp: -------------------------------------------------------------------------------- 1 | #include "loot_generator.h" 2 | 3 | #include 4 | #include 5 | 6 | namespace loot_gen { 7 | 8 | unsigned LootGenerator::Generate(TimeInterval time_delta, unsigned loot_count, 9 | unsigned looter_count) { 10 | time_without_loot_ += time_delta; 11 | const unsigned loot_shortage = loot_count > looter_count ? 0u : looter_count - loot_count; 12 | const double ratio = std::chrono::duration{time_without_loot_} / base_interval_; 13 | const double probability 14 | = std::clamp((1.0 - std::pow(1.0 - probability_, ratio)) * random_generator_(), 0.0, 1.0); 15 | const unsigned generated_loot = static_cast(std::round(loot_shortage * probability)); 16 | if (generated_loot > 0) { 17 | time_without_loot_ = {}; 18 | } 19 | return generated_loot; 20 | } 21 | 22 | } // namespace loot_gen 23 | -------------------------------------------------------------------------------- /src/logger_helper.cpp: -------------------------------------------------------------------------------- 1 | #include "logger_helper.h" 2 | 3 | namespace { 4 | 5 | void MyFormatter(logging::record_view const& rec, logging::formatting_ostream& strm) { 6 | 7 | auto ts = rec[timestamp]; 8 | //strm << to_iso_extended_string(*ts) << ": "; 9 | json::object message; 10 | message["timestmap"] = to_iso_extended_string(*ts); 11 | 12 | auto value = rec[additional_value]; 13 | if (value) 14 | message["data"] = *value; 15 | 16 | auto data = rec[additional_data]; 17 | if (data) 18 | message["data"] = *data; 19 | 20 | message["message"] = *rec[logging::expressions::smessage]; 21 | 22 | strm << json::serialize(message); 23 | } 24 | 25 | } 26 | 27 | void logger_helper::InitLogger() { 28 | 29 | logging::add_common_attributes(); 30 | 31 | logging::add_console_log( 32 | std::cout, 33 | keywords::format = &MyFormatter, 34 | keywords::auto_flush = true 35 | ); 36 | } -------------------------------------------------------------------------------- /src/infrastructure/serializing_listener.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include "infrastructure/application_serialization.h" 6 | #include "application/application_listener.h" 7 | #include "application/application.h" 8 | 9 | #include 10 | #include 11 | 12 | namespace infrastructure { 13 | 14 | using InputArchive = boost::archive::text_iarchive; 15 | using OutputArchive = boost::archive::text_oarchive; 16 | 17 | using namespace std::literals; 18 | namespace fs = std::filesystem; 19 | 20 | class SerializingListener : public IApplicationlListener { 21 | 22 | public: 23 | SerializingListener(application::Application& app, const fs::path& save_file_path); 24 | void SetSavePeriod(const std::chrono::milliseconds& period); 25 | void OnUpdate(const std::chrono::milliseconds& delta) override; 26 | void Save(); 27 | private: 28 | void Restore(); 29 | 30 | private: 31 | std::chrono::milliseconds total_ = 0ms; 32 | application::Application& app_; 33 | fs::path save_file_path_; 34 | std::optional save_period_; 35 | }; 36 | 37 | } // infrastructure -------------------------------------------------------------------------------- /src/infrastructure/serializing_listener.cpp: -------------------------------------------------------------------------------- 1 | #include "serializing_listener.h" 2 | 3 | #include 4 | #include 5 | 6 | #include 7 | #include 8 | 9 | 10 | infrastructure::SerializingListener::SerializingListener(application::Application& app, const fs::path& save_file_path) 11 | : app_(app) 12 | , save_file_path_(save_file_path) { 13 | 14 | if (fs::exists(save_file_path)) { 15 | std::ifstream archive{save_file_path_}; 16 | InputArchive input_archive{ archive }; 17 | serialization::ApplicationRepr repr; 18 | input_archive >> repr; 19 | repr.Restore(app_); 20 | } 21 | } 22 | 23 | void infrastructure::SerializingListener::SetSavePeriod(const std::chrono::milliseconds &period) { 24 | save_period_ = period; 25 | } 26 | 27 | void infrastructure::SerializingListener::OnUpdate(const std::chrono::milliseconds &delta) { 28 | 29 | if (!save_period_) 30 | return; 31 | 32 | total_ += delta; 33 | 34 | if (total_ >= save_period_) { 35 | Save(); 36 | total_ = 0ms; 37 | } 38 | } 39 | 40 | void infrastructure::SerializingListener::Save() { 41 | 42 | std::ofstream archive{ save_file_path_ }; 43 | OutputArchive output_archive{ archive }; 44 | serialization::ApplicationRepr repr{ app_ }; 45 | output_archive << repr; 46 | } 47 | -------------------------------------------------------------------------------- /static/load_simulator.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |

Эта страница создаёт нагрузку на сервер...

9 |

Частота запросов в секунду (от 1 до 100):

10 | 11 | 12 | 45 | 46 | -------------------------------------------------------------------------------- /src/model/collision_detector.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "geom.h" 4 | 5 | #include 6 | #include 7 | 8 | namespace collision_detector { 9 | 10 | struct CollectionResult { 11 | bool IsCollected(double collect_radius) const { 12 | return proj_ratio >= 0 && proj_ratio <= 1 && sq_distance <= collect_radius * collect_radius; 13 | } 14 | 15 | // Квадрат расстояния до точки 16 | double sq_distance; 17 | // Доля пройденного отрезка 18 | double proj_ratio; 19 | }; 20 | 21 | // Движемся из точки a в точку b и пытаемся подобрать точку c 22 | CollectionResult TryCollectPoint(geom::Point2D a, geom::Point2D b, geom::Point2D c); 23 | 24 | struct Item { 25 | geom::Point2D position; 26 | double width; 27 | }; 28 | 29 | struct Gatherer { 30 | geom::Point2D start_pos; 31 | geom::Point2D end_pos; 32 | double width; 33 | }; 34 | 35 | class ItemGathererProvider { 36 | protected: 37 | ~ItemGathererProvider() = default; 38 | 39 | public: 40 | virtual size_t ItemsCount() const = 0; 41 | virtual Item GetItem(size_t idx) const = 0; 42 | virtual size_t GatherersCount() const = 0; 43 | virtual Gatherer GetGatherer(size_t idx) const = 0; 44 | }; 45 | 46 | struct GatheringEvent { 47 | size_t item_id; 48 | size_t gatherer_id; 49 | double sq_distance; 50 | double time; 51 | }; 52 | 53 | std::vector FindGatherEvents(const ItemGathererProvider& provider); 54 | 55 | } // namespace collision_detector -------------------------------------------------------------------------------- /static/js/js.cookie.min.js: -------------------------------------------------------------------------------- 1 | /*! js-cookie v3.0.1 | MIT */ 2 | !function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e=e||self,function(){var n=e.Cookies,o=e.Cookies=t();o.noConflict=function(){return e.Cookies=n,o}}())}(this,(function(){"use strict";function e(e){for(var t=1;t ParseCommandLine(int argc, const char* const argv[]) { 9 | namespace po = boost::program_options; 10 | 11 | po::options_description desc{"Allowed options"}; 12 | 13 | Arguments args; 14 | 15 | desc.add_options() 16 | ("help,h", "produce help message") 17 | ("tick-period,t", po::value(&args.tick_period)->value_name("milliseconds"), "set tick period") 18 | ("config-file,c", po::value(&args.config_file_path)->value_name("file path"), "set config file path") 19 | ("www-root,w", po::value(&args.www_root)->value_name("folder path"), "set static files root") 20 | ("randomize-spawn-points", "spawn dogs at random positions") 21 | ("state-file,s", po::value(&args.state_file_path)->value_name("save file path"), "set state file path") 22 | ("save-state-period,st", po::value(&args.save_state_period)->value_name("milliseconds"), "set state save period"); 23 | 24 | po::variables_map vm; 25 | po::store(po::parse_command_line(argc, argv, desc), vm); 26 | po::notify(vm); 27 | 28 | if (vm.contains("help"s)) { 29 | std::cout << desc; 30 | return std::nullopt; 31 | } 32 | 33 | if (vm.contains("randomize-spawn-points"s)) { 34 | args.randomize_spawn_dog = true; 35 | } 36 | 37 | if (!vm.contains("config-file"s)) { 38 | throw std::runtime_error("Config file have not been specified"); 39 | } 40 | 41 | if (!vm.contains("www-root"s)) { 42 | throw std::runtime_error("www-root folder have not been specified"); 43 | } 44 | 45 | return args; 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /src/model/geom.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | namespace geom { 7 | 8 | struct Vec2D { 9 | Vec2D() = default; 10 | Vec2D(double x, double y) 11 | : x(x) 12 | , y(y) { 13 | } 14 | 15 | Vec2D& operator*=(double scale) { 16 | x *= scale; 17 | y *= scale; 18 | return *this; 19 | } 20 | 21 | auto operator<=>(const Vec2D&) const = default; 22 | 23 | double x = 0; 24 | double y = 0; 25 | }; 26 | 27 | inline Vec2D operator*(Vec2D lhs, double rhs) { 28 | return lhs *= rhs; 29 | } 30 | 31 | inline Vec2D operator*(double lhs, Vec2D rhs) { 32 | return rhs *= lhs; 33 | } 34 | 35 | struct Point2D { 36 | Point2D() = default; 37 | Point2D(double x, double y) 38 | : x(x) 39 | , y(y) { 40 | } 41 | 42 | Point2D& operator+=(const Vec2D& rhs) { 43 | x += rhs.x; 44 | y += rhs.y; 45 | return *this; 46 | } 47 | 48 | //auto operator<=>(const Point2D&) const = default; 49 | bool operator==(const Point2D& other) const { 50 | constexpr float EPSILON = 1e-5; 51 | return std::abs(x - other.x) < EPSILON && std::abs(y - other.y) < EPSILON; 52 | } 53 | 54 | bool operator!=(const Point2D& other) const { 55 | return !(*this == other); 56 | } 57 | static float Distance(const Point2D& a, const Point2D& b) { 58 | double squared_distance = pow(b.x - a.x, 2) + pow(b.y - a.y, 2); 59 | return sqrt(squared_distance); 60 | } 61 | 62 | double x = 0; 63 | double y = 0; 64 | }; 65 | 66 | inline Point2D operator+(Point2D lhs, const Vec2D& rhs) { 67 | return lhs += rhs; 68 | } 69 | 70 | inline Point2D operator+(const Vec2D& lhs, Point2D rhs) { 71 | return rhs += lhs; 72 | } 73 | 74 | } // namespace geom 75 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Detective Pugs 2 |
3 | Puggy 4 |

5 | A project with a modern approach to C++ backend 6 |

7 |
8 | 9 | ## About The Project 10 | 11 | This project is a multiplayer game written in C++ and utilizes modern technologies for backend development. 12 | 13 | The goal of the game is to collect as many things as possible and take them back to the base. 14 | 15 | ### Built With 16 | * ![](https://img.shields.io/badge/C%2B%2B20-%2320232A?style=for-the-badge&logo=cplusplus) 17 | * ![](https://img.shields.io/badge/cmake-%2320232A?style=for-the-badge&logo=cmake) 18 | * ![](https://img.shields.io/badge/conan-%2320232A?style=for-the-badge&logo=conan) 19 | * ![](https://img.shields.io/badge/docker-%2320232A?style=for-the-badge&logo=docker) 20 | * ![](https://img.shields.io/badge/postman-%2320232A?style=for-the-badge&logo=postman) 21 | * ![](https://img.shields.io/badge/postgresql-%2320232A?style=for-the-badge&logo=postgresql) 22 | 23 | ## Getting Started 24 | 25 | ### Prerequisites 26 | 27 | 1. To build and run the server, you just need to install [Docker](https://docs.docker.com/engine/install/ubuntu). 28 | 29 | 30 | ### Build&Run 31 | 32 | 1. Go to the `build` folder and execute: 33 | ```sh 34 | docker compose up -d 35 | ``` 36 | 2. Open local address in browser: 37 | ``` 38 | 127.0.0.1 39 | ``` 40 |

41 | 42 |

43 | 44 | ### API Documentation 45 | 46 | You can read the API documentation at the [link](https://documenter.getpostman.com/view/2539805/2sA3XTgMDq) or by importing the collection into postman 47 | 48 | -------------------------------------------------------------------------------- /static/js/game_map.js: -------------------------------------------------------------------------------- 1 | let map_tiles; 2 | let xmin, xmax, ymin, ymax; 3 | let map; 4 | 5 | function gameLoadMap(map_local) { 6 | map = map_local; 7 | const road_xmin = map['roads'].map(r=>'x1' in r ? Math.min(r['x0'], r['x1']) : r['x0']); 8 | const road_xmax = map['roads'].map(r=>'x1' in r ? Math.max(r['x0'], r['x1']) : r['x0']); 9 | const road_ymin = map['roads'].map(r=>'y1' in r ? Math.min(r['y0'], r['y1']) : r['y0']); 10 | const road_ymax = map['roads'].map(r=>'y1' in r ? Math.max(r['y0'], r['y1']) : r['y0']); 11 | const building_xmin = map['buildings'].map(r=>r['x']); 12 | const building_xmax = map['buildings'].map(r=>r['x']+r['w']); 13 | const building_ymin = map['buildings'].map(r=>r['y']); 14 | const building_ymax = map['buildings'].map(r=>r['y']+r['h']); 15 | xmin = Math.min(...road_xmin, ...building_xmin); 16 | xmax = Math.max(...road_xmax, ...building_xmax); 17 | ymin = Math.min(...road_ymin, ...building_ymin); 18 | ymax = Math.max(...road_ymax, ...building_ymax); 19 | const edges = [xmin, xmax, ymin, ymax]; 20 | 21 | const def_tile = {'road': false, 'u': false, 'r': false, 'd': false, 'l': false}; 22 | map_tiles = Array.from({length: ymax-ymin+1}, _=> Array.from({length:xmax-xmin+1}, _=>({...def_tile}))); 23 | 24 | for(const r of map['roads']) { 25 | const rh = 'x1' in r; 26 | const ss = rh ? r['x0'] : r['y0']; 27 | const ee = rh ? r['x1'] : r['y1']; 28 | const s = Math.min(ss,ee); 29 | const e = Math.max(ss,ee); 30 | for(z=s;z<=e;++z) { 31 | const x = (rh ? z : r['x0']) - xmin; 32 | const y = (rh ? r['y0'] : z) - ymin; 33 | map_tiles[y][x]['road'] = true; 34 | if (z!=s) { 35 | map_tiles[y][x][rh ? 'l' : 'u'] = true; 36 | } 37 | if (z!=e) { 38 | map_tiles[y][x][rh ? 'r' : 'd'] = true; 39 | } 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /src/model/loot_generator.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | 5 | namespace loot_gen { 6 | 7 | /* 8 | * Генератор трофеев 9 | */ 10 | class LootGenerator { 11 | public: 12 | using RandomGenerator = std::function; 13 | using TimeInterval = std::chrono::milliseconds; 14 | 15 | /* 16 | * base_interval - базовый отрезок времени > 0 17 | * probability - вероятность появления трофея в течение базового интервала времени 18 | * random_generator - генератор псевдослучайных чисел в диапазоне от [0 до 1] 19 | */ 20 | LootGenerator(TimeInterval base_interval, double probability, 21 | RandomGenerator random_gen = DefaultGenerator) 22 | : base_interval_{base_interval} 23 | , probability_{probability} 24 | , random_generator_{std::move(random_gen)} { 25 | } 26 | 27 | /* 28 | * Возвращает количество трофеев, которые должны появиться на карте спустя 29 | * заданный промежуток времени. 30 | * Количество трофеев, появляющихся на карте не превышает количество мародёров. 31 | * 32 | * time_delta - отрезок времени, прошедший с момента предыдущего вызова Generate 33 | * loot_count - количество трофеев на карте до вызова Generate 34 | * looter_count - количество мародёров на карте 35 | */ 36 | unsigned Generate(TimeInterval time_delta, unsigned loot_count, unsigned looter_count); 37 | std::pair GetConfig() const { 38 | return {base_interval_.count(), probability_};} 39 | 40 | private: 41 | static double DefaultGenerator() noexcept { 42 | return 1.0; 43 | }; 44 | TimeInterval base_interval_; 45 | double probability_; 46 | TimeInterval time_without_loot_{}; 47 | RandomGenerator random_generator_; 48 | }; 49 | 50 | } // namespace loot_gen 51 | -------------------------------------------------------------------------------- /src/ticker.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | namespace net = boost::asio; 6 | namespace sys = boost::system; 7 | 8 | class Ticker : public std::enable_shared_from_this { 9 | public: 10 | using Strand = net::strand; 11 | using Handler = std::function; 12 | 13 | // Функция handler будет вызываться внутри strand с интервалом period 14 | Ticker(Strand strand, std::chrono::milliseconds period, Handler handler) 15 | : strand_{strand} 16 | , period_{period} 17 | , handler_{std::move(handler)} { 18 | } 19 | 20 | void Start() { 21 | net::dispatch(strand_, [self = shared_from_this()] { 22 | self->last_tick_ = Clock::now(); 23 | self->ScheduleTick(); 24 | }); 25 | } 26 | 27 | private: 28 | void ScheduleTick() { 29 | assert(strand_.running_in_this_thread()); 30 | timer_.expires_after(period_); 31 | timer_.async_wait([self = shared_from_this()](sys::error_code ec) { 32 | self->OnTick(ec); 33 | }); 34 | } 35 | 36 | void OnTick(sys::error_code ec) { 37 | using namespace std::chrono; 38 | assert(strand_.running_in_this_thread()); 39 | 40 | if (!ec) { 41 | auto this_tick = Clock::now(); 42 | auto delta = duration_cast(this_tick - last_tick_); 43 | last_tick_ = this_tick; 44 | try { 45 | handler_(delta); 46 | } catch (...) { 47 | } 48 | ScheduleTick(); 49 | } 50 | } 51 | 52 | using Clock = std::chrono::steady_clock; 53 | 54 | Strand strand_; 55 | std::chrono::milliseconds period_; 56 | net::steady_timer timer_{strand_}; 57 | Handler handler_; 58 | std::chrono::steady_clock::time_point last_tick_; 59 | }; -------------------------------------------------------------------------------- /static/game.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Dog Story 12 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 |
40 |
41 |
42 |
43 | 44 | 61 | -------------------------------------------------------------------------------- /src/database/postgres.cpp: -------------------------------------------------------------------------------- 1 | #include "postgres.h" 2 | 3 | 4 | namespace postgres { 5 | 6 | using pqxx::operator"" _zv; 7 | 8 | Database::Database(const std::string& conn) 9 | : conn_{ conn } { 10 | pqxx::work w(conn_); 11 | w.exec( 12 | "CREATE TABLE IF NOT EXISTS retired_players (id SERIAL PRIMARY KEY, name varchar(100), score integer, time real);"_zv); 13 | 14 | w.commit(); 15 | } 16 | 17 | void Database::AddRecord(const std::string& name, int score, double play_time) { 18 | pqxx::work w(conn_); 19 | w.exec("INSERT INTO retired_players (name, score, time) VALUES (" + 20 | w.quote(static_cast(name)) + ", " + std::to_string(score) + ", " + std::to_string(play_time) + ")"); 21 | w.commit(); 22 | } 23 | 24 | std::vector Database::GetRecords(std::optional start, std::optional maxItems) { 25 | pqxx::read_transaction r(conn_); 26 | 27 | constexpr size_t default_max = 100; 28 | std::size_t max = maxItems ? *maxItems : default_max; 29 | 30 | std::string query_text; 31 | if (start) { 32 | query_text = "SELECT name, score, time FROM retired_players ORDER BY score DESC, time ASC, name ASC OFFSET " + std::to_string(*start) + " LIMIT " + std::to_string(max) + ";"; 33 | } else { 34 | query_text = "SELECT name, score, time FROM retired_players ORDER BY score DESC, time ASC, name ASC LIMIT " + std::to_string(max) + ";"; 35 | } 36 | 37 | std::vector result; 38 | for (auto [name, score, time] : r.query(query_text)) { 39 | result.push_back({ name, score, time }); 40 | } 41 | 42 | return result; 43 | } 44 | 45 | void Database::AddRecords(const std::vector& infos) { 46 | pqxx::work w(conn_); 47 | 48 | for (auto& info : infos) { 49 | w.exec("INSERT INTO retired_players (name, score, time) VALUES (" + 50 | w.quote(static_cast(info.name)) + ", " + std::to_string(info.score) + ", " + std::to_string(info.play_time) + ")"); 51 | } 52 | 53 | w.commit(); 54 | } 55 | 56 | } -------------------------------------------------------------------------------- /src/model/tagged.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | 4 | namespace util { 5 | 6 | /** 7 | * Вспомогательный шаблонный класс "Маркированный тип". 8 | * С его помощью можно описать строгий тип на основе другого типа. 9 | * Пример: 10 | * 11 | * struct AddressTag{}; // метка типа для строки, хранящей адрес 12 | * using Address = util::Tagged; 13 | * 14 | * struct NameTag{}; // метка типа для строки, хранящей имя 15 | * using Name = util::Tagged; 16 | * 17 | * struct Person { 18 | * Name name; 19 | * Address address; 20 | * }; 21 | * 22 | * Name name{"Harry Potter"s}; 23 | * Address address{"4 Privet Drive, Little Whinging, Surrey, England"s}; 24 | * 25 | * Person p1{name, address}; // OK 26 | * Person p2{address, name}; // Ошибка, Address и Name - разные типы 27 | */ 28 | template 29 | class Tagged { 30 | public: 31 | using ValueType = Value; 32 | using TagType = Tag; 33 | 34 | explicit Tagged(Value&& v) 35 | : value_(std::move(v)) { 36 | } 37 | explicit Tagged(const Value& v) 38 | : value_(v) { 39 | } 40 | 41 | const Value& operator*() const { 42 | return value_; 43 | } 44 | 45 | Value& operator*() { 46 | return value_; 47 | } 48 | 49 | // Так в C++20 можно объявить оператор сравнения Tagged-типов 50 | // Будет просто вызван соответствующий оператор для поля value_ 51 | auto operator<=>(const Tagged&) const = default; 52 | 53 | private: 54 | Value value_; 55 | }; 56 | 57 | // Хешер для Tagged-типа, чтобы Tagged-объекты можно было хранить в unordered-контейнерах 58 | template 59 | struct TaggedHasher { 60 | size_t operator()(const TaggedValue& value) const { 61 | // Возвращает хеш значения, хранящегося внутри value 62 | return std::hash{}(*value); 63 | } 64 | }; 65 | 66 | } // namespace util 67 | -------------------------------------------------------------------------------- /tests/model-tests.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "../src/model/model.h" 5 | 6 | using namespace std::literals; 7 | 8 | SCENARIO("Model tests") { 9 | 10 | GIVEN("GameSession with simple map") { 11 | 12 | model::Map simple_map(model::Map::Id("test_map"), "TestMap"); 13 | simple_map.AddRoad(model::Road{model::Road::HORIZONTAL, {0, 0}, 10}); 14 | 15 | 16 | simple_map.AddLootType(model::LootType{"key", "assets/key.obj", "obj", 90, "#338844", 0.03}); 17 | simple_map.AddLootType(model::LootType{"wallet", "assets/wallet.obj", "obj", 90, "#338899", 0.01}); 18 | 19 | constexpr std::chrono::milliseconds TIME_INTERVAL = 50ms; 20 | model::GameSession session {simple_map , model::LootGeneratorConfig{5, 1}, 15.0}; 21 | 22 | WHEN("dont try to generate loot without looters") { 23 | THEN("no loot is generated") { 24 | session.UpdateGameState(TIME_INTERVAL.count()); 25 | REQUIRE(session.GetLootStates().size() == 0); 26 | } 27 | } 28 | 29 | WHEN("add looter") { 30 | session.AddDog(std::make_shared("test_dog"), false); 31 | THEN("loot is generated") { 32 | session.UpdateGameState(TIME_INTERVAL.count()); 33 | REQUIRE(session.GetLootStates().size() == 1); 34 | } 35 | } 36 | 37 | WHEN("add few looters") { 38 | THEN("loot type is included in the type range") { 39 | 40 | auto types_max_index = simple_map.GetLootTypes().size() - 1; 41 | 42 | for (int i = 0; i < 10; ++i) { 43 | session.AddDog(std::make_shared(std::string("test_dog") + std::to_string(i)), false); 44 | session.UpdateGameState(TIME_INTERVAL.count()); 45 | 46 | for (auto& loot : session.GetLootStates()) { 47 | REQUIRE(loot.type <= types_max_index); 48 | } 49 | } 50 | } 51 | } 52 | } 53 | } -------------------------------------------------------------------------------- /src/application/application.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "model.h" 4 | #include "player.h" 5 | #include "application_listener.h" 6 | #include "postgres.h" 7 | 8 | #include 9 | 10 | namespace application { 11 | 12 | using milliseconds = std::chrono::milliseconds; 13 | 14 | // auth_token, player_id 15 | using AuthResponse = std::pair; 16 | using UpdateListener = std::shared_ptr; 17 | 18 | struct PlayerState { 19 | std::string dog_id_; 20 | std::string dog_direction_; 21 | float horizontal_speed_; 22 | float vertical_speed_; 23 | float position_x_; 24 | float position_y_; 25 | uint64_t score_; 26 | model::LootStates bag_; 27 | }; 28 | 29 | using PlayersState = std::vector; 30 | 31 | struct GameState { 32 | PlayersState players_state_; 33 | model::LootStates loots_state_; 34 | }; 35 | 36 | using RecordsInfo = std::vector>; 37 | 38 | 39 | class Application { 40 | 41 | public: 42 | 43 | Application(model::Game& game, postgres::Database& db, bool random_spawn); 44 | 45 | const model::Game::Maps& GetMaps(); 46 | const model::Map* FindMap(std::string_view id); 47 | AuthResponse JoinToGame(std::string_view user_name, std::string_view map_id); 48 | std::vector& GetAllPlayers(); 49 | GameState GetState(std::string_view token); 50 | RecordsInfo GetRecordsInfo(std::optional start, std::optional maxItems); 51 | void Move(const std::string_view token, char direction); 52 | void UpdateGameState(const std::chrono::milliseconds time_delta); 53 | bool IsAuthorized(std::string_view token); 54 | 55 | void SetUpdateListener(UpdateListener listener) { update_listener_ = listener; } 56 | 57 | model::Game& GetGame() { return game_; } 58 | Players& GetPlayers() { return players_; } 59 | void SetPlayers(Players players) { players_ = players; } 60 | 61 | void SetRandomSpawn(bool random_spawn) { random_spawn_ = random_spawn; } 62 | bool GetRandomSpawn() { return random_spawn_; } 63 | 64 | private: 65 | 66 | void ProcessRetirementPlayers(const std::vector& ids_to_remove); 67 | 68 | private: 69 | model::Game& game_; 70 | Players players_; 71 | bool random_spawn_ = false; 72 | UpdateListener update_listener_; 73 | postgres::Database& db_; 74 | }; 75 | 76 | } -------------------------------------------------------------------------------- /src/model/collision_detector.cpp: -------------------------------------------------------------------------------- 1 | #include "collision_detector.h" 2 | #include 3 | 4 | namespace collision_detector { 5 | 6 | CollectionResult TryCollectPoint(geom::Point2D a, geom::Point2D b, geom::Point2D c) { 7 | // Проверим, что перемещение ненулевое. 8 | // Тут приходится использовать строгое равенство, а не приближённое, 9 | // пскольку при сборе заказов придётся учитывать перемещение даже на небольшое 10 | // расстояние. 11 | assert(b.x != a.x || b.y != a.y); 12 | const double u_x = c.x - a.x; 13 | const double u_y = c.y - a.y; 14 | const double v_x = b.x - a.x; 15 | const double v_y = b.y - a.y; 16 | const double u_dot_v = u_x * v_x + u_y * v_y; 17 | const double u_len2 = u_x * u_x + u_y * u_y; 18 | const double v_len2 = v_x * v_x + v_y * v_y; 19 | const double proj_ratio = u_dot_v / v_len2; 20 | const double sq_distance = u_len2 - (u_dot_v * u_dot_v) / v_len2; 21 | 22 | return CollectionResult(sq_distance, proj_ratio); 23 | } 24 | 25 | 26 | std::vector FindGatherEvents( 27 | const ItemGathererProvider& provider) { 28 | std::vector detected_events; 29 | 30 | static auto eq_pt = [](geom::Point2D p1, geom::Point2D p2) { 31 | return p1.x == p2.x && p1.y == p2.y; 32 | }; 33 | 34 | for (size_t g = 0; g < provider.GatherersCount(); ++g) { 35 | Gatherer gatherer = provider.GetGatherer(g); 36 | if (eq_pt(gatherer.start_pos, gatherer.end_pos)) { 37 | continue; 38 | } 39 | for (size_t i = 0; i < provider.ItemsCount(); ++i) { 40 | Item item = provider.GetItem(i); 41 | auto collect_result 42 | = TryCollectPoint(gatherer.start_pos, gatherer.end_pos, item.position); 43 | 44 | if (collect_result.IsCollected(gatherer.width + item.width)) { 45 | GatheringEvent evt{.item_id = i, 46 | .gatherer_id = g, 47 | .sq_distance = collect_result.sq_distance, 48 | .time = collect_result.proj_ratio}; 49 | detected_events.push_back(evt); 50 | } 51 | } 52 | } 53 | 54 | std::sort(detected_events.begin(), detected_events.end(), 55 | [](const GatheringEvent& e_l, const GatheringEvent& e_r) { 56 | return e_l.time < e_r.time; 57 | }); 58 | 59 | return detected_events; 60 | } 61 | 62 | } // namespace collision_detector -------------------------------------------------------------------------------- /tests/state-serialization-tests.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | #include "../src/model/model.h" 7 | #include "../src/model/model_serialization.h" 8 | 9 | using namespace model; 10 | using namespace std::literals; 11 | namespace { 12 | 13 | using InputArchive = boost::archive::text_iarchive; 14 | using OutputArchive = boost::archive::text_oarchive; 15 | 16 | struct Fixture { 17 | std::stringstream strm; 18 | OutputArchive output_archive{strm}; 19 | }; 20 | 21 | } // namespace 22 | 23 | SCENARIO_METHOD(Fixture, "Point serialization") { 24 | GIVEN("A point") { 25 | const geom::Point2D p{10, 20}; 26 | WHEN("point is serialized") { 27 | output_archive << p; 28 | 29 | THEN("it is equal to point after serialization") { 30 | InputArchive input_archive{strm}; 31 | geom::Point2D restored_point; 32 | input_archive >> restored_point; 33 | CHECK(p == restored_point); 34 | } 35 | } 36 | } 37 | } 38 | 39 | SCENARIO_METHOD(Fixture, "Dog Serialization") { 40 | GIVEN("a dog") { 41 | const auto dog = [] { 42 | Dog dog{"Pluto"s}; 43 | dog.SetId(42); 44 | dog.SetPosition({42.2, 12.5}); 45 | dog.AccumulateScore(42); 46 | dog.SetBagCapacity(3); 47 | CHECK(dog.PutToBag(LootState{10, 2u})); 48 | dog.SetDirection(Direction::EAST); 49 | dog.SetSpeed({2.3, -1.2}); 50 | return dog; 51 | }(); 52 | 53 | WHEN("dog is serialized") { 54 | { 55 | serialization::DogRepr repr{dog}; 56 | output_archive << repr; 57 | } 58 | 59 | THEN("it can be deserialized") { 60 | InputArchive input_archive{strm}; 61 | serialization::DogRepr repr; 62 | input_archive >> repr; 63 | const auto restored = repr.Restore(); 64 | 65 | CHECK(dog.GetDogId() == restored.GetDogId()); 66 | CHECK(dog.GetName() == restored.GetName()); 67 | CHECK(dog.GetPosition() == restored.GetPosition()); 68 | CHECK(dog.GetSpeed() == restored.GetSpeed()); 69 | CHECK(dog.GetBag().GetCapacity() == restored.GetBag().GetCapacity()); 70 | CHECK(dog.GetBag().GetObjects() == restored.GetBag().GetObjects()); 71 | } 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/http_server/http_server.cpp: -------------------------------------------------------------------------------- 1 | #include "http_server.h" 2 | 3 | #include 4 | 5 | namespace http_server { 6 | 7 | SessionBase::SessionBase(tcp::socket&& socket) 8 | : stream_(std::move(socket)) { 9 | } 10 | 11 | void SessionBase::Run() { 12 | // Вызываем метод Read, используя executor объекта stream_. 13 | // Таким образом вся работа со stream_ будет выполняться, используя его executor 14 | net::dispatch(stream_.get_executor(), 15 | beast::bind_front_handler(&SessionBase::Read, GetSharedThis())); 16 | } 17 | 18 | void SessionBase::Read() { 19 | using namespace std::literals; 20 | // Очищаем запрос от прежнего значения (метод Read может быть вызван несколько раз) 21 | request_ = {}; 22 | stream_.expires_after(30s); 23 | // Считываем request_ из stream_, используя buffer_ для хранения считанных данных 24 | http::async_read(stream_, buffer_, request_, 25 | // По окончании операции будет вызван метод OnRead 26 | beast::bind_front_handler(&SessionBase::OnRead, GetSharedThis())); 27 | } 28 | 29 | void SessionBase::OnRead(beast::error_code ec, std::size_t bytes_read) { 30 | if (ec == http::error::end_of_stream) { 31 | json::object read_error; 32 | read_error["code"s] = ec.value(); 33 | read_error["text"s] = ec.message(); 34 | read_error["where"s] = "read"; 35 | BOOST_LOG_TRIVIAL(info) << logging::add_value(additional_data, read_error) 36 | << "error"sv; 37 | return Close(); 38 | } 39 | if (ec) { 40 | json::object read_error; 41 | read_error["code"s] = ec.value(); 42 | read_error["text"s] = ec.message(); 43 | read_error["where"s] = "read"; 44 | BOOST_LOG_TRIVIAL(info) << logging::add_value(additional_data, read_error) 45 | << "error"sv; 46 | return; 47 | } 48 | HandleRequest(std::move(request_)); 49 | } 50 | 51 | void SessionBase::OnWrite(bool close, beast::error_code ec, std::size_t bytes_written) { 52 | if (ec) { 53 | json::object write_error; 54 | write_error["code"s] = ec.value(); 55 | write_error["text"s] = ec.message(); 56 | write_error["where"s] = "read"; 57 | BOOST_LOG_TRIVIAL(info) << logging::add_value(additional_data, write_error) 58 | << "error"sv; 59 | } 60 | 61 | if (close) { 62 | // Семантика ответа требует закрыть соединение 63 | return Close(); 64 | } 65 | 66 | // Считываем следующий запрос 67 | Read(); 68 | } 69 | 70 | void SessionBase::Close() { 71 | stream_.socket().shutdown(tcp::socket::shutdown_send); 72 | } 73 | 74 | 75 | } // namespace http_server 76 | -------------------------------------------------------------------------------- /tests/loot_generator_tests.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "../src/model/loot_generator.h" 5 | 6 | using namespace std::literals; 7 | 8 | SCENARIO("Loot generation") { 9 | using loot_gen::LootGenerator; 10 | using TimeInterval = LootGenerator::TimeInterval; 11 | 12 | GIVEN("a loot generator") { 13 | LootGenerator gen{1s, 1.0}; 14 | 15 | constexpr TimeInterval TIME_INTERVAL = 1s; 16 | 17 | WHEN("loot count is enough for every looter") { 18 | THEN("no loot is generated") { 19 | for (unsigned looters = 0; looters < 10; ++looters) { 20 | for (unsigned loot = looters; loot < looters + 10; ++loot) { 21 | INFO("loot count: " << loot << ", looters: " << looters); 22 | REQUIRE(gen.Generate(TIME_INTERVAL, loot, looters) == 0); 23 | } 24 | } 25 | } 26 | } 27 | 28 | WHEN("number of looters exceeds loot count") { 29 | THEN("number of loot is proportional to loot difference") { 30 | for (unsigned loot = 0; loot < 10; ++loot) { 31 | for (unsigned looters = loot; looters < loot + 10; ++looters) { 32 | INFO("loot count: " << loot << ", looters: " << looters); 33 | REQUIRE(gen.Generate(TIME_INTERVAL, loot, looters) == looters - loot); 34 | } 35 | } 36 | } 37 | } 38 | } 39 | 40 | GIVEN("a loot generator with some probability") { 41 | constexpr TimeInterval BASE_INTERVAL = 1s; 42 | LootGenerator gen{BASE_INTERVAL, 0.5}; 43 | 44 | WHEN("time is greater than base interval") { 45 | THEN("number of generated loot is increased") { 46 | CHECK(gen.Generate(BASE_INTERVAL * 2, 0, 4) == 3); 47 | } 48 | } 49 | 50 | WHEN("time is less than base interval") { 51 | THEN("number of generated loot is decreased") { 52 | const auto time_interval 53 | = std::chrono::duration_cast(std::chrono::duration{ 54 | 1.0 / (std::log(1 - 0.5) / std::log(1.0 - 0.25))}); 55 | CHECK(gen.Generate(time_interval, 0, 4) == 1); 56 | } 57 | } 58 | } 59 | 60 | GIVEN("a loot generator with custom random generator") { 61 | LootGenerator gen{1s, 0.5, [] { 62 | return 0.5; 63 | }}; 64 | WHEN("loot is generated") { 65 | THEN("number of loot is proportional to random generated values") { 66 | const auto time_interval 67 | = std::chrono::duration_cast(std::chrono::duration{ 68 | 1.0 / (std::log(1 - 0.5) / std::log(1.0 - 0.25))}); 69 | CHECK(gen.Generate(time_interval, 0, 4) == 0); 70 | CHECK(gen.Generate(time_interval, 0, 4) == 1); 71 | } 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /static/game_old.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Game 6 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 69 | 70 | 71 | 72 |
73 | Choose map: 74 |
75 | 76 | 109 | -------------------------------------------------------------------------------- /src/request_handler/request_handler_helper.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "http_server.h" 4 | 5 | namespace http_handler { 6 | 7 | using namespace std::literals; 8 | namespace beast = boost::beast; 9 | namespace http = beast::http; 10 | namespace fs = std::filesystem; 11 | namespace net = boost::asio; 12 | 13 | // Запрос, тело которого представлено в виде строки 14 | using StringRequest = http::request; 15 | // Ответ, тело которого представлено в виде строки 16 | using StringResponse = http::response; 17 | 18 | using FileResponse = http::response; 19 | 20 | StringResponse MakeStringResponse(const http::status status, const std::string_view body, unsigned int http_version, bool keep_alive); 21 | StringResponse MakeErrorResponse(const http::status status, const std::string_view code, const std::string_view message, unsigned int http_version, bool keep_alive); 22 | 23 | StringResponse MakeBadRequest(const std::string_view code, const std::string_view message, unsigned int http_version, bool keep_alive); 24 | StringResponse MakeNotAlowedResponse(const std::string_view message, const std::string_view allow, unsigned int http_version, bool keep_alive); 25 | StringResponse MakeNotFoundResponse(const std::string_view code, const std::string_view message, unsigned int http_version, bool keep_alive); 26 | StringResponse MakeUnauthorizedResponse(const std::string_view code, const std::string_view message, unsigned int http_version, bool keep_alive); 27 | 28 | void PrettyPrint( std::ostream& os, json::value const& jv, std::string* indent = nullptr ); 29 | std::string PrettySerialize(json::value const& jv); 30 | 31 | std::vector SplitUriPath(const std::string_view uri_path); 32 | 33 | struct ContentType { 34 | ContentType() = delete; 35 | 36 | constexpr static std::string_view TEXT_HTML = "text/html"sv; 37 | constexpr static std::string_view TEXT_CSS = "text/css"sv; 38 | constexpr static std::string_view TEXT_PLAIN = "text/plain"sv; 39 | constexpr static std::string_view TEXT_JAVASCRIPT = "text/javascript"sv; 40 | 41 | constexpr static std::string_view APPLICATION_JSON = "application/json"sv; 42 | constexpr static std::string_view APPLICATION_XML = "application/xml"sv; 43 | constexpr static std::string_view APPLICATION_OCTET_STREAM = "application/octet-stream"sv; 44 | 45 | constexpr static std::string_view IMAGE_PNG = "image/png"sv; 46 | constexpr static std::string_view IMAGE_JPEG = "image/jpeg"sv; 47 | constexpr static std::string_view IMAGE_GIF = "image/gif"sv; 48 | constexpr static std::string_view IMAGE_BMP = "image/bmp"sv; 49 | constexpr static std::string_view IMAGE_ICO = "image/vnd.microsoft.icon"sv; 50 | constexpr static std::string_view IMAGE_TIFF = "image/tiff"sv; 51 | constexpr static std::string_view IMAGE_SVG = "image/svg+xml"sv; 52 | 53 | constexpr static std::string_view AUDIO_MP3 = "audio/mpeg"sv; 54 | // При необходимости внутрь ContentType можно добавить и другие типы контента 55 | }; 56 | 57 | } -------------------------------------------------------------------------------- /src/model/model_properties.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | struct Properties{ 4 | Properties() = delete; 5 | 6 | constexpr static char MAPS_ARRAY[] = "maps"; 7 | 8 | constexpr static char MAP_ID[] = "id"; 9 | constexpr static char MAP_NAME[] = "name"; 10 | 11 | constexpr static char ROADS_ARRAY[] = "roads"; 12 | constexpr static char ROAD_POINT_X[] = "x0"; 13 | constexpr static char ROAD_POINT_Y[] = "y0"; 14 | constexpr static char ROAD_HORIZONTAL[] = "x1"; 15 | constexpr static char ROAD_VERTICAL[] = "y1"; 16 | 17 | constexpr static char BUILDINGS_ARRAY[] = "buildings"; 18 | constexpr static char BUILDING_POINT_X[] = "x"; 19 | constexpr static char BUILDING_POINT_Y[] = "y"; 20 | constexpr static char BUILDING_WEIGHT[] = "w"; 21 | constexpr static char BUILDING_HEIGHT[] = "h"; 22 | 23 | constexpr static char OFFICES_ARRAY[] = "offices"; 24 | constexpr static char OFFICE_ID[] = "id"; 25 | constexpr static char OFFICE_POINT_X[] = "x"; 26 | constexpr static char OFFICE_POINT_Y[] = "y"; 27 | constexpr static char OFFICE_OFFSET_X[] = "offsetX"; 28 | constexpr static char OFFICE_OFFSET_Y[] = "offsetY"; 29 | 30 | constexpr static char DEFAULT_DOG_SPEED[] = "defaultDogSpeed"; 31 | constexpr static char DOG_SPEED[] = "dogSpeed"; 32 | 33 | constexpr static char JOIN_USER_NAME[] = "userName"; 34 | constexpr static char JOIN_MAP_ID[] = "mapId"; 35 | 36 | constexpr static char AUTH_TOKEN[] = "authToken"; 37 | constexpr static char PLAYER_ID[] = "playerId"; 38 | constexpr static char USER_NAME[] = "name"; 39 | 40 | constexpr static char PLAYERS_RESPONSE[] = "players"; 41 | constexpr static char PLAYER_POSITION[] = "pos"; 42 | constexpr static char PLAYER_SPEED[] = "speed"; 43 | constexpr static char PLAYER_DIRECTION[] = "dir"; 44 | constexpr static char PLAYER_BAG[] = "bag"; 45 | constexpr static char PLAYER_BAG_OBJ_ID[] = "id"; 46 | constexpr static char PLAYER_BAG_OBJ_TYPE[] = "type"; 47 | constexpr static char PLAYER_SCORE[] = "score"; 48 | constexpr static char PLAYER_LOST_OBJECTS[] = "lostObjects"; 49 | constexpr static char PLAYER_LOST_OBJECT_TYPE[] = "type"; 50 | constexpr static char PLAYER_LOST_OBJECT_POS[] = "pos"; 51 | 52 | constexpr static char MOVE_ACTION[] = "move"; 53 | 54 | constexpr static char TIME_DELTA[] = "timeDelta"; 55 | 56 | constexpr static char LOOT_GENERATOR_CONFIG[] = "lootGeneratorConfig"; 57 | constexpr static char LOOT_GENERATOR_PERIOD[] = "period"; 58 | constexpr static char LOOT_GENERATOR_PROBABILITY[] = "probability"; 59 | 60 | constexpr static char LOOT_TYPES_ARRAY[] = "lootTypes"; 61 | constexpr static char LOOT_NAME[] = "name"; 62 | constexpr static char LOOT_FILE[] = "file"; 63 | constexpr static char LOOT_TYPE[] = "type"; 64 | constexpr static char LOOT_ROTATION[] = "rotation"; 65 | constexpr static char LOOT_COLOR[] = "color"; 66 | constexpr static char LOOT_SCALE[] = "scale"; 67 | constexpr static char LOOT_VALUE[] = "value"; 68 | 69 | constexpr static char DEFAULT_BAG_CAPACITY[] = "defaultBagCapacity"; 70 | constexpr static char BAG_CAPACITY[] = "defaultBagCapacity"; 71 | 72 | constexpr static char DOG_RETIREMENT_TIME[] = "dogRetirementTime"; 73 | 74 | }; -------------------------------------------------------------------------------- /static/hall_of_fame.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Dog Story 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 81 | 82 | 83 |
84 |

Dog story

85 | Новая игра 86 |
Загрузка...
87 | 88 | 89 | 90 | 91 | 92 | 93 |
ИмяОчкиВремя в игре
94 |
95 | 116 | 117 | -------------------------------------------------------------------------------- /src/infrastructure/application_serialization.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "model/model_serialization.h" 4 | #include "application/application.h" 5 | 6 | #include 7 | 8 | #include 9 | 10 | namespace serialization { 11 | 12 | class PlayerRepr { 13 | public: 14 | PlayerRepr() = default; 15 | explicit PlayerRepr(const application::Player& player) 16 | : player_id_{ *player.GetDog()->GetDogId() } 17 | , game_session_id_{ *player.GetSession().GetMapId() } { 18 | } 19 | 20 | application::Player Restore(application::Application& app) { 21 | model::GameSession* session = app.GetGame().GetSession(game_session_id_); 22 | auto dog = session->GetDog(player_id_); 23 | return application::Player{ session, dog }; 24 | } 25 | 26 | template 27 | void serialize(Archive& ar, [[maybe_unused]] const unsigned version) { 28 | ar& player_id_; 29 | ar& *game_session_id_; 30 | } 31 | 32 | private: 33 | std::uint64_t player_id_; 34 | model::Map::Id game_session_id_{""}; 35 | }; 36 | 37 | class PlayerTokensRepr { 38 | public: 39 | PlayerTokensRepr() = default; 40 | 41 | explicit PlayerTokensRepr(const application::Players::PlayerIdToIndex& token_to_index) { 42 | for (const auto& [token, index] : token_to_index) { 43 | player_tokens_.emplace(*token, index); 44 | } 45 | } 46 | 47 | application::Players::PlayerIdToIndex Restore() { 48 | application::Players::PlayerIdToIndex token_to_index; 49 | for (const auto& [token, index] : player_tokens_) { 50 | token_to_index.emplace(application::Token(token), index); 51 | } 52 | 53 | return std::move(token_to_index); 54 | } 55 | 56 | template 57 | void serialize(Archive& ar, [[maybe_unused]] const unsigned version) { 58 | ar& player_tokens_; 59 | } 60 | 61 | private: 62 | std::map player_tokens_; 63 | }; 64 | 65 | class PlayersRepr { 66 | public: 67 | PlayersRepr() = default; 68 | explicit PlayersRepr(const application::Players& players) 69 | : player_tokens_repr_(players.GetPlayerIdToIndex()) { 70 | 71 | for (const auto& player : players.GetPlayers()) { 72 | players_repr_.emplace_back(PlayerRepr{player}); 73 | } 74 | } 75 | 76 | void Restore(application::Application& app, application::Players& appPlayers) { 77 | 78 | std::vector players; 79 | for (auto& player_repr : players_repr_) { 80 | players.emplace_back(player_repr.Restore(app)); 81 | } 82 | 83 | appPlayers.SetPlayers(std::move(players)); 84 | appPlayers.SetPlayerIdToIndex(player_tokens_repr_.Restore()); 85 | } 86 | 87 | template 88 | void serialize(Archive& ar, [[maybe_unused]] const unsigned version) { 89 | ar& players_repr_; 90 | ar& player_tokens_repr_; 91 | } 92 | 93 | private: 94 | std::vector players_repr_; 95 | PlayerTokensRepr player_tokens_repr_; 96 | }; 97 | 98 | 99 | class ApplicationRepr { 100 | public: 101 | ApplicationRepr() {} 102 | explicit ApplicationRepr(application::Application& app) 103 | : game_repr_(app.GetGame()) 104 | , players_repr_(app.GetPlayers()) 105 | , random_spawn_(app.GetRandomSpawn()) { 106 | } 107 | 108 | 109 | void Restore(application::Application& app) { 110 | game_repr_.Restore(app.GetGame()); 111 | players_repr_.Restore(app, app.GetPlayers()); 112 | app.SetRandomSpawn(random_spawn_); 113 | } 114 | 115 | template 116 | void serialize(Archive& ar, [[maybe_unused]] const unsigned version) { 117 | ar& game_repr_; 118 | ar& players_repr_; 119 | ar& random_spawn_; 120 | } 121 | 122 | private: 123 | GameRepr game_repr_; 124 | PlayersRepr players_repr_; 125 | bool random_spawn_ = false; 126 | }; 127 | 128 | } -------------------------------------------------------------------------------- /src/main.cpp: -------------------------------------------------------------------------------- 1 | #include "sdk.h" 2 | // 3 | #include 4 | #include 5 | 6 | #include 7 | #include 8 | 9 | #include "json_loader.h" 10 | #include "request_handler.h" 11 | #include "logger_helper.h" 12 | #include "application.h" 13 | #include "cli_helper.h" 14 | #include "ticker.h" 15 | #include "infrastructure/serializing_listener.h" 16 | #include "postgres.h" 17 | 18 | using namespace std::literals; 19 | namespace net = boost::asio; 20 | namespace sys = boost::system; 21 | 22 | namespace { 23 | 24 | // Запускает функцию fn на n потоках, включая текущий 25 | template 26 | void RunWorkers(unsigned n, const Fn& fn) { 27 | n = std::max(1u, n); 28 | std::vector workers; 29 | workers.reserve(n - 1); 30 | // Запускаем n-1 рабочих потоков, выполняющих функцию fn 31 | while (--n) { 32 | workers.emplace_back(fn); 33 | } 34 | fn(); 35 | } 36 | 37 | } // namespace 38 | 39 | 40 | int main(int argc, const char* argv[]) { 41 | try { 42 | 43 | auto args = cli_helpers::ParseCommandLine(argc, argv); 44 | 45 | if (!args) { 46 | // help page 47 | return EXIT_SUCCESS; 48 | } 49 | 50 | logger_helper::InitLogger(); 51 | 52 | constexpr const char GAME_DB_URL[] = "GAME_DB_URL"; 53 | 54 | std::string db_url; 55 | if (const auto* url = std::getenv(GAME_DB_URL)) { 56 | db_url = url; 57 | } 58 | else { 59 | throw std::runtime_error(GAME_DB_URL + " environment variable not found"s); 60 | } 61 | 62 | model::Game game = json_loader::LoadGame(args->config_file_path); 63 | postgres::Database db {db_url}; 64 | application::Application app(game, db, args->randomize_spawn_dog); 65 | std::shared_ptr listener; 66 | 67 | if (!args->state_file_path.empty()) 68 | { 69 | listener.reset(new infrastructure::SerializingListener{app, args->state_file_path}); 70 | 71 | if (args->save_state_period > 0) 72 | listener->SetSavePeriod(std::chrono::milliseconds(args->save_state_period)); 73 | 74 | app.SetUpdateListener(listener); 75 | } 76 | 77 | const unsigned num_threads = std::thread::hardware_concurrency(); 78 | net::io_context ioc(num_threads); 79 | 80 | net::signal_set signals(ioc, SIGINT, SIGTERM); 81 | 82 | signals.async_wait([&ioc](const sys::error_code& ec, [[maybe_unused]] int signal_number) { 83 | if (!ec) { 84 | ioc.stop(); 85 | } 86 | }); 87 | 88 | auto api_strand = net::make_strand(ioc); 89 | auto api_handler = std::make_shared(app, api_strand); 90 | 91 | if (args->tick_period > 0) { 92 | auto ticker = std::make_shared(api_strand, std::chrono::milliseconds(args->tick_period), 93 | [&app](std::chrono::milliseconds delta) { app.UpdateGameState(delta); } 94 | ); 95 | ticker->Start(); 96 | } 97 | 98 | http_handler::RequestHandler handler(api_handler, args->www_root); 99 | 100 | http_handler::LoggingRequestHandler logging_handler (handler); 101 | 102 | // 5. Запустить обработчик HTTP-запросов, делегируя их обработчику запросов 103 | const auto address = net::ip::make_address("0.0.0.0"); 104 | constexpr net::ip::port_type port = 8080; 105 | 106 | http_server::ServeHttp(ioc, {address, port}, logging_handler); 107 | 108 | // Эта надпись сообщает тестам о том, что сервер запущен и готов обрабатывать запросы 109 | json::object start_message; 110 | start_message["port"s] = port; 111 | start_message["address"s] = address.to_string(); 112 | 113 | BOOST_LOG_TRIVIAL(info) << logging::add_value(additional_data, start_message) 114 | << "server started"sv; 115 | 116 | // 6. Запускаем обработку асинхронных операций 117 | RunWorkers(std::max(1u, num_threads), [&ioc] { 118 | ioc.run(); 119 | }); 120 | 121 | if (listener) { 122 | listener->Save(); 123 | } 124 | 125 | json::value custom_data{{"code"s, 0}}; 126 | BOOST_LOG_TRIVIAL(info) << logging::add_value(additional_value, custom_data) 127 | << "server exited"sv; 128 | 129 | } catch ([[maybe_unused]] const std::exception& ex) { 130 | 131 | json::value custom_data{{"code"s, EXIT_FAILURE}}; 132 | BOOST_LOG_TRIVIAL(info) << logging::add_value(additional_value, custom_data) 133 | << "server exited"sv << " " << ex.what(); 134 | 135 | return EXIT_FAILURE; 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/request_handler/request_handler_helper.cpp: -------------------------------------------------------------------------------- 1 | #include "request_handler_helper.h" 2 | 3 | namespace http_handler { 4 | 5 | StringResponse MakeStringResponse(const http::status status, const std::string_view body, unsigned int http_version, bool keep_alive) { 6 | StringResponse response(status, http_version); 7 | response.set(http::field::content_type, ContentType::APPLICATION_JSON); 8 | response.body() = body; 9 | response.content_length(body.size()); 10 | response.keep_alive(keep_alive); 11 | response.set(http::field::cache_control, "no-cache"); 12 | 13 | return response; 14 | } 15 | 16 | StringResponse MakeErrorResponse(const http::status status, const std::string_view code, const std::string_view message, unsigned int http_version, bool keep_alive) 17 | { 18 | json::object error_message; 19 | error_message["code"] = std::string(code); 20 | error_message["message"] = std::string(message); 21 | 22 | return MakeStringResponse(status, json::serialize(error_message), http_version, keep_alive); 23 | } 24 | 25 | StringResponse MakeBadRequest(const std::string_view code, const std::string_view message, unsigned int http_version, bool keep_alive) 26 | { 27 | return MakeErrorResponse(http::status::bad_request, code, message, http_version, keep_alive); 28 | } 29 | 30 | StringResponse MakeNotAlowedResponse(const std::string_view message, const std::string_view allow, unsigned int http_version, bool keep_alive) 31 | { 32 | auto resp = MakeErrorResponse(http::status::method_not_allowed, "invalidMethod"sv, message, http_version, keep_alive); 33 | resp.set(http::field::allow, allow); 34 | return resp; 35 | } 36 | 37 | StringResponse MakeNotFoundResponse(const std::string_view code, const std::string_view message, unsigned int http_version, bool keep_alive) 38 | { 39 | return MakeErrorResponse(http::status::not_found, code, message, http_version, keep_alive); 40 | } 41 | 42 | StringResponse MakeUnauthorizedResponse(const std::string_view code, const std::string_view message, unsigned int http_version, bool keep_alive) 43 | { 44 | return MakeErrorResponse(http::status::unauthorized, code, message, http_version, keep_alive); 45 | } 46 | 47 | void PrettyPrint( std::ostream& os, json::value const& jv, std::string* indent) { 48 | std::string indent_; 49 | if(!indent) 50 | indent = &indent_; 51 | 52 | switch(jv.kind()) 53 | { 54 | case json::kind::object: 55 | { 56 | os << "{\n"; 57 | indent->append(4, ' '); 58 | auto const& obj = jv.get_object(); 59 | if(!obj.empty()) 60 | { 61 | auto it = obj.begin(); 62 | for(;;) 63 | { 64 | os << *indent << json::serialize(it->key()) << " : "; 65 | PrettyPrint(os, it->value(), indent); 66 | if(++it == obj.end()) 67 | break; 68 | os << ",\n"; 69 | } 70 | } 71 | os << "\n"; 72 | indent->resize(indent->size() - 4); 73 | os << *indent << "}"; 74 | break; 75 | } 76 | 77 | case json::kind::array: 78 | { 79 | os << "[\n"; 80 | indent->append(4, ' '); 81 | auto const& arr = jv.get_array(); 82 | if(! arr.empty()) 83 | { 84 | auto it = arr.begin(); 85 | for(;;) 86 | { 87 | os << *indent; 88 | PrettyPrint( os, *it, indent); 89 | if(++it == arr.end()) 90 | break; 91 | os << ",\n"; 92 | } 93 | } 94 | os << "\n"; 95 | indent->resize(indent->size() - 4); 96 | os << *indent << "]"; 97 | break; 98 | } 99 | 100 | case json::kind::string: 101 | { 102 | os << json::serialize(jv.get_string()); 103 | break; 104 | } 105 | 106 | case json::kind::uint64: 107 | os << jv.get_uint64(); 108 | break; 109 | 110 | case json::kind::int64: 111 | os << jv.get_int64(); 112 | break; 113 | 114 | case json::kind::double_: 115 | os << std::fixed << std::setprecision(5) 116 | << jv.get_double(); 117 | break; 118 | 119 | case json::kind::bool_: 120 | if(jv.get_bool()) 121 | os << "true"; 122 | else 123 | os << "false"; 124 | break; 125 | 126 | case json::kind::null: 127 | os << "null"; 128 | break; 129 | } 130 | 131 | if(indent->empty()) 132 | os << "\n"; 133 | } 134 | 135 | std::string PrettySerialize(json::value const& jv) { 136 | std::stringstream ss; 137 | PrettyPrint(ss, jv); 138 | 139 | return ss.str(); 140 | } 141 | 142 | std::vector SplitUriPath(const std::string_view uri_path) 143 | { 144 | std::vector tokens; 145 | boost::split(tokens, uri_path, boost::is_any_of("/")); 146 | return tokens; 147 | } 148 | } -------------------------------------------------------------------------------- /src/application/application.cpp: -------------------------------------------------------------------------------- 1 | #include "application.h" 2 | 3 | namespace application { 4 | 5 | Application::Application(model::Game &game, postgres::Database& db, bool random_spawn) 6 | : game_(game) 7 | , random_spawn_(random_spawn) 8 | , db_(db) { 9 | } 10 | 11 | const model::Game::Maps &Application::GetMaps() { 12 | return game_.GetMaps(); 13 | } 14 | 15 | const model::Map *Application::FindMap(std::string_view id) { 16 | return game_.FindMap(model::Map::Id(std::string(id))); 17 | } 18 | 19 | AuthResponse Application::JoinToGame(std::string_view user_name, std::string_view map_id) { 20 | 21 | auto map_id_t = model::Map::Id(std::string(map_id)); 22 | model::GameSession* session = game_.GetSession(map_id_t); 23 | 24 | auto dog = std::make_shared(user_name); 25 | 26 | auto def_speed_global = game_.GetDefaultDogSpeed(); 27 | auto def_speed_map = game_.FindMap(map_id_t)->GetDogSpeed(); 28 | 29 | if (def_speed_map) { 30 | dog->SetDefaultSpeed(*def_speed_map); 31 | } 32 | else if (def_speed_global) { 33 | dog->SetDefaultSpeed(*def_speed_global); 34 | } 35 | 36 | auto def_bag_capacity = game_.GetDefaultBagCapacity(); 37 | auto def_map_capacity = game_.FindMap(map_id_t)->GetBagCapacity(); 38 | 39 | if (def_bag_capacity) { 40 | dog->SetBagCapacity(*def_bag_capacity); 41 | } 42 | else if (def_map_capacity) { 43 | dog->SetBagCapacity(*def_map_capacity); 44 | } 45 | 46 | std::uint64_t dog_id = session->AddDog(dog, random_spawn_); 47 | 48 | auto [player, token] = players_.AddPlayer(dog, session); 49 | 50 | return {*token, dog_id}; 51 | } 52 | 53 | bool Application::IsAuthorized(std::string_view token) { 54 | return players_.IsTokenValid(Token(std::string(token))); 55 | } 56 | 57 | void Application::ProcessRetirementPlayers(const std::vector& ids_to_remove) { 58 | 59 | std::vector infos; 60 | 61 | for (const auto& dog_id : ids_to_remove) { 62 | 63 | auto player = players_.FindByDogId(dog_id); 64 | 65 | if (!player) 66 | continue; 67 | 68 | auto dog = player->GetDog(); 69 | 70 | auto name = dog->GetName(); 71 | auto score = dog->GetScore(); 72 | auto uptime = dog->GetUptime(); 73 | 74 | infos.push_back({ name, score, uptime }); 75 | 76 | players_.RemovePlayerByDogId(dog_id); 77 | } 78 | 79 | if (!infos.empty()) { 80 | db_.AddRecords(infos); 81 | } 82 | } 83 | 84 | void Application::Move(const std::string_view token, char direction) 85 | { 86 | auto player = players_.FindByToken(Token{ std::string(token) }); 87 | auto dog = player->GetDog(); 88 | 89 | dog->Move(model::Direction(direction)); 90 | } 91 | 92 | void Application::UpdateGameState(const std::chrono::milliseconds time_delta) { 93 | 94 | std::vector all_ids_to_remove; 95 | 96 | for (auto& session : game_.GetSessions()) { 97 | auto sessions_ids_to_remove = session.second.UpdateGameState(time_delta.count()); 98 | 99 | std::transform(sessions_ids_to_remove.begin(), sessions_ids_to_remove.end(), 100 | std::back_inserter(all_ids_to_remove), [](auto id) { return id; }); 101 | } 102 | 103 | ProcessRetirementPlayers(all_ids_to_remove); 104 | 105 | if (update_listener_) { 106 | update_listener_->OnUpdate(time_delta); 107 | } 108 | } 109 | 110 | std::vector& Application::GetAllPlayers() { 111 | return players_.GetPlayers(); 112 | } 113 | 114 | GameState Application::GetState(std::string_view token) 115 | { 116 | GameState states; 117 | 118 | for (auto& player : GetAllPlayers()) { 119 | PlayerState state; 120 | 121 | auto dog = player.GetDog(); 122 | state.dog_id_ = std::to_string(*dog->GetDogId()); 123 | 124 | std::string dir = std::string{ dog->GetDirection() }; 125 | state.dog_direction_ = dir == "S" ? "" : dir; 126 | 127 | auto pos = dog->GetPosition(); 128 | state.position_x_ = pos.x; 129 | state.position_y_ = pos.y; 130 | 131 | auto speed = dog->GetSpeed(); 132 | state.horizontal_speed_ = speed.horizontal; 133 | state.vertical_speed_ = speed.vertical; 134 | 135 | state.bag_ = dog->GetBag().GetObjects(); 136 | state.score_ = dog->GetScore(); 137 | 138 | states.players_state_.push_back(state); 139 | } 140 | 141 | auto player = players_.FindByToken(Token(std::string(token))); 142 | 143 | if (player) { 144 | auto loot = player->GetSession()->GetLootStates(); 145 | states.loots_state_ = std::move(loot); 146 | } 147 | 148 | return states; 149 | } 150 | RecordsInfo Application::GetRecordsInfo(std::optional start, std::optional maxItems) 151 | { 152 | auto records = db_.GetRecords(start, maxItems); 153 | RecordsInfo records_info; 154 | 155 | for (auto& record : records) { 156 | records_info.push_back({ record.name, record.score, record.play_time }); 157 | } 158 | 159 | return records_info; 160 | } 161 | 162 | } -------------------------------------------------------------------------------- /src/application/player.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "model.h" 4 | 5 | #include 6 | #include 7 | #include 8 | 9 | namespace application { 10 | 11 | struct TokenTag {}; 12 | 13 | using Token = util::Tagged; 14 | using TokenHasher = util::TaggedHasher; 15 | 16 | class Player { 17 | public: 18 | Player(model::GameSession* session, std::shared_ptr dog); 19 | 20 | model::GameSession* GetSession(); 21 | std::shared_ptr GetDog(); 22 | 23 | model::GameSession GetSession() const; 24 | std::shared_ptr GetDog() const; 25 | 26 | private: 27 | model::GameSession* session_; 28 | std::shared_ptr dog_; 29 | }; 30 | 31 | class Players { 32 | public: 33 | Players() = default; 34 | 35 | Players& operator=(const Players& other) { 36 | if (this != &other) { 37 | players_ = other.players_; 38 | player_id_to_index_ = other.player_id_to_index_; 39 | } 40 | return *this; 41 | } 42 | 43 | using PlayerIdToIndex = std::unordered_map; 44 | 45 | std::pair AddPlayer(std::shared_ptr dog, model::GameSession* session) { 46 | 47 | Token token = GenerateToken(); 48 | 49 | players_.emplace_back(Player(session, dog)); 50 | 51 | player_id_to_index_[token] = players_.size() - 1; 52 | 53 | return {&players_.back(), token}; 54 | } 55 | 56 | bool IsTokenValid(Token token) const { 57 | return player_id_to_index_.find(token) != player_id_to_index_.end(); 58 | } 59 | 60 | 61 | Player* FindByToken(Token token) { 62 | if (auto it = player_id_to_index_.find(token); it != player_id_to_index_.end()) { 63 | return &players_.at(it->second); 64 | } 65 | return nullptr; 66 | } 67 | 68 | std::optional FindByToken(Token token) const { 69 | if (auto it = player_id_to_index_.find(token); it != player_id_to_index_.end()) { 70 | return players_.at(it->second); 71 | } 72 | 73 | return std::nullopt; 74 | } 75 | 76 | Player* FindByDogIdAndMapId(model::Dog::Id dog_id, model::Map::Id map_id) { 77 | for (Player& player : players_) { 78 | if (player.GetDog()->GetDogId() == dog_id && player.GetSession()->GetMapId() == map_id) { 79 | return &player; 80 | } 81 | } 82 | return nullptr; 83 | } 84 | 85 | std::optional FindByDogId(const model::Dog::Id& dog_id) const { 86 | for (const Player& player : players_) { 87 | if (player.GetDog()->GetDogId() == dog_id) { 88 | return player; 89 | } 90 | } 91 | return std::nullopt; 92 | } 93 | 94 | void RemovePlayerByDogId(const model::Dog::Id& dog_id) { 95 | 96 | auto it = std::find_if(players_.begin(), players_.end(), [&dog_id](const Player& player) { 97 | return player.GetDog()->GetDogId() == dog_id; 98 | }); 99 | 100 | if (it == players_.end()) { 101 | return; 102 | } 103 | 104 | auto indexToRemove = std::distance(players_.begin(), it); 105 | 106 | players_.erase(it); 107 | 108 | auto tokenIt = std::find_if(player_id_to_index_.begin(), player_id_to_index_.end(), 109 | [indexToRemove](const auto& pair) { return pair.second == indexToRemove; }); 110 | 111 | if (tokenIt != player_id_to_index_.end()) { 112 | player_id_to_index_.erase(tokenIt); 113 | } 114 | 115 | for (auto& pair : player_id_to_index_) { 116 | if (pair.second > indexToRemove) { 117 | pair.second--; 118 | } 119 | } 120 | } 121 | 122 | std::vector& GetPlayers() { 123 | return players_; 124 | } 125 | 126 | std::vector GetPlayers() const { 127 | return players_; 128 | } 129 | 130 | void SetPlayers(const std::vector& players) { 131 | players_ = std::move(players); 132 | } 133 | 134 | PlayerIdToIndex GetPlayerIdToIndex() const { 135 | return player_id_to_index_; 136 | } 137 | 138 | void SetPlayerIdToIndex(const PlayerIdToIndex& player_id_to_index) { 139 | player_id_to_index_ = player_id_to_index; 140 | } 141 | 142 | private: 143 | 144 | Token GenerateToken() { 145 | std::stringstream ss; 146 | ss << std::hex << std::setw(16) << std::setfill('0') << generator1_(); 147 | ss << std::setw(16) << std::setfill('0') << generator2_(); 148 | 149 | return Token(ss.str()); 150 | } 151 | 152 | std::random_device random_device_; 153 | std::mt19937_64 generator1_{[this] { 154 | std::uniform_int_distribution dist; 155 | return dist(random_device_); 156 | }()}; 157 | std::mt19937_64 generator2_{[this] { 158 | std::uniform_int_distribution dist; 159 | return dist(random_device_); 160 | }()}; 161 | 162 | std::vector players_; 163 | PlayerIdToIndex player_id_to_index_; 164 | }; 165 | 166 | } -------------------------------------------------------------------------------- /src/request_handler/api_request_handler.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "request_handler_helper.h" 4 | #include "application.h" 5 | 6 | #include 7 | #include 8 | 9 | 10 | namespace http_handler { 11 | 12 | inline std::unordered_map parseParameters(const std::string& uri) { 13 | std::unordered_map params; 14 | 15 | std::regex paramRegex("(\\w+)=(\\w+)"); 16 | std::smatch match; 17 | 18 | std::string::const_iterator searchStart(uri.cbegin()); 19 | while (std::regex_search(searchStart, uri.cend(), match, paramRegex)) { 20 | params[match[1].str()] = match[2].str(); 21 | searchStart = match.suffix().first; 22 | } 23 | 24 | return params; 25 | } 26 | 27 | 28 | using Strand = net::strand; 29 | using HandlersMap = std::map>; 30 | 31 | class APIRequestHandler : public std::enable_shared_from_this { 32 | public: 33 | APIRequestHandler(application::Application& app, Strand api_strand) 34 | : app_(app) 35 | , api_strand_(api_strand) {} 36 | 37 | template 38 | void Handle(http::request>&& req, Send&& send) { 39 | 40 | auto handle_api = [self = shared_from_this(), send, 41 | req = std::forward(req)] { 42 | assert(self->api_strand_.running_in_this_thread()); 43 | return send(self->HandleAPIRequest(req)); 44 | }; 45 | 46 | return net::dispatch(api_strand_, handle_api); 47 | } 48 | 49 | application::Application& GetApplication() { return app_; } 50 | 51 | private: 52 | template 53 | StringResponse HandleAPIRequest(const http::request>& req) { 54 | 55 | using namespace std::placeholders; 56 | static auto get_players = std::bind(&APIRequestHandler::GetPlayers, this, _1, _2); 57 | static auto get_state = std::bind(&APIRequestHandler::GetState, this, _1, _2); 58 | static auto action = std::bind(&APIRequestHandler::Action, this, _1, _2); 59 | 60 | // TODO REFACTOR 61 | if (std::string(req.target().begin(), req.target().end()) == "/api/v1/game/state") 62 | { 63 | if (req.method() != http::verb::get && req.method() != http::verb::head) 64 | return MakeNotAlowedResponse("Invalid method"sv, "GET, HEAD"sv, req.version(), req.keep_alive()); 65 | } 66 | auto endp = std::string(req.target().begin(), req.target().end()); 67 | if (endp.starts_with("/api/v1/game/records")) { 68 | auto params = parseParameters(endp); 69 | 70 | std::optional start; 71 | if (params.count("start") > 0) { 72 | start = std::stoi(params["start"]); 73 | } 74 | 75 | std::optional maxItems; 76 | if (params.count("maxItems") > 0) { 77 | maxItems = std::stoi(params["maxItems"]); 78 | } 79 | 80 | if (maxItems && *maxItems > 100) 81 | return MakeBadRequest("invalidArgument"sv, "Max items must len than 100"sv, req.version(), req.keep_alive()); 82 | 83 | return GetRecords(req, start, maxItems); 84 | } 85 | 86 | const static HandlersMap handlers { 87 | {"/api/v1/game/join"sv, [this](const auto& req) { return this->JoinToGame(req);}}, 88 | {"/api/v1/game/players"sv,[this](const auto& req) { return this->ExecuteAuthorized(get_players, req); }}, 89 | {"/api/v1/game/state"sv, [this](const auto& req) { return this->ExecuteAuthorized(get_state, req); }}, 90 | {"/api/v1/game/player/action"sv, [this](const auto& req) { return this->ExecuteAuthorized(action, req); }}, 91 | {"/api/v1/game/tick"sv, [this](const auto& req) { return this->Tick(req); }} 92 | }; 93 | 94 | auto handler_it = handlers.find(std::string(req.target().begin(), req.target().end())); 95 | 96 | if (handler_it == handlers.end()) 97 | return MakeBadRequest("badRequest"sv, "Bad request"sv, req.version(), req.keep_alive()); 98 | 99 | return handler_it->second(req); 100 | } 101 | 102 | template 103 | StringResponse ExecuteAuthorized(Fn&& action, const StringRequest& req) { 104 | 105 | std::string authHeader {req[http::field::authorization]}; 106 | static const std::string BEARER = "Bearer "; 107 | const uint16_t TOKEN_LENGTH = 32; 108 | 109 | if (authHeader.empty() || !authHeader.starts_with(BEARER) || authHeader.substr(BEARER.length()).length() < TOKEN_LENGTH) 110 | return MakeUnauthorizedResponse("invalidToken"sv, "Authorization header is missing"sv, req.version(), req.keep_alive()); 111 | 112 | auto token = authHeader.substr(BEARER.length()); 113 | 114 | if (!app_.IsAuthorized(token)) 115 | return MakeUnauthorizedResponse("unknownToken"sv, "Player token has not been found"sv, req.version(), req.keep_alive()); 116 | 117 | return action(req, token); 118 | } 119 | 120 | StringResponse JoinToGame(const StringRequest& req); 121 | StringResponse GetPlayers(const StringRequest &req, const std::string_view token); 122 | StringResponse GetState(const StringRequest& req, const std::string_view token); 123 | StringResponse Action(const StringRequest& req, const std::string_view token); 124 | StringResponse Tick(const StringRequest& req); 125 | StringResponse GetRecords(const StringRequest& req, std::optional start, std::optional maxItems); 126 | 127 | application::Application& app_; 128 | Strand api_strand_; 129 | }; 130 | 131 | } 132 | -------------------------------------------------------------------------------- /static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Dog Story 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 145 | 146 | 147 |
148 |

Dog story

149 |
150 | 151 | 152 | 153 | 154 |
155 | 156 |
157 |
158 | 195 | 196 | -------------------------------------------------------------------------------- /src/model/model_serialization.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include "model.h" 6 | 7 | namespace geom { 8 | 9 | template 10 | void serialize(Archive& ar, Point2D& point, [[maybe_unused]] const unsigned version) { 11 | ar& point.x; 12 | ar& point.y; 13 | } 14 | 15 | } // namespace geom 16 | 17 | namespace model { 18 | 19 | template 20 | void serialize(Archive& ar, model::Speed& vec, [[maybe_unused]] const unsigned version) { 21 | ar& vec.horizontal; 22 | ar& vec.vertical; 23 | } 24 | 25 | template 26 | void serialize(Archive& ar, LootState& obj, [[maybe_unused]] const unsigned version) { 27 | ar& (obj.id); 28 | ar& (obj.type); 29 | ar& (obj.position); 30 | ar& (obj.value_); 31 | ar& (obj.width); 32 | ar& (obj.is_picked_up); 33 | } 34 | 35 | template 36 | void serialize(Archive& ar, model::LootGeneratorConfig& obj, [[maybe_unused]] const unsigned version) { 37 | ar& (obj.period_); 38 | ar& (obj.probability_); 39 | 40 | } 41 | 42 | } // namespace model 43 | 44 | namespace serialization { 45 | 46 | // DogRepr (DogRepresentation) - сериализованное представление класса Dog 47 | class DogRepr { 48 | public: 49 | DogRepr() = default; 50 | 51 | explicit DogRepr(const model::Dog& dog) 52 | : id_(dog.GetDogId()) 53 | , name_(dog.GetName()) 54 | , pos_(dog.GetPosition()) 55 | , bag_capacity_(dog.GetBag().GetCapacity()) 56 | , speed_(dog.GetSpeed()) 57 | , direction_(dog.GetDirectionEnm()) 58 | , score_(dog.GetScore()) 59 | , bag_content_(dog.GetBag().GetObjects()) { 60 | } 61 | 62 | [[nodiscard]] model::Dog Restore() const { 63 | model::Dog dog{name_}; 64 | dog.SetId(*id_); 65 | dog.SetPosition(pos_); 66 | dog.SetBagCapacity(bag_capacity_); 67 | dog.SetSpeed(speed_); 68 | dog.SetDirection(direction_); 69 | dog.AccumulateScore(score_); 70 | for (const auto& item : bag_content_) { 71 | if (!dog.PutToBag(item)) { 72 | throw std::runtime_error("Failed to put bag content"); 73 | } 74 | } 75 | return dog; 76 | } 77 | 78 | template 79 | void serialize(Archive& ar, [[maybe_unused]] const unsigned version) { 80 | ar&* id_; 81 | ar& name_; 82 | ar& pos_; 83 | ar& bag_capacity_; 84 | ar& speed_; 85 | ar& direction_; 86 | ar& score_; 87 | ar& bag_content_; 88 | } 89 | 90 | private: 91 | model::Dog::Id id_ = model::Dog::Id{0u}; 92 | std::string name_; 93 | geom::Point2D pos_; 94 | size_t bag_capacity_ = 0; 95 | model::Speed speed_; 96 | model::Direction direction_ = model::Direction::NORTH; 97 | uint64_t score_ = 0; 98 | model::LootStates bag_content_; 99 | }; 100 | 101 | class GameSessionRepr { 102 | public: 103 | GameSessionRepr() = default; 104 | 105 | explicit GameSessionRepr(const model::GameSession& session) 106 | : map_id_(session.GetMapId()) 107 | , last_dog_id(session.GetDogIdCounter()) 108 | , loot_states_(session.GetLootStates()) 109 | , loot_gen_config_(session.GetLootGeneratorConfig()) { 110 | 111 | for (const auto& dog : session.GetDogs()) { 112 | dogs_repr_.emplace_back(DogRepr(*dog)); 113 | } 114 | } 115 | 116 | model::GameSession Restore( 117 | const model::Map* map, 118 | const model::LootGeneratorConfig loot_generator_config) { 119 | 120 | model::GameSession game_session(*map, loot_generator_config, 15.0); 121 | std::vector> dogs; 122 | 123 | for (const auto& dog_repr : dogs_repr_) { 124 | dogs.emplace_back(new model::Dog(dog_repr.Restore())); 125 | } 126 | 127 | game_session.EmplaceDogs(dogs); 128 | game_session.SetDogIdCounter(last_dog_id); 129 | game_session.SetLootStates(loot_states_); 130 | 131 | 132 | return game_session; 133 | } 134 | 135 | template 136 | void serialize(Archive& ar, [[maybe_unused]] const unsigned version) { 137 | ar& *map_id_; 138 | ar& last_dog_id; 139 | ar& loot_states_; 140 | ar& dogs_repr_; 141 | ar& loot_gen_config_; 142 | } 143 | 144 | model::Map::Id GetMapId() { 145 | return map_id_; 146 | } 147 | 148 | private: 149 | model::Map::Id map_id_ {""}; 150 | std::uint64_t last_dog_id {0}; 151 | model::LootStates loot_states_; 152 | std::vector dogs_repr_; 153 | model::LootGeneratorConfig loot_gen_config_; 154 | }; 155 | 156 | class GameSessionsRepr { 157 | public: 158 | GameSessionsRepr() = default; 159 | 160 | explicit GameSessionsRepr(const model::Game::GameSessions& sessions) { 161 | 162 | for (const auto& session : sessions) { 163 | game_sessions_repr_.emplace_back(GameSessionRepr(session.second)); 164 | } 165 | } 166 | 167 | model::Game::GameSessions Restore(const model::Game& game) { 168 | 169 | model::Game::GameSessions game_sessions; 170 | 171 | for (auto& repr : game_sessions_repr_) { 172 | auto* map = game.FindMap(repr.GetMapId()); 173 | game_sessions.emplace(map->GetId(), repr.Restore(map, game.GetLootGeneratorConfig())); 174 | } 175 | 176 | return game_sessions; 177 | } 178 | 179 | template 180 | void serialize(Archive& ar, [[maybe_unused]] const unsigned version) { 181 | ar& game_sessions_repr_; 182 | } 183 | 184 | private: 185 | std::vector game_sessions_repr_; 186 | 187 | }; 188 | 189 | class GameRepr { 190 | public: 191 | GameRepr() = default; 192 | 193 | explicit GameRepr(const model::Game& game) 194 | : game_sessions_repr_(game.GetSessions()) { 195 | } 196 | 197 | void Restore(model::Game& game) { 198 | 199 | game.SetGameSessions(game_sessions_repr_.Restore(game)); 200 | } 201 | 202 | template 203 | void serialize(Archive& ar, [[maybe_unused]] const unsigned version) { 204 | ar& game_sessions_repr_; 205 | } 206 | 207 | private: 208 | GameSessionsRepr game_sessions_repr_; 209 | }; 210 | 211 | } // namespace serialization 212 | -------------------------------------------------------------------------------- /tests/collision-detector-tests.cpp: -------------------------------------------------------------------------------- 1 | #define _USE_MATH_DEFINES 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include 8 | #include 9 | 10 | #include "../src/model/collision_detector.h" 11 | 12 | namespace Catch { 13 | template<> 14 | struct StringMaker { 15 | static std::string convert(collision_detector::GatheringEvent const& value) { 16 | std::ostringstream tmp; 17 | tmp << "(" << value.gatherer_id << value.item_id << value.sq_distance << value.time << ")"; 18 | 19 | return tmp.str(); 20 | } 21 | }; 22 | } // namespace Catch 23 | 24 | namespace { 25 | 26 | template 27 | struct EqualsRangeMatcher : Catch::Matchers::MatcherGenericBase { 28 | EqualsRangeMatcher(Range const& range, Predicate predicate) 29 | : range_{range} 30 | , predicate_{predicate} { 31 | } 32 | 33 | template 34 | bool match(const OtherRange& other) const { 35 | using std::begin; 36 | using std::end; 37 | 38 | return std::equal(begin(range_), end(range_), begin(other), end(other), predicate_); 39 | } 40 | 41 | std::string describe() const override { 42 | return "Equals: " + Catch::rangeToString(range_); 43 | } 44 | 45 | private: 46 | const Range& range_; 47 | Predicate predicate_; 48 | }; 49 | 50 | template 51 | auto EqualsRange(const Range& range, Predicate prediate) { 52 | return EqualsRangeMatcher{range, prediate}; 53 | } 54 | 55 | class VectorItemGathererProvider : public collision_detector::ItemGathererProvider { 56 | public: 57 | VectorItemGathererProvider(std::vector items, 58 | std::vector gatherers) 59 | : items_(items) 60 | , gatherers_(gatherers) { 61 | } 62 | 63 | 64 | size_t ItemsCount() const override { 65 | return items_.size(); 66 | } 67 | collision_detector::Item GetItem(size_t idx) const override { 68 | return items_[idx]; 69 | } 70 | size_t GatherersCount() const override { 71 | return gatherers_.size(); 72 | } 73 | collision_detector::Gatherer GetGatherer(size_t idx) const override { 74 | return gatherers_[idx]; 75 | } 76 | 77 | private: 78 | std::vector items_; 79 | std::vector gatherers_; 80 | }; 81 | 82 | class CompareEvents { 83 | public: 84 | bool operator()(const collision_detector::GatheringEvent& l, 85 | const collision_detector::GatheringEvent& r) { 86 | if (l.gatherer_id != r.gatherer_id || l.item_id != r.item_id) 87 | return false; 88 | 89 | static const double eps = 1e-10; 90 | 91 | if (std::abs(l.sq_distance - r.sq_distance) > eps) { 92 | return false; 93 | } 94 | 95 | if (std::abs(l.time - r.time) > eps) { 96 | return false; 97 | } 98 | return true; 99 | } 100 | }; 101 | 102 | } 103 | 104 | SCENARIO("Collision detection") { 105 | WHEN("no items") { 106 | VectorItemGathererProvider provider{ 107 | {}, {{{1, 2}, {4, 2}, 5.}, {{0, 0}, {10, 10}, 5.}, {{-5, 0}, {10, 5}, 5.}}}; 108 | THEN("No events") { 109 | auto events = collision_detector::FindGatherEvents(provider); 110 | CHECK(events.empty()); 111 | } 112 | } 113 | WHEN("no gatherers") { 114 | VectorItemGathererProvider provider{ 115 | {{{1, 2}, 5.}, {{0, 0}, 5.}, {{-5, 0}, 5.}}, {}}; 116 | THEN("No events") { 117 | auto events = collision_detector::FindGatherEvents(provider); 118 | CHECK(events.empty()); 119 | } 120 | } 121 | WHEN("multiple items on a way of gatherer") { 122 | VectorItemGathererProvider provider{{ 123 | {{9, 0.27}, .1}, 124 | {{8, 0.24}, .1}, 125 | {{7, 0.21}, .1}, 126 | {{6, 0.18}, .1}, 127 | {{5, 0.15}, .1}, 128 | {{4, 0.12}, .1}, 129 | {{3, 0.09}, .1}, 130 | {{2, 0.06}, .1}, 131 | {{1, 0.03}, .1}, 132 | {{0, 0.0}, .1}, 133 | {{-1, 0}, .1}, 134 | }, { 135 | {{0, 0}, {10, 0}, 0.1}, 136 | }}; 137 | THEN("Gathered items in right order") { 138 | auto events = collision_detector::FindGatherEvents(provider); 139 | CHECK_THAT( 140 | events, 141 | EqualsRange(std::vector{ 142 | collision_detector::GatheringEvent{9, 0,0.*0., 0.0}, 143 | collision_detector::GatheringEvent{8, 0,0.03*0.03, 0.1}, 144 | collision_detector::GatheringEvent{7, 0,0.06*0.06, 0.2}, 145 | collision_detector::GatheringEvent{6, 0,0.09*0.09, 0.3}, 146 | collision_detector::GatheringEvent{5, 0,0.12*0.12, 0.4}, 147 | collision_detector::GatheringEvent{4, 0,0.15*0.15, 0.5}, 148 | collision_detector::GatheringEvent{3, 0,0.18*0.18, 0.6}, 149 | }, CompareEvents())); 150 | } 151 | } 152 | WHEN("multiple gatherers and one item") { 153 | VectorItemGathererProvider provider{{ 154 | {{0, 0}, 0.}, 155 | }, 156 | { 157 | {{-5, 0}, {5, 0}, 1.}, 158 | {{0, 1}, {0, -1}, 1.}, 159 | {{-10, 10}, {101, -100}, 0.5}, 160 | {{-100, 100}, {10, -10}, 0.5}, 161 | } 162 | }; 163 | THEN("Item gathered by faster gatherer") { 164 | auto events = collision_detector::FindGatherEvents(provider); 165 | CHECK(events.front().gatherer_id == 2); 166 | } 167 | } 168 | WHEN("Gatherers stay put") { 169 | VectorItemGathererProvider provider{{ 170 | {{0, 0}, 10.}, 171 | }, 172 | { 173 | {{-5, 0}, {-5, 0}, 1.}, 174 | {{0, 0}, {0, 0}, 1.}, 175 | {{-10, 10}, {-10, 10}, 100} 176 | } 177 | }; 178 | THEN("No events detected") { 179 | auto events = collision_detector::FindGatherEvents(provider); 180 | 181 | CHECK(events.empty()); 182 | } 183 | } 184 | } -------------------------------------------------------------------------------- /src/request_handler/request_handler.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "request_handler_helper.h" 4 | #include "model.h" 5 | #include "logger_helper.h" 6 | #include "api_request_handler.h" 7 | 8 | #include 9 | 10 | #include 11 | 12 | namespace http_handler { 13 | 14 | 15 | class RequestHandler { 16 | public: 17 | 18 | RequestHandler(std::shared_ptr api_handler, const fs::path& root_path) 19 | : root_path_{root_path} 20 | , api_request_handler_{api_handler} 21 | , app_(api_handler->GetApplication()) { 22 | } 23 | 24 | RequestHandler(const RequestHandler&) = delete; 25 | RequestHandler& operator=(const RequestHandler&) = delete; 26 | 27 | template 28 | void operator()(http::request>&& req, Send&& send) { 29 | // Обработать запрос request и отправить ответ, используя send 30 | 31 | std::string target(req.target().data(), req.target().size()); 32 | 33 | // Проверяем, относится ли звапрос к API 34 | // заппросы карт/карты обрабатываем тут, тк это статичные данные 35 | static const std::string PREFIX = "/api/v1/"; 36 | if (target.starts_with(PREFIX)) 37 | { 38 | std::string resource_type = target.substr(PREFIX.length()); 39 | if (resource_type.starts_with("maps")) 40 | { 41 | if (req.method() != http::verb::get && req.method() != http::verb::head) 42 | return send(MakeNotAlowedResponse("Invalid method"sv, "GET, HEAD"sv, req.version(), req.keep_alive())); 43 | 44 | auto tokens = SplitUriPath(resource_type); 45 | // /maps or /map/map_id 46 | if (tokens.size() != 2) 47 | return send(GetAllMaps(req.version(), req.keep_alive())); 48 | 49 | return send(GetMapById(tokens[1], req.version(), req.keep_alive())); 50 | } 51 | 52 | return api_request_handler_->Handle(std::forward(req), std::forward(send)); 53 | } 54 | 55 | std::string decoded_uri = PercentDecode(target); 56 | 57 | assert(!decoded_uri.empty()); 58 | 59 | fs::path requested_path = root_path_; 60 | 61 | if (decoded_uri == "/") 62 | requested_path /= fs::path("index.html"sv); 63 | else 64 | requested_path /= fs::path(decoded_uri.substr(1)); 65 | 66 | if (!IsSubPath(requested_path, root_path_)) 67 | { 68 | StringResponse response; 69 | response.result(http::status::bad_request); 70 | response.insert(http::field::content_type, ContentType::TEXT_PLAIN); 71 | response.body() = "I didn't invite you into my home/"sv; 72 | return send(response); 73 | } 74 | 75 | if (!fs::exists(requested_path)) 76 | { 77 | StringResponse response; 78 | response.result(http::status::not_found); 79 | response.insert(http::field::content_type, ContentType::TEXT_PLAIN); 80 | response.body() = "Sorry Mario, our princess on another server"; 81 | return send(response); 82 | } 83 | 84 | http::file_body::value_type file; 85 | boost::system::error_code ec; 86 | 87 | file.open(requested_path.generic_string().c_str(), beast::file_mode::read, ec); 88 | 89 | FileResponse file_response; 90 | 91 | file_response.insert(http::field::content_type, ExtesionToContentType(requested_path.extension().generic_string())); 92 | file_response.body() = std::move(file); 93 | file_response.prepare_payload(); 94 | 95 | return send(file_response); 96 | } 97 | 98 | private: 99 | StringResponse GetAllMaps(unsigned int http_version, bool keep_alive); 100 | StringResponse GetMapById(std::string_view id, unsigned int http_version, bool keep_alive); 101 | std::string PercentDecode(const std::string& uri) const; 102 | std::string_view ExtesionToContentType(const std::string& extension) const; 103 | bool IsSubPath(fs::path path, fs::path base) const; 104 | 105 | fs::path root_path_; 106 | std::shared_ptr api_request_handler_; 107 | application::Application& app_; 108 | }; 109 | 110 | class DurationMeasure { 111 | public: 112 | DurationMeasure() = default; 113 | 114 | int GetDurationInMilliseconds() { 115 | std::chrono::system_clock::time_point end_ts = std::chrono::system_clock::now(); 116 | auto duration = std::chrono::duration_cast(end_ts - start_ts_); 117 | return duration.count(); 118 | } 119 | 120 | private: 121 | std::chrono::system_clock::time_point start_ts_ = std::chrono::system_clock::now(); 122 | }; 123 | 124 | template 125 | class LoggingRequestHandler { 126 | public: 127 | LoggingRequestHandler(SomeRequestHandler& decorated) 128 | : decorated_(decorated) { 129 | } 130 | 131 | template 132 | static void LogRequest(const Request& req, const std::string& ip) { 133 | 134 | json::object request_data; 135 | request_data["ip"s] = ip; 136 | request_data["URI"s] = std::string(req.target()); 137 | request_data["method"s] = std::string(req.method_string()); 138 | 139 | BOOST_LOG_TRIVIAL(info) << logging::add_value(additional_data, request_data) 140 | << "request received"sv; 141 | } 142 | 143 | template 144 | static void LogResponse(const Response& res, const std::string& ip, const int duration) { 145 | 146 | json::object response_data; 147 | response_data["ip"s] = ip; 148 | response_data["response_time"s] = duration; 149 | response_data["code"s] = res.result_int(); 150 | 151 | auto ct_it = res.find(http::field::content_type); 152 | 153 | response_data["content_type"s] = ct_it != res.end() ? std::string(ct_it->value()) : std::string("null"); 154 | 155 | BOOST_LOG_TRIVIAL(info) << logging::add_value(additional_data, response_data) 156 | << "response sent"sv; 157 | } 158 | template 159 | void operator()(http::request>&& req, Send&& send, const std::string& ip) { 160 | 161 | LogRequest(req, ip); 162 | 163 | auto timer = std::make_shared(); 164 | decorated_(std::forward(req), [send_ = std::forward(send), ip, timer](auto&& resp) 165 | { 166 | auto duration = timer->GetDurationInMilliseconds(); 167 | LogResponse(resp, ip, duration); 168 | send_(resp); 169 | }); 170 | } 171 | 172 | private: 173 | SomeRequestHandler& decorated_; 174 | }; 175 | 176 | } // namespace http_handler 177 | -------------------------------------------------------------------------------- /src/json_loader.cpp: -------------------------------------------------------------------------------- 1 | #include "json_loader.h" 2 | #include "model_properties.h" 3 | 4 | #include 5 | #include 6 | 7 | #include 8 | #include 9 | 10 | namespace json_loader { 11 | 12 | namespace sys = boost::system; 13 | namespace json = boost::json; 14 | 15 | using namespace std::literals; 16 | 17 | json::value LoadJsonFromFile( std::istream& is) { 18 | 19 | json::stream_parser stream_parser; 20 | std::string line; 21 | sys::error_code ec; 22 | 23 | while (std::getline( is, line )) 24 | { 25 | stream_parser.write(line, ec); 26 | 27 | if (ec) 28 | return nullptr; 29 | } 30 | 31 | stream_parser.finish(ec); 32 | 33 | if (ec) 34 | return nullptr; 35 | 36 | return stream_parser.release(); 37 | } 38 | 39 | void LoadRoads(model::Map& model_map, const json::value& map_item) { 40 | 41 | for (const auto& roadItem : map_item.at(Properties::ROADS_ARRAY).as_array()) 42 | { 43 | auto x0 = static_cast(roadItem.at(Properties::ROAD_POINT_X).as_int64()); 44 | auto y0 = static_cast(roadItem.at(Properties::ROAD_POINT_Y).as_int64()); 45 | 46 | if (auto x1p = roadItem.as_object().if_contains(Properties::ROAD_HORIZONTAL)) 47 | { 48 | auto x1 = static_cast(x1p->as_int64()); 49 | model_map.AddRoad(model::Road{model::Road::HORIZONTAL, {x0, y0}, x1}); 50 | } 51 | else 52 | { 53 | auto y1 = static_cast(roadItem.at(Properties::ROAD_VERTICAL).as_int64()); 54 | model_map.AddRoad(model::Road{model::Road::VERTICAL, {x0, y0}, y1}); 55 | } 56 | } 57 | } 58 | 59 | void LoadBuildings(model::Map& model_map, const json::value& map_item) { 60 | 61 | for (const auto& build_item : map_item.at(Properties::BUILDINGS_ARRAY).as_array()) 62 | { 63 | auto x = static_cast(build_item.at(Properties::BUILDING_POINT_X).as_int64()); 64 | auto y = static_cast(build_item.at(Properties::BUILDING_POINT_Y).as_int64()); 65 | auto w = static_cast(build_item.at(Properties::BUILDING_WEIGHT).as_int64()); 66 | auto h = static_cast(build_item.at(Properties::BUILDING_HEIGHT).as_int64()); 67 | 68 | model_map.AddBuilding(model::Building({{x, y}, {w, h}})); 69 | } 70 | } 71 | 72 | void LoadOffices(model::Map& model_map, const json::value& map_item) { 73 | 74 | for (const auto& office_item : map_item.at(Properties::OFFICES_ARRAY).as_array()) 75 | { 76 | std::string id = office_item.at(Properties::OFFICE_ID).as_string().c_str(); 77 | auto x = static_cast(office_item.at(Properties::OFFICE_POINT_X).as_int64()); 78 | auto y = static_cast(office_item.at(Properties::OFFICE_POINT_Y).as_int64()); 79 | auto offX = static_cast(office_item.at(Properties::OFFICE_OFFSET_X).as_int64()); 80 | auto offY = static_cast(office_item.at(Properties::OFFICE_OFFSET_Y).as_int64()); 81 | 82 | model_map.AddOffice(model::Office(model::Office::Id(id),{x, y}, {offX, offY})); 83 | } 84 | } 85 | 86 | void LoadLootTypes(model::Map& model_map, const json::value& map_item) { 87 | 88 | for (const auto& loot_item : map_item.at(Properties::LOOT_TYPES_ARRAY).as_array()) 89 | { 90 | auto name = loot_item.at(Properties::LOOT_NAME).as_string().c_str(); 91 | auto file = loot_item.at(Properties::LOOT_FILE).as_string().c_str(); 92 | auto type = loot_item.at(Properties::LOOT_TYPE).as_string().c_str(); 93 | 94 | std::optional rotation; 95 | if (loot_item.as_object().contains(Properties::LOOT_ROTATION)) 96 | rotation = static_cast(loot_item.at(Properties::LOOT_ROTATION).as_int64()); 97 | 98 | std::optional color; 99 | if (loot_item.as_object().contains(Properties::LOOT_COLOR)) 100 | color = loot_item.at(Properties::LOOT_COLOR).as_string().c_str(); 101 | 102 | auto scale = static_cast(loot_item.at(Properties::LOOT_SCALE).as_double()); 103 | auto value = loot_item.at(Properties::LOOT_VALUE).as_int64(); 104 | 105 | 106 | model_map.AddLootType({name, file, type, rotation, color, scale, value}); 107 | } 108 | } 109 | 110 | model::Game LoadGame(const std::filesystem::path& json_path) { 111 | // Загрузить содержимое файла json_path, например, в виде строки 112 | // Распарсить строку как JSON, используя boost::json::parse 113 | std::ifstream file(json_path); 114 | if (!file) 115 | throw std::invalid_argument{ json_path.string()}; 116 | 117 | json::value json = LoadJsonFromFile(file); 118 | 119 | if (json.is_null()) 120 | throw std::runtime_error("Can't parse config file"); 121 | 122 | // Загрузить модель игры из файла 123 | model::Game game; 124 | 125 | if (json.as_object().contains(Properties::DEFAULT_DOG_SPEED)) { 126 | 127 | game.SetDefaultDogSpeed(static_cast(json.at(Properties::DEFAULT_DOG_SPEED).as_double())); 128 | } 129 | 130 | if (json.as_object().contains(Properties::LOOT_GENERATOR_CONFIG)) { 131 | 132 | auto config = json.at(Properties::LOOT_GENERATOR_CONFIG).as_object(); 133 | auto period = static_cast(config.at(Properties::LOOT_GENERATOR_PERIOD).as_double()); 134 | auto probability = static_cast(config.at(Properties::LOOT_GENERATOR_PROBABILITY).as_double()); 135 | 136 | game.SetLootGeneratorConfig({period, probability}); 137 | } 138 | 139 | if (json.as_object().contains(Properties::DEFAULT_BAG_CAPACITY)) { 140 | 141 | game.SetDefaultBagCapacity(static_cast(json.at(Properties::DEFAULT_BAG_CAPACITY).as_int64())); 142 | } 143 | 144 | if (json.as_object().contains(Properties::DOG_RETIREMENT_TIME)) { 145 | 146 | constexpr double ONE_SECOND = 1000.0; 147 | game.SetDogRetirementTime(static_cast(json.at(Properties::DOG_RETIREMENT_TIME).as_double() * ONE_SECOND)); 148 | } 149 | 150 | for (const auto& map_item : json.at(Properties::MAPS_ARRAY).as_array()) 151 | { 152 | std::string id = map_item.at(Properties::MAP_ID).as_string().c_str(); 153 | std::string name = map_item.at(Properties::MAP_NAME).as_string().c_str(); 154 | 155 | model::Map model_map(model::Map::Id(id), name); 156 | 157 | if (map_item.as_object().contains(Properties::DOG_SPEED)) 158 | { 159 | model_map.AddDogSpeed(static_cast(map_item.at(Properties::DOG_SPEED).as_double())); 160 | } 161 | 162 | if (json.as_object().contains(Properties::BAG_CAPACITY)) 163 | { 164 | model_map.SetBagCapacity(static_cast(json.at(Properties::BAG_CAPACITY).as_int64())); 165 | } 166 | 167 | LoadRoads(model_map, map_item); 168 | LoadBuildings(model_map, map_item); 169 | LoadOffices(model_map, map_item); 170 | LoadLootTypes(model_map, map_item); 171 | 172 | game.AddMap(model_map); 173 | } 174 | 175 | return game; 176 | } 177 | 178 | } // namespace json_loader 179 | -------------------------------------------------------------------------------- /src/request_handler/api_request_handler.cpp: -------------------------------------------------------------------------------- 1 | #include "api_request_handler.h" 2 | #include "model_properties.h" 3 | 4 | #include 5 | 6 | #include 7 | #include 8 | 9 | namespace http_handler { 10 | 11 | namespace json = boost::json; 12 | 13 | 14 | StringResponse APIRequestHandler::JoinToGame(const StringRequest& req) 15 | { 16 | if (req.method() != http::verb::post) 17 | return MakeNotAlowedResponse("Only POST method is expected"sv, "POST"sv, req.version(), req.keep_alive()); 18 | 19 | std::string user_name; 20 | std::string map_id; 21 | 22 | try { 23 | auto body_json = boost::json::parse(req.body()); 24 | user_name = body_json.at(Properties::JOIN_USER_NAME).as_string(); 25 | map_id = body_json.at(Properties::JOIN_MAP_ID).as_string(); 26 | 27 | } catch ([[maybe_unused]] const std::exception& e) { 28 | return MakeBadRequest("invalidArgument"sv, "Join game request parse error"sv, req.version(), req.keep_alive()); 29 | } 30 | 31 | if (user_name.empty()) 32 | return MakeBadRequest("invalidArgument"sv, "Invalid name"sv, req.version(), req.keep_alive()); 33 | 34 | if (!app_.FindMap(map_id)) 35 | return MakeNotFoundResponse("mapNotFound"sv, "Map not found"sv, req.version(), req.keep_alive()); 36 | 37 | auto [authToken, playerId] = app_.JoinToGame(user_name, map_id); 38 | 39 | json::object response; 40 | response[Properties::AUTH_TOKEN] = authToken; 41 | response[Properties::PLAYER_ID] = playerId; 42 | 43 | return MakeStringResponse(http::status::ok, json::serialize(response), req.version(), req.keep_alive()); 44 | } 45 | 46 | StringResponse APIRequestHandler::GetPlayers(const StringRequest &req, const std::string_view token) 47 | { 48 | if (req.method() != http::verb::get && req.method() != http::verb::head) 49 | return MakeNotAlowedResponse("Invalid method"sv, "GET, HEAD"sv, req.version(), req.keep_alive()); 50 | 51 | json::object response; 52 | 53 | for (auto& player : app_.GetAllPlayers()) { 54 | json::object player_json; 55 | 56 | player_json[Properties::USER_NAME] = player.GetDog()->GetName(); 57 | std::string id_key = std::to_string(*player.GetDog()->GetDogId()); 58 | 59 | response[id_key] = player_json; 60 | } 61 | 62 | return MakeStringResponse(http::status::ok, json::serialize(response), req.version(), req.keep_alive()); 63 | } 64 | 65 | StringResponse APIRequestHandler::GetState(const StringRequest& req, const std::string_view token) 66 | { 67 | if (req.method() != http::verb::get && req.method() != http::verb::head) 68 | return MakeNotAlowedResponse("Invalid method"sv, "GET, HEAD"sv, req.version(), req.keep_alive()); 69 | 70 | json::object response; 71 | json::object players; 72 | 73 | auto game_state = app_.GetState(token); 74 | 75 | for (auto& palyer_state : game_state.players_state_) { 76 | 77 | json::object player_json; 78 | 79 | player_json[Properties::PLAYER_POSITION] = json::array{ palyer_state.position_x_, palyer_state.position_y_ }; 80 | player_json[Properties::PLAYER_SPEED] = json::array{ palyer_state.horizontal_speed_, palyer_state.vertical_speed_ }; 81 | 82 | player_json[Properties::PLAYER_DIRECTION] = palyer_state.dog_direction_; 83 | 84 | auto json_bag = json::array{}; 85 | 86 | for (const auto& objects_in_bag : palyer_state.bag_) { 87 | json::object object_in_bag; 88 | 89 | object_in_bag[Properties::PLAYER_BAG_OBJ_ID] = objects_in_bag.id; 90 | object_in_bag[Properties::PLAYER_BAG_OBJ_TYPE] = objects_in_bag.type; 91 | 92 | json_bag.push_back(object_in_bag); 93 | } 94 | 95 | player_json[Properties::PLAYER_BAG] = json_bag; 96 | 97 | player_json[Properties::PLAYER_SCORE] = palyer_state.score_; 98 | 99 | players[palyer_state.dog_id_] = player_json; 100 | } 101 | 102 | response[Properties::PLAYERS_RESPONSE] = players; 103 | 104 | if (!game_state.loots_state_.empty()) { 105 | 106 | json::object lost_objects; 107 | 108 | for (size_t i = 0; i < game_state.loots_state_.size(); ++i) { 109 | json::object lost_object; 110 | 111 | lost_object[Properties::PLAYER_LOST_OBJECT_TYPE] = game_state.loots_state_[i].type; 112 | lost_object[Properties::PLAYER_LOST_OBJECT_POS] = json::array{ game_state.loots_state_[i].position.x, game_state.loots_state_[i].position.y }; 113 | 114 | lost_objects[std::to_string(i)] = lost_object; 115 | } 116 | 117 | response[Properties::PLAYER_LOST_OBJECTS] = lost_objects; 118 | } 119 | 120 | return MakeStringResponse(http::status::ok, PrettySerialize(response), req.version(), req.keep_alive()); 121 | } 122 | 123 | StringResponse APIRequestHandler::Action(const StringRequest& req, const std::string_view token) 124 | { 125 | if (req.method() != http::verb::post) 126 | return MakeNotAlowedResponse("Invalid method"sv, "POST"sv, req.version(), req.keep_alive()); 127 | 128 | std::string direction; 129 | 130 | auto body_json = boost::json::parse(req.body()); 131 | direction = body_json.at(Properties::MOVE_ACTION).as_string(); 132 | 133 | if (direction.empty()) { 134 | app_.Move(token, 'S'); 135 | return MakeStringResponse(http::status::ok, json::serialize(json::object{}), req.version(), req.keep_alive()); 136 | } 137 | 138 | static const std::regex MOVE_PATTERN("[LRUD]"); 139 | if (!std::regex_match(direction, MOVE_PATTERN)) { 140 | return MakeBadRequest("invalidArgument"sv, "Failed to parse action"sv, req.version(), req.keep_alive()); 141 | } 142 | 143 | app_.Move(token, direction[0]); 144 | 145 | return MakeStringResponse(http::status::ok, json::serialize(json::object{}), req.version(), req.keep_alive()); 146 | } 147 | 148 | StringResponse APIRequestHandler::Tick(const StringRequest& req) { 149 | if (req.method() != http::verb::post) 150 | return MakeNotAlowedResponse("Invalid method"sv, "POST"sv, req.version(), req.keep_alive()); 151 | 152 | std::int64_t time_delta = 0; 153 | 154 | try { 155 | auto body_json = boost::json::parse(req.body()); 156 | time_delta = body_json.at(Properties::TIME_DELTA).as_int64(); 157 | 158 | if (time_delta <= 0) 159 | throw std::exception {}; 160 | 161 | } catch ([[maybe_unused]] const std::exception& e) { 162 | return MakeBadRequest("invalidArgument"sv, "Failed to parse tick request JSON"sv, req.version(), req.keep_alive()); 163 | } 164 | 165 | app_.UpdateGameState(std::chrono::milliseconds {time_delta}); 166 | 167 | return MakeStringResponse(http::status::ok, json::serialize(json::object{}), req.version(), req.keep_alive()); 168 | } 169 | 170 | StringResponse APIRequestHandler::GetRecords(const StringRequest &req, std::optional start, std::optional maxItems) { 171 | 172 | auto records = app_.GetRecordsInfo(start, maxItems); 173 | auto response = json::array{}; 174 | 175 | for (auto& [name, score, play_time] : records) { 176 | json::object record_json; 177 | record_json["name"] = name; 178 | record_json["score"] = score; 179 | record_json["playTime"] = std::round(play_time / 1000.0); 180 | 181 | response.push_back(record_json); 182 | } 183 | 184 | return MakeStringResponse(http::status::ok, PrettySerialize(response), req.version(), req.keep_alive()); 185 | } 186 | 187 | } 188 | -------------------------------------------------------------------------------- /src/request_handler/request_handler.cpp: -------------------------------------------------------------------------------- 1 | #include "request_handler.h" 2 | #include "model_properties.h" 3 | 4 | namespace http_handler { 5 | 6 | StringResponse RequestHandler::GetAllMaps(unsigned int http_version, bool keep_alive) 7 | { 8 | json::array response; 9 | 10 | for (const auto& map : app_.GetMaps()) 11 | { 12 | json::object map_item; 13 | map_item[Properties::MAP_ID] = *map.GetId(); 14 | map_item[Properties::MAP_NAME] = map.GetName(); 15 | 16 | response.push_back(map_item); 17 | } 18 | 19 | return MakeStringResponse(http::status::ok, PrettySerialize(response), http_version, keep_alive); 20 | } 21 | 22 | void FillRoads(const model::Map& model_map, json::object& map_json) { 23 | json::array roads_json_arr; 24 | for (auto& road : model_map.GetRoads()) 25 | { 26 | json::object road_json; 27 | 28 | road_json[Properties::ROAD_POINT_X] = road.GetStart().x; 29 | road_json[Properties::ROAD_POINT_Y] = road.GetStart().y; 30 | 31 | std::string direction_key = road.IsHorizontal() ? Properties::ROAD_HORIZONTAL : Properties::ROAD_VERTICAL; 32 | 33 | road_json[direction_key] = road.IsHorizontal() ? road.GetEnd().x : road.GetEnd().y; 34 | 35 | roads_json_arr.push_back(road_json); 36 | } 37 | 38 | map_json[Properties::ROADS_ARRAY] = roads_json_arr; 39 | } 40 | 41 | void FillBuildings(const model::Map& model_map, json::object& map_json) { 42 | json::array buildings_json_arr; 43 | for (auto& building : model_map.GetBuildings()) 44 | { 45 | json::object building_json; 46 | 47 | building_json[Properties::BUILDING_POINT_X] = building.GetBounds().position.x; 48 | building_json[Properties::BUILDING_POINT_Y] = building.GetBounds().position.y; 49 | building_json[Properties::BUILDING_WEIGHT] = building.GetBounds().size.width; 50 | building_json[Properties::BUILDING_HEIGHT] = building.GetBounds().size.height; 51 | 52 | buildings_json_arr.push_back(building_json); 53 | } 54 | map_json[Properties::BUILDINGS_ARRAY] = buildings_json_arr; 55 | } 56 | 57 | void FillOffices(const model::Map& model_map, json::object& map_json) { 58 | json::array offices_json_arr; 59 | for (auto& office : model_map.GetOffices()) 60 | { 61 | json::object office_json; 62 | 63 | office_json[Properties::OFFICE_ID] = *office.GetId(); 64 | office_json[Properties::OFFICE_POINT_X] = office.GetPosition().x; 65 | office_json[Properties::OFFICE_POINT_Y] = office.GetPosition().y; 66 | office_json[Properties::OFFICE_OFFSET_X] = office.GetOffset().dx; 67 | office_json[Properties::OFFICE_OFFSET_Y] = office.GetOffset().dy; 68 | 69 | offices_json_arr.push_back(office_json); 70 | } 71 | map_json[Properties::OFFICES_ARRAY] = offices_json_arr; 72 | } 73 | 74 | void FillLootTypes(const model::Map& model_map, json::object& map_json) { 75 | json::array lootTypes_json_arr; 76 | for (auto& lootType : model_map.GetLootTypes()) 77 | { 78 | json::object lootType_json; 79 | lootType_json[Properties::LOOT_NAME] = lootType.name_; 80 | lootType_json[Properties::LOOT_FILE] = lootType.file_; 81 | lootType_json[Properties::LOOT_TYPE] = lootType.type_; 82 | if (lootType.rotation_) 83 | lootType_json[Properties::LOOT_ROTATION] = *lootType.rotation_; 84 | 85 | if (lootType.color_) 86 | lootType_json[Properties::LOOT_COLOR] = *lootType.color_; 87 | 88 | lootType_json[Properties::LOOT_SCALE] = lootType.scale_; 89 | lootType_json[Properties::LOOT_VALUE] = lootType.value_; 90 | 91 | lootTypes_json_arr.push_back(lootType_json); 92 | } 93 | 94 | map_json[Properties::LOOT_TYPES_ARRAY] = lootTypes_json_arr; 95 | } 96 | 97 | StringResponse RequestHandler::GetMapById(std::string_view id, unsigned int http_version, bool keep_alive) { 98 | 99 | const auto* model_map_p = app_.FindMap(id); 100 | 101 | if (!model_map_p) 102 | return MakeNotFoundResponse("mapNotFound"sv, "Map not found"sv, http_version, keep_alive); 103 | 104 | json::object map_json; 105 | 106 | map_json[Properties::MAP_ID] = *model_map_p->GetId(); 107 | map_json[Properties::MAP_NAME] = model_map_p->GetName(); 108 | 109 | FillRoads(*model_map_p, map_json); 110 | FillBuildings(*model_map_p, map_json); 111 | FillOffices(*model_map_p, map_json); 112 | FillLootTypes(*model_map_p, map_json); 113 | 114 | return MakeStringResponse(http::status::ok, PrettySerialize(map_json), http_version, keep_alive); 115 | } 116 | 117 | std::string RequestHandler::PercentDecode(const std::string& uri) const { 118 | 119 | std::stringstream ss; 120 | 121 | for (auto cur_it = uri.begin(); cur_it != uri.end(); ++cur_it) 122 | { 123 | if (*cur_it != '%') 124 | { 125 | ss << *cur_it; 126 | continue; 127 | } 128 | 129 | // Колчество символов после '%' должно быть не меньше 2 130 | static constexpr uint8_t HEX_LEN = 2; 131 | if (std::distance(cur_it, uri.end()) < HEX_LEN) 132 | return ""; 133 | 134 | try { 135 | std::string hex(1, *(++cur_it)); 136 | hex += *(++cur_it); 137 | 138 | int decoded_char = std::stoi(hex, nullptr, 16); 139 | 140 | // Проверяем на вхождение в ASCII диапозон 141 | if (decoded_char < 0 && decoded_char > 255) 142 | return {}; 143 | 144 | ss << static_cast(decoded_char); 145 | 146 | } catch ([[maybe_unused]] const std::invalid_argument& e) { 147 | return {}; 148 | } 149 | } 150 | 151 | return ss.str(); 152 | } 153 | 154 | std::string_view RequestHandler::ExtesionToContentType(const std::string& extension) const { 155 | 156 | static const std::map extensionMap { 157 | { ".html", ContentType::TEXT_HTML }, 158 | { ".htm", ContentType::TEXT_HTML }, 159 | { ".css", ContentType::TEXT_CSS }, 160 | { ".txt", ContentType::TEXT_PLAIN }, 161 | { ".js", ContentType::TEXT_JAVASCRIPT }, 162 | { ".json", ContentType::APPLICATION_JSON }, 163 | { ".xml", ContentType::APPLICATION_XML }, 164 | { ".png", ContentType::IMAGE_PNG }, 165 | { ".jpeg", ContentType::IMAGE_JPEG }, 166 | { ".jpg", ContentType::IMAGE_JPEG }, 167 | { ".jpe", ContentType::IMAGE_JPEG }, 168 | { ".bmp", ContentType::IMAGE_BMP }, 169 | { ".gif", ContentType::IMAGE_GIF }, 170 | { ".ico", ContentType::IMAGE_ICO }, 171 | { ".tiff", ContentType::IMAGE_TIFF }, 172 | { ".tif", ContentType::IMAGE_TIFF }, 173 | { ".svg", ContentType::IMAGE_SVG }, 174 | { ".svgz", ContentType::IMAGE_SVG }, 175 | { ".mp3", ContentType::AUDIO_MP3 } 176 | }; 177 | 178 | auto it = extensionMap.find(extension); 179 | if (it != extensionMap.end()) { 180 | return it->second; 181 | } 182 | 183 | return ContentType::APPLICATION_OCTET_STREAM; 184 | } 185 | 186 | bool RequestHandler::IsSubPath(fs::path path, fs::path base) const { 187 | // Приводим оба пути к каноничному виду (без . и ..) 188 | path = fs::weakly_canonical(path); 189 | base = fs::weakly_canonical(base); 190 | 191 | // Проверяем, что все компоненты base содержатся внутри path 192 | for (auto b = base.begin(), p = path.begin(); b != base.end(); ++b, ++p) { 193 | if (p == path.end() || *p != *b) { 194 | return false; 195 | } 196 | } 197 | return true; 198 | } 199 | 200 | 201 | } // namespace http_handler 202 | -------------------------------------------------------------------------------- /src/http_server/http_server.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "sdk.h" 3 | 4 | #define BOOST_BEAST_USE_STD_STRING_VIEW 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #include "../logger_helper.h" 12 | 13 | namespace http_server { 14 | 15 | namespace net = boost::asio; 16 | using tcp = net::ip::tcp; 17 | namespace beast = boost::beast; 18 | namespace http = beast::http; 19 | 20 | using namespace std::literals; 21 | 22 | 23 | class SessionBase { 24 | public: 25 | // Запрещаем копирование и присваивание объектов SessionBase и его наследников 26 | SessionBase(const SessionBase&) = delete; 27 | SessionBase& operator=(const SessionBase&) = delete; 28 | 29 | void Run(); 30 | 31 | protected: 32 | explicit SessionBase(tcp::socket&& socket); 33 | ~SessionBase() = default; 34 | 35 | using HttpRequest = http::request; 36 | 37 | template 38 | void Write(http::response&& response) { 39 | // Запись выполняется асинхронно, поэтому response перемещаем в область кучи 40 | auto safe_response = std::make_shared>(std::move(response)); 41 | 42 | auto self = GetSharedThis(); 43 | http::async_write(stream_, *safe_response, 44 | [safe_response, self](beast::error_code ec, std::size_t bytes_written) { 45 | self->OnWrite(safe_response->need_eof(), ec, bytes_written); 46 | }); 47 | } 48 | 49 | private: 50 | void Read(); 51 | void OnRead(beast::error_code ec, [[maybe_unused]] std::size_t bytes_read); 52 | void OnWrite(bool close, beast::error_code ec, [[maybe_unused]] std::size_t bytes_written); 53 | void Close(); 54 | 55 | // Обработку запроса делегируем подклассу 56 | virtual void HandleRequest(HttpRequest&& request) = 0; 57 | 58 | virtual std::shared_ptr GetSharedThis() = 0; 59 | 60 | private: 61 | // tcp_stream содержит внутри себя сокет и добавляет поддержку таймаутов 62 | beast::tcp_stream stream_; 63 | beast::flat_buffer buffer_; 64 | HttpRequest request_; 65 | }; 66 | 67 | template 68 | class Session : public SessionBase, public std::enable_shared_from_this> { 69 | 70 | public: 71 | template 72 | Session(tcp::socket&& socket, Handler&& request_handler, const std::string& remote_ip) 73 | : SessionBase(std::move(socket)) 74 | , request_handler_(std::forward(request_handler)) 75 | , remote_ip_(remote_ip) { 76 | } 77 | 78 | private: 79 | void HandleRequest(HttpRequest&& request) override { 80 | // Захватываем умный указатель на текущий объект Session в лямбде, 81 | // чтобы продлить время жизни сессии до вызова лямбды. 82 | // Используется generic-лямбда функция, способная принять response произвольного типа 83 | 84 | request_handler_(std::move(request), [self = this->shared_from_this()](auto&& response) { 85 | self->Write(std::move(response)); 86 | }, remote_ip_); 87 | } 88 | 89 | std::shared_ptr GetSharedThis() override { 90 | return this->shared_from_this(); 91 | } 92 | 93 | private: 94 | RequestHandler request_handler_; 95 | std::string remote_ip_; 96 | }; 97 | 98 | template 99 | class Listener : public std::enable_shared_from_this> { 100 | public: 101 | template 102 | Listener(net::io_context& ioc, const tcp::endpoint& endpoint, Handler&& request_handler) 103 | : ioc_(ioc) 104 | // Обработчики асинхронных операций acceptor_ будут вызываться в своём strand 105 | , acceptor_(net::make_strand(ioc)) 106 | , request_handler_(std::forward(request_handler)) { 107 | // Открываем acceptor, используя протокол (IPv4 или IPv6), указанный в endpoint 108 | acceptor_.open(endpoint.protocol()); 109 | 110 | // После закрытия TCP-соединения сокет некоторое время может считаться занятым, 111 | // чтобы компьютеры могли обменяться завершающими пакетами данных. 112 | // Однако это может помешать повторно открыть сокет в полузакрытом состоянии. 113 | // Флаг reuse_address разрешает открыть сокет, когда он "наполовину закрыт" 114 | acceptor_.set_option(net::socket_base::reuse_address(true)); 115 | // Привязываем acceptor к адресу и порту endpoint 116 | acceptor_.bind(endpoint); 117 | // Переводим acceptor в состояние, в котором он способен принимать новые соединения 118 | // Благодаря этому новые подключения будут помещаться в очередь ожидающих соединений 119 | acceptor_.listen(net::socket_base::max_listen_connections); 120 | } 121 | 122 | void Run() { 123 | DoAccept(); 124 | } 125 | 126 | private: 127 | void DoAccept() { 128 | acceptor_.async_accept( 129 | // Передаём последовательный исполнитель, в котором будут вызываться обработчики 130 | // асинхронных операций сокета 131 | net::make_strand(ioc_), 132 | // С помощью bind_front_handler создаём обработчик, привязанный к методу OnAccept 133 | // текущего объекта. 134 | // Так как Listener — шаблонный класс, нужно подсказать компилятору, что 135 | // shared_from_this — метод класса, а не свободная функция. 136 | // Для этого вызываем его, используя this 137 | // Этот вызов bind_front_handler аналогичен 138 | // namespace ph = std::placeholders; 139 | // std::bind(&Listener::OnAccept, this->shared_from_this(), ph::_1, ph::_2) 140 | beast::bind_front_handler(&Listener::OnAccept, this->shared_from_this())); 141 | } 142 | 143 | // Метод socket::async_accept создаст сокет и передаст его передан в OnAccept 144 | void OnAccept(beast::error_code ec, tcp::socket socket) { 145 | using namespace std::literals; 146 | 147 | if (ec) { 148 | json::object accept_error; 149 | accept_error["code"s] = ec.value(); 150 | accept_error["text"s] = ec.message(); 151 | accept_error["where"s] = "accept"; 152 | BOOST_LOG_TRIVIAL(info) << logging::add_value(additional_data, accept_error) 153 | << "error"sv; 154 | return; 155 | } 156 | 157 | // Асинхронно обрабатываем сессию 158 | AsyncRunSession(std::move(socket)); 159 | 160 | // Принимаем новое соединение 161 | DoAccept(); 162 | } 163 | 164 | void AsyncRunSession(tcp::socket&& socket) { 165 | 166 | boost::system::error_code ec_endpoint; 167 | auto remote_endpoint = socket.remote_endpoint(ec_endpoint); 168 | 169 | std::string remote_ip; 170 | if (!ec_endpoint) 171 | remote_ip = remote_endpoint.address().to_string(); 172 | 173 | std::make_shared>(std::move(socket), request_handler_, remote_ip)->Run(); 174 | } 175 | 176 | private: 177 | net::io_context& ioc_; 178 | tcp::acceptor acceptor_; 179 | RequestHandler request_handler_; 180 | }; 181 | 182 | template 183 | void ServeHttp(net::io_context& ioc, const tcp::endpoint& endpoint, RequestHandler&& handler) { 184 | // При помощи decay_t исключим ссылки из типа RequestHandler, 185 | // чтобы Listener хранил RequestHandler по значению 186 | using MyListener = Listener>; 187 | 188 | std::make_shared(ioc, endpoint, std::forward(handler))->Run(); 189 | } 190 | 191 | // Разместите здесь реализацию http-сервера, взяв её из задания по разработке асинхронного сервера 192 | 193 | } // namespace http_server 194 | -------------------------------------------------------------------------------- /data/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultDogSpeed": 3.0, 3 | "lootGeneratorConfig": { 4 | "period": 5.0, 5 | "probability": 0.5 6 | }, 7 | "dogRetirementTime": 20.0, 8 | "maps": [ 9 | { 10 | "dogSpeed": 4.0, 11 | "id": "map1", 12 | "name": "Map 1", 13 | "lootTypes": [ 14 | { 15 | "name": "key", 16 | "file": "assets/key.obj", 17 | "type": "obj", 18 | "rotation": 90, 19 | "color" : "#338844", 20 | "scale": 0.03, 21 | "value": 10 22 | }, 23 | { 24 | "name": "wallet", 25 | "file": "assets/wallet.obj", 26 | "type": "obj", 27 | "rotation": 0, 28 | "color" : "#883344", 29 | "scale": 0.01, 30 | "value": 30 31 | } 32 | ], 33 | "roads": [ 34 | { 35 | "x0": 0, 36 | "y0": 0, 37 | "x1": 40 38 | }, 39 | { 40 | "x0": 40, 41 | "y0": 0, 42 | "y1": 30 43 | }, 44 | { 45 | "x0": 40, 46 | "y0": 30, 47 | "x1": 0 48 | }, 49 | { 50 | "x0": 0, 51 | "y0": 0, 52 | "y1": 30 53 | } 54 | ], 55 | "buildings": [ 56 | { 57 | "x": 5, 58 | "y": 5, 59 | "w": 30, 60 | "h": 20 61 | } 62 | ], 63 | "offices": [ 64 | { 65 | "id": "o0", 66 | "x": 40, 67 | "y": 30, 68 | "offsetX": 5, 69 | "offsetY": 0 70 | } 71 | ] 72 | }, 73 | { 74 | "id": "town", 75 | "name": "Town", 76 | "lootTypes": [ 77 | { 78 | "name": "key", 79 | "file": "assets/key.obj", 80 | "type": "obj", 81 | "rotation": 90, 82 | "color": "#338844", 83 | "scale": 0.03, 84 | "value": 10 85 | }, 86 | { 87 | "name": "key", 88 | "file": "assets/key.obj", 89 | "type": "obj", 90 | "rotation": 90, 91 | "color": "#338844", 92 | "scale": 0.03, 93 | "value": 10 94 | } 95 | ], 96 | "roads": [ 97 | { 98 | "x0": 0, 99 | "y0": 0, 100 | "x1": 40 101 | }, 102 | { 103 | "x0": 40, 104 | "y0": 0, 105 | "y1": 30 106 | }, 107 | { 108 | "x0": 40, 109 | "y0": 30, 110 | "x1": 0 111 | }, 112 | { 113 | "x0": 0, 114 | "y0": 15, 115 | "x1": 40 116 | }, 117 | { 118 | "x0": 20, 119 | "y0": 0, 120 | "y1": 30 121 | }, 122 | { 123 | "x0": 0, 124 | "y0": 22, 125 | "x1": 17 126 | }, 127 | { 128 | "x0": 17, 129 | "y0": 18, 130 | "y1": 27 131 | }, 132 | { 133 | "x0": 10, 134 | "y0": 22, 135 | "y1": 30 136 | }, 137 | { 138 | "x0": 0, 139 | "y0": 10, 140 | "x1": 10 141 | }, 142 | { 143 | "x0": 10, 144 | "y0": 10, 145 | "y1": 5 146 | }, 147 | { 148 | "x0": 10, 149 | "y0": 5, 150 | "x1": 20 151 | }, 152 | { 153 | "x0": 20, 154 | "y0": 30, 155 | "y1": 40 156 | }, 157 | { 158 | "x0": 20, 159 | "y0": 40, 160 | "x1": 10 161 | }, 162 | { 163 | "x0": 20, 164 | "y0": 40, 165 | "x1": 30 166 | }, 167 | { 168 | "x0": 30, 169 | "y0": 0, 170 | "y1": 10 171 | }, 172 | { 173 | "x0": 20, 174 | "y0": 25, 175 | "x1": 25 176 | }, 177 | { 178 | "x0": 30, 179 | "y0": 25, 180 | "y1": 30 181 | }, 182 | { 183 | "x0": 20, 184 | "y0": 20, 185 | "x1": 25 186 | }, 187 | { 188 | "x0": 30, 189 | "y0": 15, 190 | "y1": 20 191 | }, 192 | { 193 | "x0": 35, 194 | "y0": 20, 195 | "x1": 40 196 | }, 197 | { 198 | "x0": 40, 199 | "y0": 25, 200 | "x1": 35 201 | }, 202 | { 203 | "x0": 30, 204 | "y0": 30, 205 | "y1": 35 206 | } 207 | ], 208 | "buildings": [ 209 | { 210 | "x": 2, 211 | "y": 2, 212 | "w": 6, 213 | "h": 6 214 | }, 215 | { 216 | "x": 12, 217 | "y": 7, 218 | "w": 6, 219 | "h": 6 220 | }, 221 | { 222 | "x": 22, 223 | "y": 2, 224 | "w": 6, 225 | "h": 11 226 | }, 227 | { 228 | "x": 32, 229 | "y": 2, 230 | "w": 6, 231 | "h": 11 232 | }, 233 | { 234 | "x": 22, 235 | "y": 16, 236 | "w": 4, 237 | "h": 3 238 | }, 239 | { 240 | "x": 33, 241 | "y": 16, 242 | "w": 4, 243 | "h": 3 244 | }, 245 | { 246 | "x": 34, 247 | "y": 21, 248 | "w": 4, 249 | "h": 3 250 | }, 251 | { 252 | "x": 22, 253 | "y": 21, 254 | "w": 4, 255 | "h": 3 256 | }, 257 | { 258 | "x": 22, 259 | "y": 26, 260 | "w": 5, 261 | "h": 3 262 | }, 263 | { 264 | "x": 34, 265 | "y": 26, 266 | "w": 5, 267 | "h": 3 268 | }, 269 | { 270 | "x": 28, 271 | "y": 21, 272 | "w": 4, 273 | "h": 3 274 | }, 275 | { 276 | "x": 2, 277 | "y": 16, 278 | "w": 5, 279 | "h": 5 280 | }, 281 | { 282 | "x": 9, 283 | "y": 16, 284 | "w": 6, 285 | "h": 3 286 | }, 287 | { 288 | "x": 12, 289 | "y": 24, 290 | "w": 3, 291 | "h": 4 292 | }, 293 | { 294 | "x": 2, 295 | "y": 24, 296 | "w": 6, 297 | "h": 4 298 | }, 299 | { 300 | "x": 12, 301 | "y": 1, 302 | "w": 7, 303 | "h": 2 304 | }, 305 | { 306 | "x": 11, 307 | "y": 34, 308 | "w": 4, 309 | "h": 4 310 | }, 311 | { 312 | "x": 22, 313 | "y": 31, 314 | "w": 6, 315 | "h": 2 316 | }, 317 | { 318 | "x": 22, 319 | "y": 35, 320 | "w": 6, 321 | "h": 4 322 | }, 323 | { 324 | "x": 17, 325 | "y": 41, 326 | "w": 7, 327 | "h": 4 328 | } 329 | ], 330 | "offices": [ 331 | { 332 | "id": "o0", 333 | "x": 40, 334 | "y": 30, 335 | "offsetX": 5, 336 | "offsetY": 0 337 | } 338 | ] 339 | }, 340 | { 341 | "dogSpeed": 3.0, 342 | "id": "map3", 343 | "name": "Map 3", 344 | "lootTypes": [ 345 | { 346 | "name": "key", 347 | "file": "assets/key.obj", 348 | "type": "obj", 349 | "rotation": 90, 350 | "color": "#338844", 351 | "scale": 0.03, 352 | "value": 10 353 | }, 354 | { 355 | "name": "wallet", 356 | "file": "assets/wallet.obj", 357 | "type": "obj", 358 | "rotation": 0, 359 | "color": "#883344", 360 | "scale": 0.01, 361 | "value": 30 362 | } 363 | ], 364 | "roads": [ 365 | { 366 | "x0": 2, 367 | "y0": 2, 368 | "y1": 16 369 | }, 370 | { 371 | "x0": 12, 372 | "y0": 7, 373 | "y1": 0 374 | }, 375 | { 376 | "x0": 20, 377 | "y0": 0, 378 | "y1": 14 379 | }, 380 | { 381 | "x0": 14, 382 | "y0": 16, 383 | "y1": 14 384 | }, 385 | { 386 | "x0": 8, 387 | "y0": 7, 388 | "y1": 14 389 | }, 390 | { 391 | "x0": 2, 392 | "y0": 7, 393 | "x1": 20 394 | }, 395 | { 396 | "x0": 12, 397 | "y0": 0, 398 | "x1": 20 399 | }, 400 | { 401 | "x0": 20, 402 | "y0": 14, 403 | "x1": 8 404 | }, 405 | { 406 | "x0": 2, 407 | "y0": 16, 408 | "x1": 14 409 | } 410 | ], 411 | "buildings": [ 412 | { 413 | "x": 4, 414 | "y": 2, 415 | "w": 6, 416 | "h": 3 417 | }, 418 | { 419 | "x": 14, 420 | "y": 2, 421 | "w": 4, 422 | "h": 3 423 | }, 424 | { 425 | "x": 4, 426 | "y": 9, 427 | "w": 2, 428 | "h": 5 429 | }, 430 | { 431 | "x": 9, 432 | "y": 9, 433 | "w": 4, 434 | "h": 3 435 | }, 436 | { 437 | "x": 14, 438 | "y": 9, 439 | "w": 4, 440 | "h": 3 441 | } 442 | ], 443 | "offices": [ 444 | { 445 | "id": "o1", 446 | "x": 8, 447 | "y": 14, 448 | "offsetX": 1, 449 | "offsetY": -1 450 | } 451 | ] 452 | } 453 | ] 454 | } -------------------------------------------------------------------------------- /postman/DetectivePugs.postman_collection.json: -------------------------------------------------------------------------------- 1 | { 2 | "info": { 3 | "_postman_id": "a4b4f302-b3f9-4fc7-a012-b69d2fa555c1", 4 | "name": "DetectivePugs", 5 | "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", 6 | "_exporter_id": "2539805" 7 | }, 8 | "item": [ 9 | { 10 | "name": "Get all maps", 11 | "request": { 12 | "method": "GET", 13 | "header": [], 14 | "url": { 15 | "raw": "http://127.0.0.1/api/v1/maps", 16 | "protocol": "http", 17 | "host": [ 18 | "127", 19 | "0", 20 | "0", 21 | "1" 22 | ], 23 | "path": [ 24 | "api", 25 | "v1", 26 | "maps" 27 | ] 28 | }, 29 | "description": "Returns a list of all available maps on the server" 30 | }, 31 | "response": [ 32 | { 33 | "name": "maps", 34 | "originalRequest": { 35 | "method": "GET", 36 | "header": [], 37 | "url": { 38 | "raw": "http://127.0.0.1/api/v1/maps", 39 | "protocol": "http", 40 | "host": [ 41 | "127", 42 | "0", 43 | "0", 44 | "1" 45 | ], 46 | "path": [ 47 | "api", 48 | "v1", 49 | "maps" 50 | ] 51 | } 52 | }, 53 | "status": "OK", 54 | "code": 200, 55 | "_postman_previewlanguage": "json", 56 | "header": [ 57 | { 58 | "key": "Content-Type", 59 | "value": "application/json" 60 | }, 61 | { 62 | "key": "Content-Length", 63 | "value": "185" 64 | }, 65 | { 66 | "key": "Cache-Control", 67 | "value": "no-cache" 68 | } 69 | ], 70 | "cookie": [], 71 | "body": "[\n {\n \"id\": \"map1\",\n \"name\": \"Map 1\"\n },\n {\n \"id\": \"town\",\n \"name\": \"Town\"\n },\n {\n \"id\": \"map3\",\n \"name\": \"Map 3\"\n }\n]" 72 | } 73 | ] 74 | }, 75 | { 76 | "name": "Get certain map", 77 | "request": { 78 | "method": "GET", 79 | "header": [], 80 | "url": { 81 | "raw": "http://127.0.0.1/api/v1/maps/map1", 82 | "protocol": "http", 83 | "host": [ 84 | "127", 85 | "0", 86 | "0", 87 | "1" 88 | ], 89 | "path": [ 90 | "api", 91 | "v1", 92 | "maps", 93 | "map1" 94 | ] 95 | }, 96 | "description": "Returns information about a specific map" 97 | }, 98 | "response": [ 99 | { 100 | "name": "map1", 101 | "originalRequest": { 102 | "method": "GET", 103 | "header": [], 104 | "url": { 105 | "raw": "http://127.0.0.1/api/v1/maps/map1", 106 | "protocol": "http", 107 | "host": [ 108 | "127", 109 | "0", 110 | "0", 111 | "1" 112 | ], 113 | "path": [ 114 | "api", 115 | "v1", 116 | "maps", 117 | "map1" 118 | ] 119 | } 120 | }, 121 | "status": "OK", 122 | "code": 200, 123 | "_postman_previewlanguage": "json", 124 | "header": [ 125 | { 126 | "key": "Content-Type", 127 | "value": "application/json" 128 | }, 129 | { 130 | "key": "Content-Length", 131 | "value": "1334" 132 | }, 133 | { 134 | "key": "Cache-Control", 135 | "value": "no-cache" 136 | } 137 | ], 138 | "cookie": [], 139 | "body": "{\n \"id\": \"map1\",\n \"name\": \"Map 1\",\n \"roads\": [\n {\n \"x0\": 0,\n \"y0\": 0,\n \"x1\": 40\n },\n {\n \"x0\": 40,\n \"y0\": 0,\n \"y1\": 30\n },\n {\n \"x0\": 40,\n \"y0\": 30,\n \"x1\": 0\n },\n {\n \"x0\": 0,\n \"y0\": 0,\n \"y1\": 30\n }\n ],\n \"buildings\": [\n {\n \"x\": 5,\n \"y\": 5,\n \"w\": 30,\n \"h\": 20\n }\n ],\n \"offices\": [\n {\n \"id\": \"o0\",\n \"x\": 40,\n \"y\": 30,\n \"offsetX\": 5,\n \"offsetY\": 0\n }\n ],\n \"lootTypes\": [\n {\n \"name\": \"key\",\n \"file\": \"assets/key.obj\",\n \"type\": \"obj\",\n \"rotation\": 90,\n \"color\": \"#338844\",\n \"scale\": 0.03,\n \"value\": 10\n },\n {\n \"name\": \"wallet\",\n \"file\": \"assets/wallet.obj\",\n \"type\": \"obj\",\n \"rotation\": 0,\n \"color\": \"#883344\",\n \"scale\": 0.01,\n \"value\": 30\n }\n ]\n}" 140 | } 141 | ] 142 | }, 143 | { 144 | "name": "Join to game", 145 | "request": { 146 | "method": "POST", 147 | "header": [], 148 | "body": { 149 | "mode": "raw", 150 | "raw": "", 151 | "options": { 152 | "raw": { 153 | "language": "json" 154 | } 155 | } 156 | }, 157 | "url": { 158 | "raw": "http://127.0.0.1/api/v1/game/join", 159 | "protocol": "http", 160 | "host": [ 161 | "127", 162 | "0", 163 | "0", 164 | "1" 165 | ], 166 | "path": [ 167 | "api", 168 | "v1", 169 | "game", 170 | "join" 171 | ] 172 | }, 173 | "description": "Join the game and receive an ID with and auth token" 174 | }, 175 | "response": [ 176 | { 177 | "name": "join", 178 | "originalRequest": { 179 | "method": "POST", 180 | "header": [], 181 | "body": { 182 | "mode": "raw", 183 | "raw": "{\r\n \"userName\": \"Scooby Doo\",\r\n \"mapId\": \"map1\"\r\n}", 184 | "options": { 185 | "raw": { 186 | "language": "json" 187 | } 188 | } 189 | }, 190 | "url": { 191 | "raw": "http://127.0.0.1/api/v1/game/join", 192 | "protocol": "http", 193 | "host": [ 194 | "127", 195 | "0", 196 | "0", 197 | "1" 198 | ], 199 | "path": [ 200 | "api", 201 | "v1", 202 | "game", 203 | "join" 204 | ] 205 | } 206 | }, 207 | "status": "OK", 208 | "code": 200, 209 | "_postman_previewlanguage": "json", 210 | "header": [ 211 | { 212 | "key": "Content-Type", 213 | "value": "application/json" 214 | }, 215 | { 216 | "key": "Content-Length", 217 | "value": "61" 218 | }, 219 | { 220 | "key": "Cache-Control", 221 | "value": "no-cache" 222 | } 223 | ], 224 | "cookie": [], 225 | "body": "{\n \"authToken\": \"11148d10a4d300f2812c19b4acbc81fc\",\n \"playerId\": 1\n}" 226 | } 227 | ] 228 | }, 229 | { 230 | "name": "Move player", 231 | "request": { 232 | "auth": { 233 | "type": "bearer", 234 | "bearer": [ 235 | { 236 | "key": "token", 237 | "value": "bb3377890a26f047d022614320a0614b", 238 | "type": "string" 239 | } 240 | ] 241 | }, 242 | "method": "POST", 243 | "header": [], 244 | "body": { 245 | "mode": "raw", 246 | "raw": "", 247 | "options": { 248 | "raw": { 249 | "language": "json" 250 | } 251 | } 252 | }, 253 | "url": { 254 | "raw": "http://127.0.0.1:80/api/v1/game/player/action", 255 | "protocol": "http", 256 | "host": [ 257 | "127", 258 | "0", 259 | "0", 260 | "1" 261 | ], 262 | "port": "80", 263 | "path": [ 264 | "api", 265 | "v1", 266 | "game", 267 | "player", 268 | "action" 269 | ] 270 | }, 271 | "description": "Set the character's direction of movement. \n\nAllowed values:\n* R\n* L\n* U\n* D" 272 | }, 273 | "response": [ 274 | { 275 | "name": "Move player", 276 | "originalRequest": { 277 | "method": "POST", 278 | "header": [], 279 | "body": { 280 | "mode": "raw", 281 | "raw": "{\r\n \"move\": \"R\"\r\n}", 282 | "options": { 283 | "raw": { 284 | "language": "json" 285 | } 286 | } 287 | }, 288 | "url": { 289 | "raw": "http://127.0.0.1:80/api/v1/game/player/action", 290 | "protocol": "http", 291 | "host": [ 292 | "127", 293 | "0", 294 | "0", 295 | "1" 296 | ], 297 | "port": "80", 298 | "path": [ 299 | "api", 300 | "v1", 301 | "game", 302 | "player", 303 | "action" 304 | ] 305 | } 306 | }, 307 | "status": "OK", 308 | "code": 200, 309 | "_postman_previewlanguage": "json", 310 | "header": [ 311 | { 312 | "key": "Content-Type", 313 | "value": "application/json" 314 | }, 315 | { 316 | "key": "Content-Length", 317 | "value": "2" 318 | }, 319 | { 320 | "key": "Cache-Control", 321 | "value": "no-cache" 322 | } 323 | ], 324 | "cookie": [], 325 | "body": "{}" 326 | } 327 | ] 328 | }, 329 | { 330 | "name": "Get game state", 331 | "request": { 332 | "auth": { 333 | "type": "bearer", 334 | "bearer": [ 335 | { 336 | "key": "token", 337 | "value": "bb3377890a26f047d022614320a0614b", 338 | "type": "string" 339 | } 340 | ] 341 | }, 342 | "method": "GET", 343 | "header": [], 344 | "url": { 345 | "raw": "http://127.0.0.1:80/api/v1/game/state", 346 | "protocol": "http", 347 | "host": [ 348 | "127", 349 | "0", 350 | "0", 351 | "1" 352 | ], 353 | "port": "80", 354 | "path": [ 355 | "api", 356 | "v1", 357 | "game", 358 | "state" 359 | ] 360 | }, 361 | "description": "Returns information about the current game. \nplayers - array with players.\n\n- pos - player's x, y position\n \n- speed - acceleration of the player by x, y\n \n- dir - player's direction\n \n- bag - array with found items\n \n- lostObjects - array of items on the map\n \n- type - type of object\n \n- pos - item position by x, y" 362 | }, 363 | "response": [ 364 | { 365 | "name": "state", 366 | "originalRequest": { 367 | "method": "GET", 368 | "header": [], 369 | "url": { 370 | "raw": "http://127.0.0.1:80/api/v1/game/state", 371 | "protocol": "http", 372 | "host": [ 373 | "127", 374 | "0", 375 | "0", 376 | "1" 377 | ], 378 | "port": "80", 379 | "path": [ 380 | "api", 381 | "v1", 382 | "game", 383 | "state" 384 | ] 385 | } 386 | }, 387 | "status": "OK", 388 | "code": 200, 389 | "_postman_previewlanguage": "json", 390 | "header": [ 391 | { 392 | "key": "Content-Type", 393 | "value": "application/json" 394 | }, 395 | { 396 | "key": "Content-Length", 397 | "value": "481" 398 | }, 399 | { 400 | "key": "Cache-Control", 401 | "value": "no-cache" 402 | } 403 | ], 404 | "cookie": [], 405 | "body": "{\n \"players\": {\n \"2\": {\n \"pos\": [\n 40.4,\n 0\n ],\n \"speed\": [\n 0,\n 0\n ],\n \"dir\": \"R\",\n \"bag\": [],\n \"score\": 0\n }\n },\n \"lostObjects\": {\n \"0\": {\n \"type\": 0,\n \"pos\": [\n 2.28074,\n 30\n ]\n }\n }\n}" 406 | } 407 | ] 408 | }, 409 | { 410 | "name": "Tick", 411 | "protocolProfileBehavior": { 412 | "disableBodyPruning": true 413 | }, 414 | "request": { 415 | "method": "GET", 416 | "header": [], 417 | "body": { 418 | "mode": "raw", 419 | "raw": "", 420 | "options": { 421 | "raw": { 422 | "language": "json" 423 | } 424 | } 425 | }, 426 | "url": { 427 | "raw": "http://127.0.0.1:80/api/v1/game/tick", 428 | "protocol": "http", 429 | "host": [ 430 | "127", 431 | "0", 432 | "0", 433 | "1" 434 | ], 435 | "port": "80", 436 | "path": [ 437 | "api", 438 | "v1", 439 | "game", 440 | "tick" 441 | ] 442 | }, 443 | "description": "Used in test scenarios" 444 | }, 445 | "response": [] 446 | } 447 | ] 448 | } -------------------------------------------------------------------------------- /static/js/loaders/MTLLoader.js: -------------------------------------------------------------------------------- 1 | ( function () { 2 | 3 | /** 4 | * Loads a Wavefront .mtl file specifying materials 5 | */ 6 | 7 | class MTLLoader extends THREE.Loader { 8 | 9 | constructor( manager ) { 10 | 11 | super( manager ); 12 | 13 | } 14 | /** 15 | * Loads and parses a MTL asset from a URL. 16 | * 17 | * @param {String} url - URL to the MTL file. 18 | * @param {Function} [onLoad] - Callback invoked with the loaded object. 19 | * @param {Function} [onProgress] - Callback for download progress. 20 | * @param {Function} [onError] - Callback for download errors. 21 | * 22 | * @see setPath setResourcePath 23 | * 24 | * @note In order for relative texture references to resolve correctly 25 | * you must call setResourcePath() explicitly prior to load. 26 | */ 27 | 28 | 29 | load( url, onLoad, onProgress, onError ) { 30 | 31 | const scope = this; 32 | const path = this.path === '' ? THREE.LoaderUtils.extractUrlBase( url ) : this.path; 33 | const loader = new THREE.FileLoader( this.manager ); 34 | loader.setPath( this.path ); 35 | loader.setRequestHeader( this.requestHeader ); 36 | loader.setWithCredentials( this.withCredentials ); 37 | loader.load( url, function ( text ) { 38 | 39 | try { 40 | 41 | onLoad( scope.parse( text, path ) ); 42 | 43 | } catch ( e ) { 44 | 45 | if ( onError ) { 46 | 47 | onError( e ); 48 | 49 | } else { 50 | 51 | console.error( e ); 52 | 53 | } 54 | 55 | scope.manager.itemError( url ); 56 | 57 | } 58 | 59 | }, onProgress, onError ); 60 | 61 | } 62 | 63 | setMaterialOptions( value ) { 64 | 65 | this.materialOptions = value; 66 | return this; 67 | 68 | } 69 | /** 70 | * Parses a MTL file. 71 | * 72 | * @param {String} text - Content of MTL file 73 | * @return {MaterialCreator} 74 | * 75 | * @see setPath setResourcePath 76 | * 77 | * @note In order for relative texture references to resolve correctly 78 | * you must call setResourcePath() explicitly prior to parse. 79 | */ 80 | 81 | 82 | parse( text, path ) { 83 | 84 | const lines = text.split( '\n' ); 85 | let info = {}; 86 | const delimiter_pattern = /\s+/; 87 | const materialsInfo = {}; 88 | 89 | for ( let i = 0; i < lines.length; i ++ ) { 90 | 91 | let line = lines[ i ]; 92 | line = line.trim(); 93 | 94 | if ( line.length === 0 || line.charAt( 0 ) === '#' ) { 95 | 96 | // Blank line or comment ignore 97 | continue; 98 | 99 | } 100 | 101 | const pos = line.indexOf( ' ' ); 102 | let key = pos >= 0 ? line.substring( 0, pos ) : line; 103 | key = key.toLowerCase(); 104 | let value = pos >= 0 ? line.substring( pos + 1 ) : ''; 105 | value = value.trim(); 106 | 107 | if ( key === 'newmtl' ) { 108 | 109 | // New material 110 | info = { 111 | name: value 112 | }; 113 | materialsInfo[ value ] = info; 114 | 115 | } else { 116 | 117 | if ( key === 'ka' || key === 'kd' || key === 'ks' || key === 'ke' ) { 118 | 119 | const ss = value.split( delimiter_pattern, 3 ); 120 | info[ key ] = [ parseFloat( ss[ 0 ] ), parseFloat( ss[ 1 ] ), parseFloat( ss[ 2 ] ) ]; 121 | 122 | } else { 123 | 124 | info[ key ] = value; 125 | 126 | } 127 | 128 | } 129 | 130 | } 131 | 132 | const materialCreator = new MaterialCreator( this.resourcePath || path, this.materialOptions ); 133 | materialCreator.setCrossOrigin( this.crossOrigin ); 134 | materialCreator.setManager( this.manager ); 135 | materialCreator.setMaterials( materialsInfo ); 136 | return materialCreator; 137 | 138 | } 139 | 140 | } 141 | /** 142 | * Create a new MTLLoader.MaterialCreator 143 | * @param baseUrl - Url relative to which textures are loaded 144 | * @param options - Set of options on how to construct the materials 145 | * side: Which side to apply the material 146 | * THREE.FrontSide (default), THREE.BackSide, THREE.DoubleSide 147 | * wrap: What type of wrapping to apply for textures 148 | * THREE.RepeatWrapping (default), THREE.ClampToEdgeWrapping, THREE.MirroredRepeatWrapping 149 | * normalizeRGB: RGBs need to be normalized to 0-1 from 0-255 150 | * Default: false, assumed to be already normalized 151 | * ignoreZeroRGBs: Ignore values of RGBs (Ka,Kd,Ks) that are all 0's 152 | * Default: false 153 | * @constructor 154 | */ 155 | 156 | 157 | class MaterialCreator { 158 | 159 | constructor( baseUrl = '', options = {} ) { 160 | 161 | this.baseUrl = baseUrl; 162 | this.options = options; 163 | this.materialsInfo = {}; 164 | this.materials = {}; 165 | this.materialsArray = []; 166 | this.nameLookup = {}; 167 | this.crossOrigin = 'anonymous'; 168 | this.side = this.options.side !== undefined ? this.options.side : THREE.FrontSide; 169 | this.wrap = this.options.wrap !== undefined ? this.options.wrap : THREE.RepeatWrapping; 170 | 171 | } 172 | 173 | setCrossOrigin( value ) { 174 | 175 | this.crossOrigin = value; 176 | return this; 177 | 178 | } 179 | 180 | setManager( value ) { 181 | 182 | this.manager = value; 183 | 184 | } 185 | 186 | setMaterials( materialsInfo ) { 187 | 188 | this.materialsInfo = this.convert( materialsInfo ); 189 | this.materials = {}; 190 | this.materialsArray = []; 191 | this.nameLookup = {}; 192 | 193 | } 194 | 195 | convert( materialsInfo ) { 196 | 197 | if ( ! this.options ) return materialsInfo; 198 | const converted = {}; 199 | 200 | for ( const mn in materialsInfo ) { 201 | 202 | // Convert materials info into normalized form based on options 203 | const mat = materialsInfo[ mn ]; 204 | const covmat = {}; 205 | converted[ mn ] = covmat; 206 | 207 | for ( const prop in mat ) { 208 | 209 | let save = true; 210 | let value = mat[ prop ]; 211 | const lprop = prop.toLowerCase(); 212 | 213 | switch ( lprop ) { 214 | 215 | case 'kd': 216 | case 'ka': 217 | case 'ks': 218 | // Diffuse color (color under white light) using RGB values 219 | if ( this.options && this.options.normalizeRGB ) { 220 | 221 | value = [ value[ 0 ] / 255, value[ 1 ] / 255, value[ 2 ] / 255 ]; 222 | 223 | } 224 | 225 | if ( this.options && this.options.ignoreZeroRGBs ) { 226 | 227 | if ( value[ 0 ] === 0 && value[ 1 ] === 0 && value[ 2 ] === 0 ) { 228 | 229 | // ignore 230 | save = false; 231 | 232 | } 233 | 234 | } 235 | 236 | break; 237 | 238 | default: 239 | break; 240 | 241 | } 242 | 243 | if ( save ) { 244 | 245 | covmat[ lprop ] = value; 246 | 247 | } 248 | 249 | } 250 | 251 | } 252 | 253 | return converted; 254 | 255 | } 256 | 257 | preload() { 258 | 259 | for ( const mn in this.materialsInfo ) { 260 | 261 | this.create( mn ); 262 | 263 | } 264 | 265 | } 266 | 267 | getIndex( materialName ) { 268 | 269 | return this.nameLookup[ materialName ]; 270 | 271 | } 272 | 273 | getAsArray() { 274 | 275 | let index = 0; 276 | 277 | for ( const mn in this.materialsInfo ) { 278 | 279 | this.materialsArray[ index ] = this.create( mn ); 280 | this.nameLookup[ mn ] = index; 281 | index ++; 282 | 283 | } 284 | 285 | return this.materialsArray; 286 | 287 | } 288 | 289 | create( materialName ) { 290 | 291 | if ( this.materials[ materialName ] === undefined ) { 292 | 293 | this.createMaterial_( materialName ); 294 | 295 | } 296 | 297 | return this.materials[ materialName ]; 298 | 299 | } 300 | 301 | createMaterial_( materialName ) { 302 | 303 | // Create material 304 | const scope = this; 305 | const mat = this.materialsInfo[ materialName ]; 306 | const params = { 307 | name: materialName, 308 | side: this.side 309 | }; 310 | 311 | function resolveURL( baseUrl, url ) { 312 | 313 | if ( typeof url !== 'string' || url === '' ) return ''; // Absolute URL 314 | 315 | if ( /^https?:\/\//i.test( url ) ) return url; 316 | return baseUrl + url; 317 | 318 | } 319 | 320 | function setMapForType( mapType, value ) { 321 | 322 | if ( params[ mapType ] ) return; // Keep the first encountered texture 323 | 324 | const texParams = scope.getTextureParams( value, params ); 325 | const map = scope.loadTexture( resolveURL( scope.baseUrl, texParams.url ) ); 326 | map.repeat.copy( texParams.scale ); 327 | map.offset.copy( texParams.offset ); 328 | map.wrapS = scope.wrap; 329 | map.wrapT = scope.wrap; 330 | 331 | if ( mapType === 'map' || mapType === 'emissiveMap' ) { 332 | 333 | map.encoding = THREE.sRGBEncoding; 334 | 335 | } 336 | 337 | params[ mapType ] = map; 338 | 339 | } 340 | 341 | for ( const prop in mat ) { 342 | 343 | const value = mat[ prop ]; 344 | let n; 345 | if ( value === '' ) continue; 346 | 347 | switch ( prop.toLowerCase() ) { 348 | 349 | // Ns is material specular exponent 350 | case 'kd': 351 | // Diffuse color (color under white light) using RGB values 352 | params.color = new THREE.Color().fromArray( value ).convertSRGBToLinear(); 353 | break; 354 | 355 | case 'ks': 356 | // Specular color (color when light is reflected from shiny surface) using RGB values 357 | params.specular = new THREE.Color().fromArray( value ).convertSRGBToLinear(); 358 | break; 359 | 360 | case 'ke': 361 | // Emissive using RGB values 362 | params.emissive = new THREE.Color().fromArray( value ).convertSRGBToLinear(); 363 | break; 364 | 365 | case 'map_kd': 366 | // Diffuse texture map 367 | setMapForType( 'map', value ); 368 | break; 369 | 370 | case 'map_ks': 371 | // Specular map 372 | setMapForType( 'specularMap', value ); 373 | break; 374 | 375 | case 'map_ke': 376 | // Emissive map 377 | setMapForType( 'emissiveMap', value ); 378 | break; 379 | 380 | case 'norm': 381 | setMapForType( 'normalMap', value ); 382 | break; 383 | 384 | case 'map_bump': 385 | case 'bump': 386 | // Bump texture map 387 | setMapForType( 'bumpMap', value ); 388 | break; 389 | 390 | case 'map_d': 391 | // Alpha map 392 | setMapForType( 'alphaMap', value ); 393 | params.transparent = true; 394 | break; 395 | 396 | case 'ns': 397 | // The specular exponent (defines the focus of the specular highlight) 398 | // A high exponent results in a tight, concentrated highlight. Ns values normally range from 0 to 1000. 399 | params.shininess = parseFloat( value ); 400 | break; 401 | 402 | case 'd': 403 | n = parseFloat( value ); 404 | 405 | if ( n < 1 ) { 406 | 407 | params.opacity = n; 408 | params.transparent = true; 409 | 410 | } 411 | 412 | break; 413 | 414 | case 'tr': 415 | n = parseFloat( value ); 416 | if ( this.options && this.options.invertTrProperty ) n = 1 - n; 417 | 418 | if ( n > 0 ) { 419 | 420 | params.opacity = 1 - n; 421 | params.transparent = true; 422 | 423 | } 424 | 425 | break; 426 | 427 | default: 428 | break; 429 | 430 | } 431 | 432 | } 433 | 434 | this.materials[ materialName ] = new THREE.MeshPhongMaterial( params ); 435 | return this.materials[ materialName ]; 436 | 437 | } 438 | 439 | getTextureParams( value, matParams ) { 440 | 441 | const texParams = { 442 | scale: new THREE.Vector2( 1, 1 ), 443 | offset: new THREE.Vector2( 0, 0 ) 444 | }; 445 | const items = value.split( /\s+/ ); 446 | let pos; 447 | pos = items.indexOf( '-bm' ); 448 | 449 | if ( pos >= 0 ) { 450 | 451 | matParams.bumpScale = parseFloat( items[ pos + 1 ] ); 452 | items.splice( pos, 2 ); 453 | 454 | } 455 | 456 | pos = items.indexOf( '-s' ); 457 | 458 | if ( pos >= 0 ) { 459 | 460 | texParams.scale.set( parseFloat( items[ pos + 1 ] ), parseFloat( items[ pos + 2 ] ) ); 461 | items.splice( pos, 4 ); // we expect 3 parameters here! 462 | 463 | } 464 | 465 | pos = items.indexOf( '-o' ); 466 | 467 | if ( pos >= 0 ) { 468 | 469 | texParams.offset.set( parseFloat( items[ pos + 1 ] ), parseFloat( items[ pos + 2 ] ) ); 470 | items.splice( pos, 4 ); // we expect 3 parameters here! 471 | 472 | } 473 | 474 | texParams.url = items.join( ' ' ).trim(); 475 | return texParams; 476 | 477 | } 478 | 479 | loadTexture( url, mapping, onLoad, onProgress, onError ) { 480 | 481 | const manager = this.manager !== undefined ? this.manager : THREE.DefaultLoadingManager; 482 | let loader = manager.getHandler( url ); 483 | 484 | if ( loader === null ) { 485 | 486 | loader = new THREE.TextureLoader( manager ); 487 | 488 | } 489 | 490 | if ( loader.setCrossOrigin ) loader.setCrossOrigin( this.crossOrigin ); 491 | const texture = loader.load( url, onLoad, onProgress, onError ); 492 | if ( mapping !== undefined ) texture.mapping = mapping; 493 | return texture; 494 | 495 | } 496 | 497 | } 498 | 499 | THREE.MTLLoader = MTLLoader; 500 | 501 | } )(); 502 | -------------------------------------------------------------------------------- /static/js/utils/SkeletonUtils.js: -------------------------------------------------------------------------------- 1 | ( function () { 2 | 3 | function retarget( target, source, options = {} ) { 4 | 5 | const pos = new THREE.Vector3(), 6 | quat = new THREE.Quaternion(), 7 | scale = new THREE.Vector3(), 8 | bindBoneMatrix = new THREE.Matrix4(), 9 | relativeMatrix = new THREE.Matrix4(), 10 | globalMatrix = new THREE.Matrix4(); 11 | options.preserveMatrix = options.preserveMatrix !== undefined ? options.preserveMatrix : true; 12 | options.preservePosition = options.preservePosition !== undefined ? options.preservePosition : true; 13 | options.preserveHipPosition = options.preserveHipPosition !== undefined ? options.preserveHipPosition : false; 14 | options.useTargetMatrix = options.useTargetMatrix !== undefined ? options.useTargetMatrix : false; 15 | options.hip = options.hip !== undefined ? options.hip : 'hip'; 16 | options.names = options.names || {}; 17 | const sourceBones = source.isObject3D ? source.skeleton.bones : getBones( source ), 18 | bones = target.isObject3D ? target.skeleton.bones : getBones( target ); 19 | let bindBones, bone, name, boneTo, bonesPosition; // reset bones 20 | 21 | if ( target.isObject3D ) { 22 | 23 | target.skeleton.pose(); 24 | 25 | } else { 26 | 27 | options.useTargetMatrix = true; 28 | options.preserveMatrix = false; 29 | 30 | } 31 | 32 | if ( options.preservePosition ) { 33 | 34 | bonesPosition = []; 35 | 36 | for ( let i = 0; i < bones.length; i ++ ) { 37 | 38 | bonesPosition.push( bones[ i ].position.clone() ); 39 | 40 | } 41 | 42 | } 43 | 44 | if ( options.preserveMatrix ) { 45 | 46 | // reset matrix 47 | target.updateMatrixWorld(); 48 | target.matrixWorld.identity(); // reset children matrix 49 | 50 | for ( let i = 0; i < target.children.length; ++ i ) { 51 | 52 | target.children[ i ].updateMatrixWorld( true ); 53 | 54 | } 55 | 56 | } 57 | 58 | if ( options.offsets ) { 59 | 60 | bindBones = []; 61 | 62 | for ( let i = 0; i < bones.length; ++ i ) { 63 | 64 | bone = bones[ i ]; 65 | name = options.names[ bone.name ] || bone.name; 66 | 67 | if ( options.offsets[ name ] ) { 68 | 69 | bone.matrix.multiply( options.offsets[ name ] ); 70 | bone.matrix.decompose( bone.position, bone.quaternion, bone.scale ); 71 | bone.updateMatrixWorld(); 72 | 73 | } 74 | 75 | bindBones.push( bone.matrixWorld.clone() ); 76 | 77 | } 78 | 79 | } 80 | 81 | for ( let i = 0; i < bones.length; ++ i ) { 82 | 83 | bone = bones[ i ]; 84 | name = options.names[ bone.name ] || bone.name; 85 | boneTo = getBoneByName( name, sourceBones ); 86 | globalMatrix.copy( bone.matrixWorld ); 87 | 88 | if ( boneTo ) { 89 | 90 | boneTo.updateMatrixWorld(); 91 | 92 | if ( options.useTargetMatrix ) { 93 | 94 | relativeMatrix.copy( boneTo.matrixWorld ); 95 | 96 | } else { 97 | 98 | relativeMatrix.copy( target.matrixWorld ).invert(); 99 | relativeMatrix.multiply( boneTo.matrixWorld ); 100 | 101 | } // ignore scale to extract rotation 102 | 103 | 104 | scale.setFromMatrixScale( relativeMatrix ); 105 | relativeMatrix.scale( scale.set( 1 / scale.x, 1 / scale.y, 1 / scale.z ) ); // apply to global matrix 106 | 107 | globalMatrix.makeRotationFromQuaternion( quat.setFromRotationMatrix( relativeMatrix ) ); 108 | 109 | if ( target.isObject3D ) { 110 | 111 | const boneIndex = bones.indexOf( bone ), 112 | wBindMatrix = bindBones ? bindBones[ boneIndex ] : bindBoneMatrix.copy( target.skeleton.boneInverses[ boneIndex ] ).invert(); 113 | globalMatrix.multiply( wBindMatrix ); 114 | 115 | } 116 | 117 | globalMatrix.copyPosition( relativeMatrix ); 118 | 119 | } 120 | 121 | if ( bone.parent && bone.parent.isBone ) { 122 | 123 | bone.matrix.copy( bone.parent.matrixWorld ).invert(); 124 | bone.matrix.multiply( globalMatrix ); 125 | 126 | } else { 127 | 128 | bone.matrix.copy( globalMatrix ); 129 | 130 | } 131 | 132 | if ( options.preserveHipPosition && name === options.hip ) { 133 | 134 | bone.matrix.setPosition( pos.set( 0, bone.position.y, 0 ) ); 135 | 136 | } 137 | 138 | bone.matrix.decompose( bone.position, bone.quaternion, bone.scale ); 139 | bone.updateMatrixWorld(); 140 | 141 | } 142 | 143 | if ( options.preservePosition ) { 144 | 145 | for ( let i = 0; i < bones.length; ++ i ) { 146 | 147 | bone = bones[ i ]; 148 | name = options.names[ bone.name ] || bone.name; 149 | 150 | if ( name !== options.hip ) { 151 | 152 | bone.position.copy( bonesPosition[ i ] ); 153 | 154 | } 155 | 156 | } 157 | 158 | } 159 | 160 | if ( options.preserveMatrix ) { 161 | 162 | // restore matrix 163 | target.updateMatrixWorld( true ); 164 | 165 | } 166 | 167 | } 168 | 169 | function retargetClip( target, source, clip, options = {} ) { 170 | 171 | options.useFirstFramePosition = options.useFirstFramePosition !== undefined ? options.useFirstFramePosition : false; 172 | options.fps = options.fps !== undefined ? options.fps : 30; 173 | options.names = options.names || []; 174 | 175 | if ( ! source.isObject3D ) { 176 | 177 | source = getHelperFromSkeleton( source ); 178 | 179 | } 180 | 181 | const numFrames = Math.round( clip.duration * ( options.fps / 1000 ) * 1000 ), 182 | delta = 1 / options.fps, 183 | convertedTracks = [], 184 | mixer = new THREE.AnimationMixer( source ), 185 | bones = getBones( target.skeleton ), 186 | boneDatas = []; 187 | let positionOffset, bone, boneTo, boneData, name; 188 | mixer.clipAction( clip ).play(); 189 | mixer.update( 0 ); 190 | source.updateMatrixWorld(); 191 | 192 | for ( let i = 0; i < numFrames; ++ i ) { 193 | 194 | const time = i * delta; 195 | retarget( target, source, options ); 196 | 197 | for ( let j = 0; j < bones.length; ++ j ) { 198 | 199 | name = options.names[ bones[ j ].name ] || bones[ j ].name; 200 | boneTo = getBoneByName( name, source.skeleton ); 201 | 202 | if ( boneTo ) { 203 | 204 | bone = bones[ j ]; 205 | boneData = boneDatas[ j ] = boneDatas[ j ] || { 206 | bone: bone 207 | }; 208 | 209 | if ( options.hip === name ) { 210 | 211 | if ( ! boneData.pos ) { 212 | 213 | boneData.pos = { 214 | times: new Float32Array( numFrames ), 215 | values: new Float32Array( numFrames * 3 ) 216 | }; 217 | 218 | } 219 | 220 | if ( options.useFirstFramePosition ) { 221 | 222 | if ( i === 0 ) { 223 | 224 | positionOffset = bone.position.clone(); 225 | 226 | } 227 | 228 | bone.position.sub( positionOffset ); 229 | 230 | } 231 | 232 | boneData.pos.times[ i ] = time; 233 | bone.position.toArray( boneData.pos.values, i * 3 ); 234 | 235 | } 236 | 237 | if ( ! boneData.quat ) { 238 | 239 | boneData.quat = { 240 | times: new Float32Array( numFrames ), 241 | values: new Float32Array( numFrames * 4 ) 242 | }; 243 | 244 | } 245 | 246 | boneData.quat.times[ i ] = time; 247 | bone.quaternion.toArray( boneData.quat.values, i * 4 ); 248 | 249 | } 250 | 251 | } 252 | 253 | mixer.update( delta ); 254 | source.updateMatrixWorld(); 255 | 256 | } 257 | 258 | for ( let i = 0; i < boneDatas.length; ++ i ) { 259 | 260 | boneData = boneDatas[ i ]; 261 | 262 | if ( boneData ) { 263 | 264 | if ( boneData.pos ) { 265 | 266 | convertedTracks.push( new THREE.VectorKeyframeTrack( '.bones[' + boneData.bone.name + '].position', boneData.pos.times, boneData.pos.values ) ); 267 | 268 | } 269 | 270 | convertedTracks.push( new THREE.QuaternionKeyframeTrack( '.bones[' + boneData.bone.name + '].quaternion', boneData.quat.times, boneData.quat.values ) ); 271 | 272 | } 273 | 274 | } 275 | 276 | mixer.uncacheAction( clip ); 277 | return new THREE.AnimationClip( clip.name, - 1, convertedTracks ); 278 | 279 | } 280 | 281 | function getHelperFromSkeleton( skeleton ) { 282 | 283 | const source = new THREE.SkeletonHelper( skeleton.bones[ 0 ] ); 284 | source.skeleton = skeleton; 285 | return source; 286 | 287 | } 288 | 289 | function getSkeletonOffsets( target, source, options = {} ) { 290 | 291 | const targetParentPos = new THREE.Vector3(), 292 | targetPos = new THREE.Vector3(), 293 | sourceParentPos = new THREE.Vector3(), 294 | sourcePos = new THREE.Vector3(), 295 | targetDir = new THREE.Vector2(), 296 | sourceDir = new THREE.Vector2(); 297 | options.hip = options.hip !== undefined ? options.hip : 'hip'; 298 | options.names = options.names || {}; 299 | 300 | if ( ! source.isObject3D ) { 301 | 302 | source = getHelperFromSkeleton( source ); 303 | 304 | } 305 | 306 | const nameKeys = Object.keys( options.names ), 307 | nameValues = Object.values( options.names ), 308 | sourceBones = source.isObject3D ? source.skeleton.bones : getBones( source ), 309 | bones = target.isObject3D ? target.skeleton.bones : getBones( target ), 310 | offsets = []; 311 | let bone, boneTo, name, i; 312 | target.skeleton.pose(); 313 | 314 | for ( i = 0; i < bones.length; ++ i ) { 315 | 316 | bone = bones[ i ]; 317 | name = options.names[ bone.name ] || bone.name; 318 | boneTo = getBoneByName( name, sourceBones ); 319 | 320 | if ( boneTo && name !== options.hip ) { 321 | 322 | const boneParent = getNearestBone( bone.parent, nameKeys ), 323 | boneToParent = getNearestBone( boneTo.parent, nameValues ); 324 | boneParent.updateMatrixWorld(); 325 | boneToParent.updateMatrixWorld(); 326 | targetParentPos.setFromMatrixPosition( boneParent.matrixWorld ); 327 | targetPos.setFromMatrixPosition( bone.matrixWorld ); 328 | sourceParentPos.setFromMatrixPosition( boneToParent.matrixWorld ); 329 | sourcePos.setFromMatrixPosition( boneTo.matrixWorld ); 330 | targetDir.subVectors( new THREE.Vector2( targetPos.x, targetPos.y ), new THREE.Vector2( targetParentPos.x, targetParentPos.y ) ).normalize(); 331 | sourceDir.subVectors( new THREE.Vector2( sourcePos.x, sourcePos.y ), new THREE.Vector2( sourceParentPos.x, sourceParentPos.y ) ).normalize(); 332 | const laterialAngle = targetDir.angle() - sourceDir.angle(); 333 | const offset = new THREE.Matrix4().makeRotationFromEuler( new THREE.Euler( 0, 0, laterialAngle ) ); 334 | bone.matrix.multiply( offset ); 335 | bone.matrix.decompose( bone.position, bone.quaternion, bone.scale ); 336 | bone.updateMatrixWorld(); 337 | offsets[ name ] = offset; 338 | 339 | } 340 | 341 | } 342 | 343 | return offsets; 344 | 345 | } 346 | 347 | function renameBones( skeleton, names ) { 348 | 349 | const bones = getBones( skeleton ); 350 | 351 | for ( let i = 0; i < bones.length; ++ i ) { 352 | 353 | const bone = bones[ i ]; 354 | 355 | if ( names[ bone.name ] ) { 356 | 357 | bone.name = names[ bone.name ]; 358 | 359 | } 360 | 361 | } 362 | 363 | return this; 364 | 365 | } 366 | 367 | function getBones( skeleton ) { 368 | 369 | return Array.isArray( skeleton ) ? skeleton : skeleton.bones; 370 | 371 | } 372 | 373 | function getBoneByName( name, skeleton ) { 374 | 375 | for ( let i = 0, bones = getBones( skeleton ); i < bones.length; i ++ ) { 376 | 377 | if ( name === bones[ i ].name ) return bones[ i ]; 378 | 379 | } 380 | 381 | } 382 | 383 | function getNearestBone( bone, names ) { 384 | 385 | while ( bone.isBone ) { 386 | 387 | if ( names.indexOf( bone.name ) !== - 1 ) { 388 | 389 | return bone; 390 | 391 | } 392 | 393 | bone = bone.parent; 394 | 395 | } 396 | 397 | } 398 | 399 | function findBoneTrackData( name, tracks ) { 400 | 401 | const regexp = /\[(.*)\]\.(.*)/, 402 | result = { 403 | name: name 404 | }; 405 | 406 | for ( let i = 0; i < tracks.length; ++ i ) { 407 | 408 | // 1 is track name 409 | // 2 is track type 410 | const trackData = regexp.exec( tracks[ i ].name ); 411 | 412 | if ( trackData && name === trackData[ 1 ] ) { 413 | 414 | result[ trackData[ 2 ] ] = i; 415 | 416 | } 417 | 418 | } 419 | 420 | return result; 421 | 422 | } 423 | 424 | function getEqualsBonesNames( skeleton, targetSkeleton ) { 425 | 426 | const sourceBones = getBones( skeleton ), 427 | targetBones = getBones( targetSkeleton ), 428 | bones = []; 429 | 430 | search: for ( let i = 0; i < sourceBones.length; i ++ ) { 431 | 432 | const boneName = sourceBones[ i ].name; 433 | 434 | for ( let j = 0; j < targetBones.length; j ++ ) { 435 | 436 | if ( boneName === targetBones[ j ].name ) { 437 | 438 | bones.push( boneName ); 439 | continue search; 440 | 441 | } 442 | 443 | } 444 | 445 | } 446 | 447 | return bones; 448 | 449 | } 450 | 451 | function clone( source ) { 452 | 453 | const sourceLookup = new Map(); 454 | const cloneLookup = new Map(); 455 | const clone = source.clone(); 456 | parallelTraverse( source, clone, function ( sourceNode, clonedNode ) { 457 | 458 | sourceLookup.set( clonedNode, sourceNode ); 459 | cloneLookup.set( sourceNode, clonedNode ); 460 | 461 | } ); 462 | clone.traverse( function ( node ) { 463 | 464 | if ( ! node.isSkinnedMesh ) return; 465 | const clonedMesh = node; 466 | const sourceMesh = sourceLookup.get( node ); 467 | const sourceBones = sourceMesh.skeleton.bones; 468 | clonedMesh.skeleton = sourceMesh.skeleton.clone(); 469 | clonedMesh.bindMatrix.copy( sourceMesh.bindMatrix ); 470 | clonedMesh.skeleton.bones = sourceBones.map( function ( bone ) { 471 | 472 | return cloneLookup.get( bone ); 473 | 474 | } ); 475 | clonedMesh.bind( clonedMesh.skeleton, clonedMesh.bindMatrix ); 476 | 477 | } ); 478 | return clone; 479 | 480 | } 481 | 482 | function parallelTraverse( a, b, callback ) { 483 | 484 | callback( a, b ); 485 | 486 | for ( let i = 0; i < a.children.length; i ++ ) { 487 | 488 | parallelTraverse( a.children[ i ], b.children[ i ], callback ); 489 | 490 | } 491 | 492 | } 493 | 494 | THREE.SkeletonUtils = {}; 495 | THREE.SkeletonUtils.clone = clone; 496 | THREE.SkeletonUtils.findBoneTrackData = findBoneTrackData; 497 | THREE.SkeletonUtils.getBoneByName = getBoneByName; 498 | THREE.SkeletonUtils.getBones = getBones; 499 | THREE.SkeletonUtils.getEqualsBonesNames = getEqualsBonesNames; 500 | THREE.SkeletonUtils.getHelperFromSkeleton = getHelperFromSkeleton; 501 | THREE.SkeletonUtils.getNearestBone = getNearestBone; 502 | THREE.SkeletonUtils.getSkeletonOffsets = getSkeletonOffsets; 503 | THREE.SkeletonUtils.renameBones = renameBones; 504 | THREE.SkeletonUtils.retarget = retarget; 505 | THREE.SkeletonUtils.retargetClip = retargetClip; 506 | 507 | } )(); 508 | -------------------------------------------------------------------------------- /src/model/model.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include "tagged.h" 10 | #include "loot_generator.h" 11 | #include "collision_detector.h" 12 | #include "geom.h" 13 | 14 | namespace model { 15 | 16 | using Dimension = float; 17 | using Coord = Dimension; 18 | 19 | using Point = geom::Point2D; 20 | 21 | struct Size { 22 | Dimension width, height; 23 | }; 24 | 25 | struct Rectangle { 26 | Point position; 27 | Size size; 28 | }; 29 | 30 | struct Offset { 31 | Dimension dx, dy; 32 | }; 33 | 34 | class Road { 35 | struct HorizontalTag { 36 | explicit HorizontalTag() = default; 37 | }; 38 | 39 | struct VerticalTag { 40 | explicit VerticalTag() = default; 41 | }; 42 | 43 | public: 44 | constexpr static HorizontalTag HORIZONTAL{}; 45 | constexpr static VerticalTag VERTICAL{}; 46 | 47 | Road(HorizontalTag, Point start, Coord end_x, float width = 0.4) noexcept; 48 | Road(VerticalTag, Point start, Coord end_y, float width = 0.4) noexcept; 49 | 50 | bool IsOnTheRoad(Point point) const noexcept; 51 | Point BoundToTheRoad(Point point) const noexcept; 52 | 53 | bool operator==(const Road& other) const; 54 | bool IsHorizontal() const noexcept; 55 | bool IsVertical() const noexcept; 56 | 57 | Point GetStart() const noexcept; 58 | Point GetEnd() const noexcept; 59 | 60 | Point start_; 61 | Point end_; 62 | float width_; 63 | }; 64 | 65 | class Building { 66 | public: 67 | explicit Building(const Rectangle& bounds) noexcept 68 | : bounds_{bounds} { 69 | } 70 | 71 | const Rectangle& GetBounds() const noexcept { 72 | return bounds_; 73 | } 74 | 75 | private: 76 | Rectangle bounds_; 77 | }; 78 | 79 | class Office { 80 | public: 81 | using Id = util::Tagged; 82 | 83 | Office(Id id, Point position, Offset offset) noexcept 84 | : id_{std::move(id)} 85 | , position_{position} 86 | , offset_{offset} { 87 | } 88 | 89 | const Id& GetId() const noexcept { 90 | return id_; 91 | } 92 | 93 | Point GetPosition() const noexcept { 94 | return position_; 95 | } 96 | 97 | Offset GetOffset() const noexcept { 98 | return offset_; 99 | } 100 | 101 | private: 102 | Id id_; 103 | Point position_; 104 | Offset offset_; 105 | }; 106 | 107 | struct LootType { 108 | std::string name_; 109 | std::string file_; 110 | std::string type_; 111 | std::optional rotation_; 112 | std::optional color_; 113 | float scale_; 114 | uint64_t value_; 115 | }; 116 | 117 | class Map { 118 | public: 119 | using Id = util::Tagged; 120 | using Roads = std::vector; 121 | using Buildings = std::vector; 122 | using Offices = std::vector; 123 | using LootTypes = std::vector; 124 | 125 | Map(Id id, std::string name) noexcept 126 | : id_(std::move(id)) 127 | , name_(std::move(name)) { 128 | } 129 | 130 | const Id& GetId() const noexcept { 131 | return id_; 132 | } 133 | 134 | const std::string& GetName() const noexcept { 135 | return name_; 136 | } 137 | 138 | const Buildings& GetBuildings() const noexcept { 139 | return buildings_; 140 | } 141 | 142 | const Roads& GetRoads() const noexcept { 143 | return roads_; 144 | } 145 | 146 | const Offices& GetOffices() const noexcept { 147 | return offices_; 148 | } 149 | 150 | const LootTypes& GetLootTypes() const noexcept { 151 | return loot_types_; 152 | } 153 | 154 | void AddRoad(const Road& road) { 155 | roads_.emplace_back(road); 156 | } 157 | 158 | void AddBuilding(const Building& building) { 159 | buildings_.emplace_back(building); 160 | } 161 | 162 | void AddOffice(Office office); 163 | 164 | void AddLootType(const LootType loot) { 165 | loot_types_.emplace_back(loot); 166 | } 167 | 168 | void AddDogSpeed(float speed) { 169 | dog_speed_ = speed; 170 | } 171 | 172 | std::optional GetDogSpeed() const { 173 | return dog_speed_; 174 | } 175 | 176 | void SetBagCapacity(int capacity) { 177 | bag_capacity_ = capacity; 178 | } 179 | 180 | std::optional GetBagCapacity() const { 181 | return bag_capacity_; 182 | } 183 | 184 | private: 185 | using OfficeIdToIndex = std::unordered_map>; 186 | 187 | Id id_; 188 | std::string name_; 189 | Roads roads_; 190 | Buildings buildings_; 191 | 192 | OfficeIdToIndex warehouse_id_to_index_; 193 | Offices offices_; 194 | LootTypes loot_types_; 195 | 196 | std::optional dog_speed_; 197 | std::optional bag_capacity_; 198 | }; 199 | 200 | 201 | struct Speed { 202 | float horizontal, vertical; 203 | 204 | auto operator<=>(const Speed&) const = default; 205 | 206 | bool operator==(const Speed& other) const { 207 | constexpr float epsilon = 0.1f; 208 | 209 | return std::abs(horizontal - other.horizontal) < epsilon && 210 | std::abs(vertical - other.vertical) < epsilon; 211 | } 212 | }; 213 | 214 | enum class Direction : char { 215 | NORTH = 'U', 216 | SOUTH = 'D', 217 | WEST = 'L', 218 | EAST = 'R', 219 | STOP = 'S' 220 | }; 221 | 222 | struct LootState { 223 | int id; 224 | size_t type; 225 | Point position; 226 | uint64_t value_ = 0; 227 | float width = 0.f; 228 | bool is_picked_up = false; 229 | 230 | auto operator<=>(const LootState&) const = default; 231 | }; 232 | 233 | using LootStates = std::vector; 234 | 235 | class LostObjectsBag { 236 | public: 237 | 238 | explicit LostObjectsBag(size_t capacity) : capacity_(capacity) { 239 | lost_objects_.reserve(capacity_); 240 | } 241 | 242 | bool IsFull() const { 243 | return lost_objects_.size() == capacity_; 244 | } 245 | 246 | bool IsEmpty() const { 247 | return lost_objects_.empty(); 248 | } 249 | 250 | bool Add(LootState lost_object) { 251 | if (IsFull()) { 252 | return false; 253 | } 254 | 255 | lost_objects_.push_back(std::move(lost_object)); 256 | return true; 257 | } 258 | 259 | size_t Drop() { 260 | 261 | size_t score = std::accumulate(lost_objects_.begin(), lost_objects_.end(), 0, 262 | [](int sum, const auto& obj) { 263 | return sum + obj.value_; 264 | }); 265 | 266 | lost_objects_.clear(); 267 | return score; 268 | } 269 | 270 | LootStates GetObjects() const { 271 | return lost_objects_; 272 | } 273 | 274 | size_t GetCapacity() { 275 | return capacity_; 276 | } 277 | 278 | private: 279 | LootStates lost_objects_; 280 | size_t capacity_; 281 | }; 282 | 283 | class Dog { 284 | public: 285 | using Id = util::Tagged; 286 | 287 | explicit Dog(std::string_view name) noexcept; 288 | 289 | void SetId(std::uint64_t dog_id); 290 | void SetPosition(const Point& point); 291 | void Move(Direction dir); 292 | void SetSpeed(const Speed& speed); 293 | void SetDefaultSpeed(float speed); 294 | void SetBagCapacity(unsigned int capacity); 295 | 296 | Point GetPosition() const; 297 | Speed GetSpeed() const; 298 | 299 | void SetDirection(Direction dir); 300 | 301 | bool PutToBag(const LootState &object); 302 | 303 | char GetDirection() const; 304 | Direction GetDirectionEnm() const; 305 | Id GetDogId() const; 306 | std::string GetName() const; 307 | LostObjectsBag GetBag() const; 308 | LostObjectsBag& GetBag(); 309 | 310 | Point CalculateNextPosition(std::uint64_t time_delta) const; 311 | 312 | void AccumulateScore(uint64_t score); 313 | uint64_t GetScore() const; 314 | 315 | void ResetRestTime() { 316 | rest_time_ = 0.0; 317 | } 318 | 319 | void IncrementRestTime(double time_delta) { 320 | rest_time_ += time_delta; 321 | } 322 | 323 | double GetRestTime() const { 324 | return rest_time_; 325 | } 326 | 327 | double GetUptime() const { 328 | return uptime_; 329 | } 330 | 331 | void IncrementUpTime(double time_delta) { 332 | uptime_ += time_delta; 333 | } 334 | 335 | private: 336 | std::string name_; 337 | Id id_ {0}; 338 | Point point_{ 0.f, 0.f }; 339 | Speed speed_{ 0.f, 0.f }; 340 | Direction direction_{ Direction::NORTH }; 341 | float default_speed_{1}; 342 | unsigned int bag_capacity_ {3}; 343 | LostObjectsBag bag_{bag_capacity_}; 344 | uint64_t score_ {0}; 345 | double rest_time_ = {0.0}; 346 | double uptime_ = {0.0}; 347 | }; 348 | 349 | class RoadLoader { 350 | public: 351 | using RoadPairs = std::vector>; 352 | explicit RoadLoader(const std::vector& roads); 353 | 354 | std::vector GetDicts() const; 355 | 356 | private: 357 | 358 | static RoadPairs CheckRoads(const std::vector& roads); 359 | static Road MergeRoads(const Road& road1, const Road& road2); 360 | void HandleTheRoads(); 361 | 362 | std::vector vertical_roads_; 363 | std::vector horizontal_roads_; 364 | std::vector new_roads_; 365 | }; 366 | 367 | struct LootGeneratorConfig { 368 | float period_; 369 | float probability_; 370 | }; 371 | 372 | class ItemDogProvider : public collision_detector::ItemGathererProvider { 373 | public: 374 | using Items = std::vector; 375 | using Gatherers = std::vector; 376 | 377 | ItemDogProvider(Items items, Gatherers gatherers) : 378 | items_(std::move(items)), 379 | gatherers_(std::move(gatherers)) {} 380 | 381 | size_t ItemsCount() const override { 382 | return items_.size(); 383 | } 384 | 385 | size_t GatherersCount() const override { 386 | return gatherers_.size(); 387 | } 388 | 389 | collision_detector::Item GetItem(size_t idx) const override { 390 | return items_[idx]; 391 | } 392 | 393 | collision_detector::Gatherer GetGatherer(size_t idx) const override { 394 | return gatherers_[idx]; 395 | } 396 | 397 | private: 398 | Items items_; 399 | Gatherers gatherers_; 400 | }; 401 | 402 | class GameSession { 403 | public: 404 | explicit GameSession(const Map& map, const LootGeneratorConfig& config, double dog_retirement_time); 405 | GameSession() = delete; 406 | 407 | std::uint64_t AddDog(std::shared_ptr dog, bool random_spawn); 408 | Map::Id GetMapId() const; 409 | std::vector> GetDogs() const; 410 | std::vector UpdateGameState(const std::int64_t time_delta); 411 | std::optional TryMoveOnMap(const Point& from, const Point& to) const; 412 | LootStates GetLootStates() const; 413 | 414 | std::shared_ptr GetDog(std::uint64_t id) { 415 | for (auto& dog : dogs_) { 416 | if (*dog->GetDogId() == id) { 417 | return dog; 418 | } 419 | } 420 | return nullptr; 421 | } 422 | 423 | std::uint64_t GetDogIdCounter() const { return dog_id_counter_; } 424 | void SetDogIdCounter(std::uint64_t dog_id_counter) { dog_id_counter_ = dog_id_counter; } 425 | 426 | void EmplaceDogs(std::vector> dogs) { 427 | dogs_ = std::move(dogs); 428 | } 429 | 430 | LootGeneratorConfig GetLootGeneratorConfig() const { 431 | return {loot_generator_.GetConfig().first, loot_generator_.GetConfig().second}; 432 | } 433 | 434 | void SetLootStates(const LootStates& states) { 435 | loot_states_ = states; 436 | } 437 | 438 | private: 439 | void GenerateLootOnMap(unsigned loot_to_gen); 440 | static float GenerateRandomFloat(float min, float max); 441 | static size_t GetRandomSizeT(size_t n); 442 | Point GenerateRandomPosition(); 443 | 444 | const Map map_; 445 | std::vector> dogs_; 446 | std::uint64_t dog_id_counter_{0}; 447 | std::vector roads_; 448 | loot_gen::LootGenerator loot_generator_; 449 | LootStates loot_states_; 450 | double dog_retirement_time_; 451 | }; 452 | 453 | class Game { 454 | public: 455 | using Maps = std::vector; 456 | 457 | void AddMap(Map map); 458 | 459 | const Maps& GetMaps() const noexcept { 460 | return maps_; 461 | } 462 | 463 | const Map* FindMap(const Map::Id& id) const noexcept { 464 | if (auto it = map_id_to_index_.find(id); it != map_id_to_index_.end()) { 465 | return &maps_.at(it->second); 466 | } 467 | return nullptr; 468 | } 469 | 470 | GameSession* GetSession(const model::Map::Id& id) { 471 | if (sessions_.find(id) != sessions_.end()) { 472 | return &sessions_.at(id); 473 | } 474 | 475 | const Map* map = FindMap(id); 476 | if (!map) { 477 | return nullptr; 478 | } 479 | 480 | auto p = sessions_.emplace(id, GameSession{*map, loot_generator_config_, dog_retirement_time_}); 481 | if (!p.second) { 482 | return nullptr; 483 | } 484 | 485 | return &p.first->second; 486 | } 487 | 488 | void SetDefaultDogSpeed(float speed) { 489 | default_dog_speed_ = speed; 490 | } 491 | 492 | void SetLootGeneratorConfig(const LootGeneratorConfig& config) { 493 | loot_generator_config_ = config; 494 | } 495 | 496 | LootGeneratorConfig GetLootGeneratorConfig() const { 497 | return loot_generator_config_; 498 | } 499 | 500 | std::optional GetDefaultDogSpeed() const { 501 | return default_dog_speed_; 502 | } 503 | 504 | void SetDefaultBagCapacity(int capacity) { 505 | default_bag_capacity_ = capacity; 506 | } 507 | 508 | std::optional GetDefaultBagCapacity() const { 509 | return default_bag_capacity_; 510 | } 511 | 512 | void SetDogRetirementTime(double time) { 513 | dog_retirement_time_ = time; 514 | } 515 | 516 | double GetDogRetirementTime() const { 517 | return dog_retirement_time_; 518 | } 519 | 520 | using MapIdHasher = util::TaggedHasher; 521 | using GameSessions = std::unordered_map; 522 | 523 | GameSessions& GetSessions() { 524 | return sessions_; 525 | } 526 | 527 | GameSessions GetSessions() const { 528 | return sessions_; 529 | } 530 | 531 | void SetGameSessions(GameSessions sessions) { 532 | sessions_ = std::move(sessions); 533 | } 534 | 535 | private: 536 | using MapIdToIndex = std::unordered_map; 537 | GameSessions sessions_; 538 | std::vector maps_; 539 | MapIdToIndex map_id_to_index_; 540 | 541 | std::optional default_dog_speed_; 542 | LootGeneratorConfig loot_generator_config_; 543 | std::optional default_bag_capacity_; 544 | // defalut retirement time is 60 seconds 545 | double dog_retirement_time_{60 * 1000.0}; 546 | }; 547 | 548 | } // namespace model 549 | --------------------------------------------------------------------------------