diff --git a/.gitignore b/.gitignore index eb7c5624..0a58a6c1 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,7 @@ doc/latex/ # Configuration file config.yaml + +# Local build script +localbuild.sh +localmake.sh diff --git a/.travis.yml b/.travis.yml index c6a7c218..4d71a2a4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,7 +14,12 @@ env: addons: apt: + sources: + - ubuntu-toolchain-r-test packages: + - gcc-4.9 + - g++-4.9 + - doxygen - doxygen-doc - doxygen-latex @@ -23,6 +28,10 @@ addons: script: - ./install.sh + + - cmake -DCMAKE_BUILD_TYPE=Debug -H. -Bbuild -DCMAKE_CXX_COMPILER=g++-4.9 -DCMAKE_C_COMPILER=gcc-4.9 + - cd ./build + - make after_success: - cd $TRAVIS_BUILD_DIR/doc diff --git a/CHANGELOG.txt b/CHANGELOG.txt new file mode 100644 index 00000000..71089dec --- /dev/null +++ b/CHANGELOG.txt @@ -0,0 +1,33 @@ +0.3.0: Database and ManiaLink support + * Database support (MySQL) + * Includes: maps, players, records, karma, times + * ManiaLink support + * Easy action handling + * Easy-to-use ManiaLink list (UIList) + * Chat command support + * Support for plugin configuration via .yaml-file + * Improved access from plugins to controller features + * Added Jukebox/Map list plugin + * Added Karma plugin + * Karma widget (Eyepiece-style) + * WhoKarma overview + * Added Local Records plugin + * Local Records widget (Eyepiece-style) + * Added Map Widget plugin (Eyepiece-style) + +0.2.0: Plugin system + * Plugin system + * (Dynamically) loading shared object files + * CallBack events + * Access to methods, playerlist and maplist + * Proper CallBack handling + * Easy way to call server methods (via Methods) + +0.1.0: GbxRemote, request/callback handling + * GbxRemote + * Socket connection with the server + * Send requests and receive responses + * Receive callbacks + * Handling of callbacks + * Keeping playerlist up-to-date + * Reading configuration file diff --git a/CMakeLists.txt b/CMakeLists.txt index b0d975d0..177352db 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,11 +7,12 @@ add_definitions(-Wno-deprecated) set (EXECUTABLE_OUTPUT_PATH ${PROJECT_SOURCE_DIR}) link_directories(${PROJECT_SOURCE_DIR}/lib/yaml/build) include_directories("${PROJECT_SOURCE_DIR}/lib/yaml/include") +include_directories("${PROJECT_SOURCE_DIR}/lib/json") set (PROJECT_SOURCE_DIR ${PROJECT_SOURCE_DIR}/src) set (VERSION_MAJOR 0) -set (VERSION_MINOR 2) +set (VERSION_MINOR 3) set (VERSION_PATCH 0) # configure a header file to pass some of the CMake settings @@ -27,4 +28,8 @@ file(GLOB_RECURSE SOURCES src/*.cpp) add_executable(Mania++ ${SOURCES} lib/pugixml/libpugixml.a) -target_link_libraries(Mania++ yaml-cpp ${EXECUTABLE_OUTPUT_PATH}/lib/pugixml/libpugixml.a dl) +target_link_libraries(Mania++ yaml-cpp + ${EXECUTABLE_OUTPUT_PATH}/lib/pugixml/libpugixml.a + dl + mysqlcppconn + curl) diff --git a/README.md b/README.md index e28fa959..dc37d94f 100644 --- a/README.md +++ b/README.md @@ -13,46 +13,51 @@ Mania++ and its sources are available under the [GNU General Public License v3]( ## Tested environments ## * Ubuntu 16.10 (Linux 4.8.0-30), 64-bit with gcc 6.2.0 (development) * Ubuntu 16.04.1 LTS (Linux 4.4.0-31), 32-bit with gcc 5.4.0 (testing) -* Ubuntu 14.04.5 LTS (Linux 4.4.0-51), 64-bit with gcc 4.8.4 (Travis) +* Ubuntu 14.04.5 LTS (Linux 4.4.0-51), 64-bit with gcc 4.9.4 (Travis) * Debian 8.6 (Linux 3.16.0-4), 64-bit with gcc 4.9.2 (production) +* "Bash on Ubuntu 14.04.5 LTS on Windows 10" (Linux 4.4.0-51), 64-bit with gcc 4.9.4 (development) -Mania++ is (currently) not compatible with Windows systems and the ```./install.sh```-script requires a Debian-like system to function (with ```apt-get```). +The ```./install.sh```-script requires a Debian-like system to function (with ```apt-get```). The JSON library requires gcc/g++ v4.9+, the 14.04 LTS only comes with 4.8.4, so you will have to [upgrade the version by hand](http://askubuntu.com/a/456849) and tell the build script that you want to use that version: ```-DCMAKE_CXX_COMPILER=g++-4.9 -DCMAKE_C_COMPILER=gcc-4.9```. ## Requirements ## -* C++11 -* Git _(if you want the latest develop)_ +* Minimum gcc 4.9 / C++11 +* Git * [CMake](https://cmake.org) ## Aims and working points ## * [Be comparable or better than standard PhpRemote](https://themaximum.github.io/mania-pp/comparison.html) -* Create more usable objects (f.e. `Record`?) -* Expand usable objects (`Player` and `Map`) -* Working plugin system - * Plugin interface - * Callback handling - * Access to server, database and ManiaLink -* Database support -* ManiaLink support ## Achieved goals ## * Working GbxRemote module * Send methods and receive responses * Receive callbacks * De-XMLify responses and callbacks -* Create usable objects (f.e. `Player` and `Map`) +* Easy-to-use objects (f.e. `Player` and `Map`) * Working configuration system (YAML) +* Database support (MySQL) +* ManiaLink support + * ManiaLink handler (`UIManager`) + * Easy use of a ManiaLink list (`UIList`) +* Chat command handling +* Working plugin system + * Plugin interface + * Callback handling + * Access to server, database, playerlist, maplist and ManiaLink + * Chat commands + * Receive settings from configuration file + * _Welcome/goodbye, maplist/jukebox, local records, map karma and mapinfo widget_ ## Installing for the first time ## * ```./install.sh``` * Installs Boost (requirement for yaml-cpp) * Installs [yaml-cpp](https://github.com/jbeder/yaml-cpp/) 0.5.3 * Installs [pugixml](https://github.com/zeux/pugixml) 1.8.1 - * Builds the code (via ```build.sh```) + * Installs [JSON for Modern C++](https://github.com/nlohmann/json) 2.0.10 -## Building (updates) ## +## Building ## * ```./build.sh``` * Move ```config.dist.yaml``` to ```config.yaml``` -* Edit the configuration file with the server information +* Edit the configuration file with the server/database information ## Running ## * ```./Mania++``` diff --git a/config.dist.yaml b/config.dist.yaml index 93ae0021..e81eba66 100644 --- a/config.dist.yaml +++ b/config.dist.yaml @@ -5,3 +5,32 @@ config: authentication: username: 'SuperAdmin' password: '***' + + database: + address: 'localhost' + port: 3306 + authentication: + username: '***' + password: '***' + database: 'maniapp' + + program: + checkVersion: true + +plugins: + - 'HelloGoodbye' + - 'Jukebox': + skipMapWhenLeft: true + - 'LocalRecords': + limit: 100 + widgetEntries: 22 + widgetTopCount: 5 + widgetX: 49.2 + widgetY: 28.2 + - 'Karma': + widgetX: 49.2 + widgetY: 39.2 + voteAfterFinishes: 0 + - 'MapWidget': + widgetX: 49.2 + widgetY: 48.2 diff --git a/install.sh b/install.sh index 4b7e00d8..ee55e78c 100755 --- a/install.sh +++ b/install.sh @@ -1,5 +1,5 @@ -# Install Boost and xerces -sudo apt-get -y install libboost-dev +# Install Git, Boost and MySQL +sudo apt-get -y install git libboost-dev libmysqlcppconn-dev libcurl4-openssl-dev # Go to libraries directory cd ./lib/ @@ -18,6 +18,17 @@ make yaml-cpp # Return to lib directory cd ../../ +# Create and go to JSON directory +mkdir json +cd ./json + +# Download version 2.0.10 of the JSON library (remove older/existing version) +rm ./json.hpp +wget https://github.com/nlohmann/json/releases/download/v2.0.10/json.hpp + +# Return to lib directory +cd ../ + # Download version 1.8.1 of the PugiXML library git clone https://github.com/zeux/pugixml.git pugixml/ cd pugixml/ @@ -29,6 +40,3 @@ make # Return to root directory cd ../../ - -# Build project as normal -./build.sh diff --git a/maniapp.sql b/maniapp.sql new file mode 100644 index 00000000..ecca09b3 --- /dev/null +++ b/maniapp.sql @@ -0,0 +1,99 @@ +SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO"; +SET time_zone = "+00:00"; + +-- +-- Database: `maniapp` +-- + +-- -------------------------------------------------------- + +-- +-- Tablestructure for table `karma` +-- + +CREATE TABLE IF NOT EXISTS `karma` ( + `Id` int(11) NOT NULL AUTO_INCREMENT, + `MapId` mediumint(9) NOT NULL DEFAULT '0', + `PlayerId` mediumint(9) NOT NULL DEFAULT '0', + `Score` tinyint(3) NOT NULL DEFAULT '0', + PRIMARY KEY (`Id`), + UNIQUE KEY `PlayerId` (`PlayerId`,`MapId`), + KEY `MapId` (`MapId`), + KEY `Score` (`Score`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8 ; + +-- -------------------------------------------------------- + +-- +-- Tablestructure for table `maps` +-- + +CREATE TABLE IF NOT EXISTS `maps` ( + `Id` mediumint(9) NOT NULL AUTO_INCREMENT, + `Uid` varchar(27) NOT NULL DEFAULT '', + `Name` varchar(100) NOT NULL DEFAULT '', + `Author` varchar(30) NOT NULL DEFAULT '', + `Environment` varchar(10) NOT NULL DEFAULT '', + `RoundsJuke` tinyint(1) NOT NULL DEFAULT '0', + PRIMARY KEY (`Id`), + UNIQUE KEY `Uid` (`Uid`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8 ; + +-- -------------------------------------------------------- + +-- +-- Tablestructure for table `players` +-- + +CREATE TABLE IF NOT EXISTS `players` ( + `Id` mediumint(9) NOT NULL AUTO_INCREMENT, + `Login` varchar(50) NOT NULL DEFAULT '', + `NickName` varchar(100) DEFAULT NULL, + `Nation` varchar(150) NOT NULL DEFAULT '', + `UpdatedAt` datetime NOT NULL DEFAULT '0000-00-00 00:00:00', + `Wins` mediumint(9) NOT NULL DEFAULT '0', + `TimePlayed` int(10) unsigned NOT NULL DEFAULT '0', + PRIMARY KEY (`Id`), + UNIQUE KEY `Login` (`Login`), + KEY `Nation` (`Nation`), + KEY `Wins` (`Wins`), + KEY `UpdatedAt` (`UpdatedAt`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8 ; + +-- -------------------------------------------------------- + +-- +-- Tablestructure for table `records` +-- + +CREATE TABLE IF NOT EXISTS `records` ( + `Id` int(11) NOT NULL AUTO_INCREMENT, + `MapId` mediumint(9) NOT NULL DEFAULT '0', + `PlayerId` mediumint(9) NOT NULL DEFAULT '0', + `Score` int(11) NOT NULL DEFAULT '0', + `Date` datetime NOT NULL DEFAULT '0000-00-00 00:00:00', + `Checkpoints` text NOT NULL, + PRIMARY KEY (`Id`), + UNIQUE KEY `PlayerId` (`PlayerId`,`MapId`), + KEY `MapId` (`MapId`), + KEY `Score` (`Score`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8 ; + +-- -------------------------------------------------------- + +-- +-- Tablestructure for table `times` +-- + +CREATE TABLE IF NOT EXISTS `times` ( + `Id` int(11) NOT NULL AUTO_INCREMENT, + `MapId` mediumint(9) NOT NULL DEFAULT '0', + `PlayerId` mediumint(9) NOT NULL DEFAULT '0', + `Score` int(11) NOT NULL DEFAULT '0', + `Date` int(10) unsigned NOT NULL DEFAULT '0', + `Checkpoints` text NOT NULL, + PRIMARY KEY (`Id`), + KEY `PlayerId` (`PlayerId`,`MapId`), + KEY `MapId` (`MapId`), + KEY `Score` (`Score`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8 ; diff --git a/plugins/HelloGoodbye/src/HelloGoodbyePlugin.cpp b/plugins/HelloGoodbye/src/HelloGoodbyePlugin.cpp index cdb01548..eaa80e82 100644 --- a/plugins/HelloGoodbye/src/HelloGoodbyePlugin.cpp +++ b/plugins/HelloGoodbye/src/HelloGoodbyePlugin.cpp @@ -11,7 +11,7 @@ HelloGoodbyePlugin::HelloGoodbyePlugin() void HelloGoodbyePlugin::Init() { - std::cout << "[ INFO ] Current amount of maps: " << maps->size() << std::endl; + } void HelloGoodbyePlugin::OnPlayerConnect(Player player) @@ -19,12 +19,14 @@ void HelloGoodbyePlugin::OnPlayerConnect(Player player) std::cout << "PLUGIN Player Connected: " << player.Login << "!" << std::endl; std::stringstream chatMessage; - chatMessage << "Player joins: "; + chatMessage << "$39fPlayer joins: "; chatMessage << player.NickName; - chatMessage << " $s$Ladder: "; + chatMessage << " $z$s$39fNation: $fff"; + chatMessage << player.Country; + chatMessage << " $z$s$39fLadder: $fff"; chatMessage << player.LadderRanking; - methods->ChatSendServerMessage(chatMessage.str()); + controller->Server->ChatSendServerMessage(chatMessage.str()); } void HelloGoodbyePlugin::OnPlayerDisconnect(Player player) @@ -33,7 +35,7 @@ void HelloGoodbyePlugin::OnPlayerDisconnect(Player player) std::stringstream chatMessage; chatMessage << player.NickName; - chatMessage << " $s$zhas left the game."; + chatMessage << " $z$s$39fhas left the game."; - methods->ChatSendServerMessage(chatMessage.str()); + controller->Server->ChatSendServerMessage(chatMessage.str()); } diff --git a/plugins/Jukebox/CMakeLists.txt b/plugins/Jukebox/CMakeLists.txt new file mode 100644 index 00000000..f29c5ac5 --- /dev/null +++ b/plugins/Jukebox/CMakeLists.txt @@ -0,0 +1,20 @@ +cmake_minimum_required (VERSION 2.6) +project (JukeboxPlugin) + +add_definitions(-std=c++11) +add_definitions(-Wno-deprecated) +add_definitions(-Wl,--export-dynamic) +add_definitions(-rdynamic) + +set (LIBRARY_OUTPUT_PATH ${PROJECT_SOURCE_DIR}) +link_directories(${PROJECT_SOURCE_DIR}/../../lib/yaml/build) +include_directories("${PROJECT_SOURCE_DIR}/../../lib/yaml/include") +set (PROJECT_SOURCE_DIR ${PROJECT_SOURCE_DIR}/src) + +include_directories("${PROJECT_BINARY_DIR}") +include_directories("${PROJECT_SOURCE_DIR}/../../../src") + +file(GLOB_RECURSE SOURCES src/*.cpp) + +add_library(JukeboxPlugin SHARED ${SOURCES}) +target_link_libraries(JukeboxPlugin yaml-cpp mysqlcppconn) diff --git a/plugins/Jukebox/build.sh b/plugins/Jukebox/build.sh new file mode 100755 index 00000000..8f88dbb9 --- /dev/null +++ b/plugins/Jukebox/build.sh @@ -0,0 +1,4 @@ +cmake -DCMAKE_BUILD_TYPE=Debug -H. -Bbuild + +cd ./build +make diff --git a/plugins/Jukebox/src/JukeboxPlugin.cpp b/plugins/Jukebox/src/JukeboxPlugin.cpp new file mode 100644 index 00000000..461dc193 --- /dev/null +++ b/plugins/Jukebox/src/JukeboxPlugin.cpp @@ -0,0 +1,177 @@ +#include "JukeboxPlugin.h" + +JukeboxPlugin::JukeboxPlugin() +{ + Version = "0.1.0"; + Author = "TheM"; + + EndMatch.push_back([this](std::vector rankings, int winnerTeam) { OnEndMatch(); }); + RegisterCommand("list", [this](Player player, std::vector parameters) { DisplayMapList(player); }); + RegisterCommand("jukebox", [this](Player player, std::vector parameters) { ChatJukebox(player, parameters); }); +} + +void JukeboxPlugin::Init() +{ + loadSettings(); + controller->UI->RegisterEvent("JukeboxMap", ([this](Player player, std::string answer, std::vector entries) { JukeboxMap(player, answer); })); +} + +void JukeboxPlugin::loadSettings() +{ + std::map::iterator skipMapWhenLeftIt = Settings.find("skipMapWhenLeft"); + if(skipMapWhenLeftIt != Settings.end()) + std::istringstream(skipMapWhenLeftIt->second) >> skipMapWhenLeft; +} + +void JukeboxPlugin::OnEndMatch() +{ + if(skipMapWhenLeft) + { + for (std::vector::iterator jukeIt = (jukebox.end() - 1); jukeIt != (jukebox.begin() - 1); --jukeIt) + { + std::map::iterator findPlayer = controller->Players->find(jukeIt->player.Login); + if(findPlayer == controller->Players->end()) + { + std::stringstream skipMessage; + skipMessage << "$fa0Map $fff" << jukeIt->map.Name << "$z$s$fa0 skipped because requester $fff" << jukeIt->player.NickName << "$z$s$fa0 left."; + controller->Server->ChatSendServerMessage(skipMessage.str()); + jukeIt = jukebox.erase(jukeIt); + } + } + } + + if(jukebox.size() > 0) + { + JukeboxItem nextMap = jukebox.at(0); + + std::stringstream nextMessage; + nextMessage << "$fa0The next map will be $fff" << nextMap.map.Name << "$z$s$fa0 as requested by $fff" << nextMap.player.NickName << "$z$s$fa0."; + controller->Server->ChatSendServerMessage(nextMessage.str()); + + controller->Server->ChooseNextMap(nextMap.map.FileName); + jukebox.erase(jukebox.begin()); + } +} + +void JukeboxPlugin::ChatJukebox(Player player, std::vector parameters) +{ + if(parameters.size() > 0) + { + if(parameters.at(0).find("display") != std::string::npos || + parameters.at(0).find("list") != std::string::npos) + { + UIList list = UIList(); + list.Id = "JukeboxList"; + list.Title = "Maps currently in the Jukebox"; + list.IconStyle = "Icons64x64_1"; + list.IconSubstyle = "ToolTree"; + list.Columns.push_back(std::pair("#", 5)); + list.Columns.push_back(std::pair("Map", 30)); + list.Columns.push_back(std::pair("Requested by", 30)); + + for(int itemId = 0; itemId < jukebox.size(); itemId++) + { + JukeboxItem item = jukebox[itemId]; + + std::stringstream index; + index << (itemId + 1); + + std::map row = std::map(); + row.insert(std::pair("#", index.str())); + row.insert(std::pair("Map", item.map.Name)); + row.insert(std::pair("Requested by", item.player.NickName)); + list.Rows.push_back(row); + } + + controller->UI->DisplayList(list, player); + } + else if(parameters.at(0).find("drop") != std::string::npos) + { + for(int jukeId = 0; jukeId < jukebox.size(); jukeId++) + { + if(jukebox.at(jukeId).player.Login == player.Login) + { + std::stringstream dropMessage; + dropMessage << "$fff" << player.NickName << "$z$s$fa0 dropped $fff" << jukebox.at(jukeId).map.Name << "$z$s$fa0 from the jukebox."; + controller->Server->ChatSendServerMessage(dropMessage.str()); + + jukebox.erase((jukebox.begin() + jukeId)); + return; + } + } + } + } + else + { + std::stringstream helpMessage; + helpMessage << "$fff/jukebox$fa0 options: $fffdisplay/list$fa0 (displays current Jukebox list), $fffdrop$fa0 (drops your Jukebox entry)."; + controller->Server->ChatSendServerMessageToLogin(helpMessage.str(), player.Login); + } +} + +void JukeboxPlugin::JukeboxMap(Player player, std::string answer) +{ + int paramBegin = (answer.find('(') + 1); + int paramEnd = answer.find(')'); + std::string mapUid = answer.substr(paramBegin, (paramEnd - paramBegin)); + + for(int jukeId = 0; jukeId < jukebox.size(); jukeId++) + { + if(jukebox.at(jukeId).map.UId == mapUid) + { + controller->Server->ChatSendServerMessageToLogin("$fa0This map has already been added to the jukebox, pick another one.", player.Login); + return; + } + + if(jukebox.at(jukeId).player.Login == player.Login) + { + controller->Server->ChatSendServerMessageToLogin("$fa0You already have a map in the jukebox! Wait till it's been played before adding another.", player.Login); + return; + } + } + + std::map::iterator mapIt = controller->Maps->List.find(mapUid); + if(mapIt != controller->Maps->List.end()) + { + Map map = mapIt->second; + jukebox.push_back({ map, player }); + + std::stringstream nextMessage; + nextMessage << "$fff" << map.Name << "$z$s$fa0 was added to the jukebox by $fff" << player.NickName << "$z$s$fa0."; + controller->Server->ChatSendServerMessage(nextMessage.str()); + } +} + +void JukeboxPlugin::DisplayMapList(Player player) +{ + std::vector> mapsVector; + std::copy(controller->Maps->List.begin(), controller->Maps->List.end(), back_inserter(mapsVector)); + std::sort(mapsVector.begin(), mapsVector.end(), [=](const std::pair& a, const std::pair& b) { return a.second.Id > b.second.Id; }); + + UIList list = UIList(); + list.Id = "MapList"; + list.Title = "Server map list"; + list.IconStyle = "Icons64x64_1"; + list.IconSubstyle = "Browser"; + list.Columns.push_back(std::pair("#", 5)); + list.Columns.push_back(std::pair("Name", 40)); + list.Columns.push_back(std::pair("Author", 20)); + list.Actions.insert(std::pair>("Name", std::pair("JukeboxMap", "UId"))); + + for(int mapId = 0; mapId < mapsVector.size(); mapId++) + { + Map mapInList = mapsVector[mapId].second; + + std::stringstream index; + index << (mapId + 1); + + std::map row = std::map(); + row.insert(std::pair("#", index.str())); + row.insert(std::pair("UId", mapInList.UId)); + row.insert(std::pair("Name", mapInList.Name)); + row.insert(std::pair("Author", mapInList.Author)); + list.Rows.push_back(row); + } + + controller->UI->DisplayList(list, player); +} diff --git a/plugins/Jukebox/src/JukeboxPlugin.h b/plugins/Jukebox/src/JukeboxPlugin.h new file mode 100644 index 00000000..2d174431 --- /dev/null +++ b/plugins/Jukebox/src/JukeboxPlugin.h @@ -0,0 +1,37 @@ +#ifndef KARMAPLUGIN_H_ +#define KARMAPLUGIN_H_ + +#include +#include +#include +#include + +#include "Plugins/Plugin.h" +#include "UI/UIList.h" +#include "Utils/Time.h" + +struct JukeboxItem +{ + Map map; + Player player; +}; + +class JukeboxPlugin : public Plugin +{ +public: + JukeboxPlugin(); + + void Init(); + void OnEndMatch(); + void ChatJukebox(Player player, std::vector parameters); + void JukeboxMap(Player player, std::string answer); + void DisplayMapList(Player player); + +private: + void loadSettings(); + + std::vector jukebox; + bool skipMapWhenLeft = true; +}; + +#endif // KARMAPLUGIN_H_ diff --git a/plugins/Jukebox/src/main.cpp b/plugins/Jukebox/src/main.cpp new file mode 100644 index 00000000..1b5587f7 --- /dev/null +++ b/plugins/Jukebox/src/main.cpp @@ -0,0 +1,25 @@ +#include "JukeboxPlugin.h" + +/** + * Mania++ is a Server Controller for TrackMania 2 servers, written in C++. + * Copyright (C) 2016 Max Klaversma + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +extern "C" Plugin* startPlugin() +{ + JukeboxPlugin* plugin = new JukeboxPlugin(); + return (Plugin*)plugin; +} diff --git a/plugins/Karma/CMakeLists.txt b/plugins/Karma/CMakeLists.txt new file mode 100644 index 00000000..6475e5e9 --- /dev/null +++ b/plugins/Karma/CMakeLists.txt @@ -0,0 +1,20 @@ +cmake_minimum_required (VERSION 2.6) +project (KarmaPlugin) + +add_definitions(-std=c++11) +add_definitions(-Wno-deprecated) +add_definitions(-Wl,--export-dynamic) +add_definitions(-rdynamic) + +set (LIBRARY_OUTPUT_PATH ${PROJECT_SOURCE_DIR}) +link_directories(${PROJECT_SOURCE_DIR}/../../lib/yaml/build) +include_directories("${PROJECT_SOURCE_DIR}/../../lib/yaml/include") +set (PROJECT_SOURCE_DIR ${PROJECT_SOURCE_DIR}/src) + +include_directories("${PROJECT_BINARY_DIR}") +include_directories("${PROJECT_SOURCE_DIR}/../../../src") + +file(GLOB_RECURSE SOURCES src/*.cpp) + +add_library(KarmaPlugin SHARED ${SOURCES}) +target_link_libraries(KarmaPlugin yaml-cpp mysqlcppconn) diff --git a/plugins/Karma/build.sh b/plugins/Karma/build.sh new file mode 100755 index 00000000..8f88dbb9 --- /dev/null +++ b/plugins/Karma/build.sh @@ -0,0 +1,4 @@ +cmake -DCMAKE_BUILD_TYPE=Debug -H. -Bbuild + +cd ./build +make diff --git a/plugins/Karma/src/KarmaPlugin.cpp b/plugins/Karma/src/KarmaPlugin.cpp new file mode 100644 index 00000000..d79623d4 --- /dev/null +++ b/plugins/Karma/src/KarmaPlugin.cpp @@ -0,0 +1,396 @@ +#include "KarmaPlugin.h" + +KarmaPlugin::KarmaPlugin() +{ + Version = "0.1.1"; + Author = "TheM"; + karma = MapKarma(); + + BeginMap.push_back([this](Map map) { OnBeginMap(map); }); + PlayerConnect.push_back([this](Player player) { OnPlayerConnect(player); }); + PlayerChat.push_back([this](Player player, std::string text) { OnPlayerChat(player, text); }); + + RegisterCommand("karma", [this](Player player, std::vector parameters) { DisplayCurrentKarma(player); }); + RegisterCommand("whokarma", [this](Player player, std::vector parameters) { DisplayWhoKarma(player); }); + RegisterCommand("++", [this](Player player, std::vector parameters) { VotePositive(player); }); + RegisterCommand("--", [this](Player player, std::vector parameters) { VoteNegative(player); }); +} + +void KarmaPlugin::Init() +{ + loadSettings(); + widget = KarmaWidget(controller->UI); + widget.WidgetX = widgetX; + widget.WidgetY = widgetY; + controller->UI->RegisterEvent(widget.ActionId, ([this](Player player, std::string answer, std::vector entries) { DisplayWhoKarma(player); })); + controller->UI->RegisterEvent(widget.PositiveAction, ([this](Player player, std::string answer, std::vector entries) { VotePositive(player); })); + controller->UI->RegisterEvent(widget.NegativeAction, ([this](Player player, std::string answer, std::vector entries) { VoteNegative(player); })); + + retrieveVotes(*controller->Maps->Current); + karma.Calculate(votes); + displayToAll(); +} + +void KarmaPlugin::loadSettings() +{ + std::map::iterator widgetXIt = Settings.find("widgetX"); + if(widgetXIt != Settings.end()) + widgetX = atof(widgetXIt->second.c_str()); + + std::map::iterator widgetYIt = Settings.find("widgetY"); + if(widgetYIt != Settings.end()) + widgetY = atof(widgetYIt->second.c_str()); + + std::map::iterator voteAfterFinishesIt = Settings.find("voteAfterFinishes"); + if(voteAfterFinishesIt != Settings.end()) + voteAfterFinishes = atoi(voteAfterFinishesIt->second.c_str()); +} + +void KarmaPlugin::OnBeginMap(Map map) +{ + retrieveVotes(*controller->Maps->Current); + karma.Calculate(votes); + displayToAll(); +} + +void KarmaPlugin::OnPlayerConnect(Player player) +{ + int personalVote = -1; + std::map::iterator voteIt = votes.find(player.Login); + if(voteIt != votes.end()) + personalVote = voteIt->second; + + if(!widget.DisplayToPlayer(player, &karma, personalVote)) + Logging::PrintError(controller->Server->GetCurrentError()); +} + +void KarmaPlugin::DisplayCurrentKarma(Player player) +{ + std::stringstream chatMessage; + chatMessage << "$ff0Current map karma: $fff" << (karma.PlusVotes - karma.MinVotes) << "$ff0 ["; + chatMessage << "$fff" << (karma.PlusVotes + karma.MinVotes) << "$ff0 votes,"; + chatMessage << " ++: $fff" << karma.PlusVotes << "$ff0 ($fff" << karma.Percentage << "%$ff0),"; + chatMessage << " --: $fff" << karma.MinVotes << "$ff0 ($fff" << karma.MinPercentage << "%$ff0)]"; + + int personalVote = -1; + std::map::iterator voteIt = votes.find(player.Login); + if(voteIt != votes.end()) + personalVote = voteIt->second; + + chatMessage << " {Your vote: $fff"; + switch(personalVote) + { + case 1: + chatMessage << "++"; + break; + case 0: + chatMessage << "--"; + break; + default: + chatMessage << "none"; + break; + } + chatMessage << "$ff0}"; + + controller->Server->ChatSendServerMessageToLogin(chatMessage.str(), player.Login); +} + +void KarmaPlugin::OnPlayerChat(Player player, std::string text) +{ + if(text.find("++") == 0) + { + VotePositive(player); + } + else if(text.find("--") == 0) + { + VoteNegative(player); + } +} + +void KarmaPlugin::VotePositive(Player player) +{ + std::stringstream chatMessage; + chatMessage << "$ff0"; + + if(voteAfterFinishes > 0) + { + int timesDriven = retrieveTimesDriven(player); + if(timesDriven < voteAfterFinishes) + { + chatMessage << "You can only vote after $fff" << voteAfterFinishes << "$ff0 finishes (you have: $fff" << timesDriven << "$ff0)."; + controller->Server->ChatSendServerMessageToLogin(chatMessage.str(), player.Login); + return; + } + } + + int personalVote = -1; + std::map::iterator voteIt = votes.find(player.Login); + if(voteIt != votes.end()) + personalVote = voteIt->second; + + if(personalVote != 1) + { + sql::PreparedStatement* pstmt; + try + { + pstmt = controller->Database->prepareStatement("INSERT INTO `karma` (`MapId`, `PlayerId`, `Score`) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE `Score` = VALUES(`Score`)"); + pstmt->setInt(1, controller->Maps->Current->Id); + pstmt->setInt(2, player.Id); + pstmt->setInt(3, 1); + pstmt->executeQuery(); + + retrieveVotes(*controller->Maps->Current); + karma.Calculate(votes); + displayToAll(); + + if(personalVote == -1) + chatMessage << "Successfully voted $fff++$ff0 on this map!"; + else + chatMessage << "Changed your $fff--$ff0 vote to a $fff++$ff0 vote on this map!"; + } + catch(sql::SQLException &e) + { + std::cout << "Failed to save karma for " << player.Login << " on '" << controller->Maps->Current->Name << "' ..." << std::endl; + Logging::PrintError(e.getErrorCode(), e.what()); + } + + if(pstmt != NULL) + { + delete pstmt; + pstmt = NULL; + } + } + else + { + chatMessage << "You already voted $fff++$ff0 on this map!"; + } + + controller->Server->ChatSendServerMessageToLogin(chatMessage.str(), player.Login); +} + +void KarmaPlugin::VoteNegative(Player player) +{ + std::stringstream chatMessage; + chatMessage << "$ff0"; + + if(voteAfterFinishes > 0) + { + int timesDriven = retrieveTimesDriven(player); + if(timesDriven < voteAfterFinishes) + { + chatMessage << "You can only vote after $fff" << voteAfterFinishes << "$ff0 finishes (you have: $fff" << timesDriven << "$ff0)."; + controller->Server->ChatSendServerMessageToLogin(chatMessage.str(), player.Login); + return; + } + } + + int personalVote = -1; + std::map::iterator voteIt = votes.find(player.Login); + if(voteIt != votes.end()) + personalVote = voteIt->second; + + if(personalVote != 0) + { + sql::PreparedStatement* pstmt; + try + { + pstmt = controller->Database->prepareStatement("INSERT INTO `karma` (`MapId`, `PlayerId`, `Score`) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE `Score` = VALUES(`Score`)"); + pstmt->setInt(1, controller->Maps->Current->Id); + pstmt->setInt(2, player.Id); + pstmt->setInt(3, 0); + pstmt->executeQuery(); + + retrieveVotes(*controller->Maps->Current); + karma.Calculate(votes); + displayToAll(); + + if(personalVote == -1) + chatMessage << "Successfully voted $fff--$ff0 on this map!"; + else + chatMessage << "Changed your $fff++$ff0 vote to a $fff--$ff0 vote on this map!"; + } + catch(sql::SQLException &e) + { + std::cout << "Failed to save karma for " << player.Login << " on '" << controller->Maps->Current->Name << "' ..." << std::endl; + Logging::PrintError(e.getErrorCode(), e.what()); + } + + if(pstmt != NULL) + { + delete pstmt; + pstmt = NULL; + } + } + else + { + chatMessage << "You already voted $fff--$ff0 on this map!"; + } + + controller->Server->ChatSendServerMessageToLogin(chatMessage.str(), player.Login); +} + +void KarmaPlugin::DisplayWhoKarma(Player player) +{ + std::vector> votesVector; + std::copy(votes.begin(), votes.end(), back_inserter(votesVector)); + std::sort(votesVector.begin(), votesVector.end(), [=](const std::pair& a, const std::pair& b) { return a.second > b.second; }); + + UIList list = UIList(); + list.Id = "WhoKarma"; + list.Title = "WhoKarma for: $z$s$fff" + controller->Maps->Current->Name; + list.IconStyle = "Icons128x128_1"; + list.IconSubstyle = "CustomStars"; + list.Columns.push_back(std::pair("#", 5)); + list.Columns.push_back(std::pair("Player", 40)); + list.Columns.push_back(std::pair("Vote", 20)); + + for(int voteId = 0; voteId < votesVector.size(); voteId++) + { + std::string vote = "++"; + if(votesVector[voteId].second == -1) + vote = "--"; + + std::string playerName = votesVector[voteId].first; + sql::PreparedStatement* pstmt; + sql::ResultSet* result; + try + { + pstmt = controller->Database->prepareStatement("SELECT * FROM `players` WHERE `Login` = ?"); + pstmt->setString(1, playerName); + result = pstmt->executeQuery(); + if(result->next()) + { + playerName = result->getString("NickName"); + } + } + catch(sql::SQLException &e) { } + + if(pstmt != NULL) + delete pstmt; pstmt = NULL; + + if(result != NULL) + delete result; result = NULL; + + std::stringstream index; + index << (voteId + 1); + + std::map row = std::map(); + row.insert(std::pair("#", index.str())); + row.insert(std::pair("Player", playerName)); + row.insert(std::pair("Vote", vote)); + list.Rows.push_back(row); + } + + controller->UI->DisplayList(list, player); +} + +void KarmaPlugin::displayToAll() +{ + for(std::map::iterator player = controller->Players->begin(); player != controller->Players->end(); ++player) + { + int personalVote = -1; + std::map::iterator voteIt = votes.find(player->second.Login); + if(voteIt != votes.end()) + personalVote = voteIt->second; + + if(!widget.DisplayToPlayer(player->second, &karma, personalVote)) + Logging::PrintError(controller->Server->GetCurrentError()); + } +} + +void KarmaPlugin::retrieveVotes(Map map) +{ + votes = std::map(); + + sql::PreparedStatement* pstmt; + sql::ResultSet* result; + try + { + pstmt = controller->Database->prepareStatement("SELECT * FROM `karma` WHERE `MapId` = ?"); + pstmt->setInt(1, map.Id); + result = pstmt->executeQuery(); + + while(result->next()) + { + delete pstmt; pstmt = NULL; + sql::ResultSet* playerResult; + try + { + pstmt = controller->Database->prepareStatement("SELECT * FROM `players` WHERE `Id` = ?"); + pstmt->setInt(1, result->getInt("PlayerId")); + playerResult = pstmt->executeQuery(); + playerResult->next(); + + std::string login = playerResult->getString("Login"); + int score = result->getInt("Score"); + + votes.insert(std::pair(login, score)); + } + catch(sql::InvalidArgumentException &e) + { + // Player cannot be found in the database. + // Just skip the karma vote. + } + + if(playerResult != NULL) + { + delete playerResult; + playerResult = NULL; + } + } + } + catch(sql::SQLException &e) + { + std::cout << "Failed to retrieve karma votes for '" << map.Name << "' ..." << std::endl; + Logging::PrintError(e.getErrorCode(), e.what()); + } + + if(pstmt != NULL) + { + delete pstmt; + pstmt = NULL; + } + + if(result != NULL) + { + delete result; + result = NULL; + } +} + +int KarmaPlugin::retrieveTimesDriven(Player player) +{ + int timesDriven = 0; + + sql::PreparedStatement* pstmt; + sql::ResultSet* result; + try + { + pstmt = controller->Database->prepareStatement("SELECT COUNT(*) AS `timesDriven` FROM `times` WHERE `PlayerId` = ? AND `MapId` = ?"); + pstmt->setInt(1, player.Id); + pstmt->setInt(2, controller->Maps->Current->Id); + result = pstmt->executeQuery(); + result->next(); + + timesDriven = result->getInt("timesDriven"); + } + catch(sql::SQLException &e) + { + std::cout << "Failed to retrieve # times driven for player " << player.Login << " on map '" << controller->Maps->Current->Name << "' ..." << std::endl; + Logging::PrintError(e.getErrorCode(), e.what()); + } + + if(pstmt != NULL) + { + delete pstmt; + pstmt = NULL; + } + + if(result != NULL) + { + delete result; + result = NULL; + } + + return timesDriven; +} diff --git a/plugins/Karma/src/KarmaPlugin.h b/plugins/Karma/src/KarmaPlugin.h new file mode 100644 index 00000000..beccda63 --- /dev/null +++ b/plugins/Karma/src/KarmaPlugin.h @@ -0,0 +1,47 @@ +#ifndef KARMAPLUGIN_H_ +#define KARMAPLUGIN_H_ + +#include +#include +#include + +#include "Plugins/Plugin.h" +#include "UI/UIList.h" +#include "Utils/Time.h" + +#include "Objects/MapKarma.h" +#include "Widget/KarmaWidget.h" + +class KarmaPlugin : public Plugin +{ +public: + KarmaPlugin(); + + void Init(); + void OnBeginMap(Map map); + void OnPlayerConnect(Player player); + void OnPlayerChat(Player player, std::string text); + + void DisplayCurrentKarma(Player player); + void VoteNegative(Player player); + void VotePositive(Player player); + void DisplayWhoKarma(Player player); + +private: + std::map votes; + MapKarma karma; + + KarmaWidget widget; + + double widgetX = 49.2; + double widgetY = 39.2; + int voteAfterFinishes = 0; + + void loadSettings(); + void displayToAll(); + void retrieveVotes(Map map); + + int retrieveTimesDriven(Player player); +}; + +#endif // KARMAPLUGIN_H_ diff --git a/plugins/Karma/src/Objects/MapKarma.h b/plugins/Karma/src/Objects/MapKarma.h new file mode 100644 index 00000000..8642c73e --- /dev/null +++ b/plugins/Karma/src/Objects/MapKarma.h @@ -0,0 +1,36 @@ +#ifndef MAPKARMA_H_ +#define MAPKARMA_H_ + +struct MapKarma +{ +public: + int MinVotes = 0; + int PlusVotes = 0; + double Percentage = 0.0; + double MinPercentage = 0.0; + + void Calculate(std::map votes) + { + MinVotes = 0; + PlusVotes = 0; + Percentage = 0.0; + MinPercentage = 0.0; + + for(std::map::iterator vote = votes.begin(); vote != votes.end(); ++vote) + { + if(vote->second == 1) + { + PlusVotes++; + } + else + { + MinVotes++; + } + } + + Percentage = std::round(((double)PlusVotes / (double)(PlusVotes + MinVotes)) * 1000) / 10; + MinPercentage = std::round((100 - Percentage) * 10) / 10; + } +}; + +#endif // MAPKARMA_H_ diff --git a/plugins/Karma/src/Widget/KarmaWidget.cpp b/plugins/Karma/src/Widget/KarmaWidget.cpp new file mode 100644 index 00000000..f30bd005 --- /dev/null +++ b/plugins/Karma/src/Widget/KarmaWidget.cpp @@ -0,0 +1,142 @@ +#include "KarmaWidget.h" + +KarmaWidget::KarmaWidget() +{ + +} + +KarmaWidget::KarmaWidget(UIManager* uiManager) +{ + ui = uiManager; + + frame = UIFrame(); + frame.ManiaLinkId = manialinkId; + frame.Timeout = 0; + frame.CloseOnClick = false; +} + +bool KarmaWidget::DisplayToPlayer(Player player, MapKarma* karma, int personalScore) +{ + double widgetWidth = 15.5; + double widgetHeight = 11; + double columnHeight = (widgetHeight - 3.1); + double backgroundWidth = (widgetWidth - 0.2); + double backgroundHeight = (widgetHeight - 0.2); + double borderWidth = (widgetWidth + 0.4); + double borderHeight = (widgetHeight + 0.6); + + double left_IconX = 0.6; + double left_IconY = 0; + double left_TitleX = 3.2; + double left_TitleY = -0.65; + std::string left_TitleHalign = "left"; + double left_ImageOpenX = -0.3; + std::string left_ImageOpen = "http://static.undef.name/ingame/records-eyepiece/edge-open-ld-dark.png"; + + double right_IconX = 12.5; + double right_IconY = 0; + double right_TitleX = 12.4; + double right_TitleY = -0.65; + std::string right_TitleHalign = "right"; + double right_ImageOpenX = 12.2; + std::string right_ImageOpen = "http://static.undef.name/ingame/records-eyepiece/edge-open-rd-dark.png"; + + std::string backgroundColor = "3342"; + std::string backgroundFocus = "09F6"; + + std::string backgroundStyle = "Bgs1"; + std::string backgroundSubstyle = "BgTitleGlow"; + std::string borderStyle = "Bgs1"; + std::string borderSubstyle = "BgTitleShadow"; + + double imageOpenX = (WidgetX < 0) ? (right_ImageOpenX + (widgetWidth - 15.5)) : left_ImageOpenX; + double imageOpenY = -(widgetHeight - 3.18); + std::string imageOpen = (WidgetX < 0) ? right_ImageOpen : left_ImageOpen; + + double titleBackgroundWidth = (widgetWidth - 0.8); + std::string titleStyle = "BgsPlayerCard"; + std::string titleSubstyle = "BgRacePlayerName"; + double titleX = (WidgetX < 0) ? (right_TitleX + (widgetWidth - 15.5)) : left_TitleX; + double titleY = (WidgetX < 0) ? right_TitleY : left_TitleY; + std::string titleHalign = (WidgetX < 0) ? right_TitleHalign : left_TitleHalign; + + double iconX = (WidgetX < 0) ? (right_IconX + (widgetWidth - 15.5)) : left_IconX; + double iconY = (WidgetX < 0) ? right_IconY : left_IconY; + std::string iconStyle = "Icons128x128_1"; + std::string iconSubstyle = "CustomStars"; + + std::string textColor = "FFFF"; + + std::stringstream widget; + widget << " "; + + widget << "