├── .gitignore ├── .gitmodules ├── .travis.yml ├── CMakeLists.txt ├── LICENSE ├── README.md ├── a_mysql.inc.in ├── appveyor.yml ├── cmake └── FindMySQLCAPI.cmake ├── example_scripts ├── INFO.txt ├── login_system-cache.pwn └── login_system-orm.pwn ├── pawn.json ├── src ├── CCallback.cpp ├── CCallback.hpp ├── CConnection.cpp ├── CConnection.hpp ├── CDispatcher.cpp ├── CDispatcher.hpp ├── CError.hpp ├── CHandle.cpp ├── CHandle.hpp ├── CLog.cpp ├── CLog.hpp ├── CMakeLists.txt ├── COptions.cpp ├── COptions.hpp ├── COrm.cpp ├── COrm.hpp ├── CQuery.cpp ├── CQuery.hpp ├── CResult.cpp ├── CResult.hpp ├── CSingleton.hpp ├── main.cpp ├── misc.hpp ├── mysql.hpp ├── natives.cpp ├── natives.hpp ├── plugin.def ├── sdk.hpp ├── types.hpp └── version.hpp.in └── tests ├── speed_test.pwn ├── test_data ├── mysql-invalid.ini ├── mysql-invalid1.ini ├── mysql-invalid2.ini ├── mysql-invalid3.ini ├── mysql-invalid4.ini ├── mysql-invalid5.ini ├── mysql.ini └── scriptfiles │ └── test.sql └── unit_test.pwn /.gitignore: -------------------------------------------------------------------------------- 1 | [Bb]in/ 2 | build/ 3 | [Rr]elease/ 4 | [Dd]ebug/ 5 | 6 | #Linux 7 | *.o 8 | *.so 9 | *~ 10 | *.swp 11 | __*/ 12 | .* 13 | 14 | #Visual Studio 15 | *.sdf 16 | *.opensdf 17 | *.suo 18 | *.psess 19 | *.vsp 20 | *.user 21 | 22 | #Merge files 23 | *.orig 24 | 25 | #archives 26 | *.rar 27 | *.zip 28 | *.tar.gz 29 | 30 | #MacOS 31 | .DS_Store 32 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "libs/sdk"] 2 | path = libs/sdk 3 | url = https://github.com/maddinat0r/samp-plugin-sdk.git 4 | [submodule "libs/cmake"] 5 | path = libs/cmake 6 | url = https://github.com/Zeex/cmake-modules.git 7 | [submodule "tests/include/amx"] 8 | path = tests/include/amx 9 | url = https://github.com/Zeex/amx_assembly.git 10 | [submodule "libs/fmt"] 11 | path = libs/fmt 12 | url = https://github.com/fmtlib/fmt.git 13 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: cpp 2 | sudo: required 3 | dist: trusty 4 | 5 | env: 6 | global: 7 | - SAMP_SERVER_ROOT=$TRAVIS_BUILD_DIR/samp03 8 | - PATH=$TRAVIS_BUILD_DIR/samp-server-cli:$PATH 9 | matrix: 10 | - STATIC_BUILD=ON BUILD_TYPE=Release 11 | - STATIC_BUILD=ON BUILD_TYPE=Debug 12 | - STATIC_BUILD=OFF BUILD_TYPE=Release 13 | - STATIC_BUILD=OFF BUILD_TYPE=Debug 14 | 15 | addons: 16 | apt: 17 | packages: 18 | - mysql-server-5.6 19 | - mysql-client-core-5.6 20 | - mysql-client-5.6 21 | - cmake 22 | - g++-multilib 23 | 24 | cache: 25 | directories: 26 | - mysql-connector-c-6.1.6-src 27 | 28 | before_install: 29 | - if [ ! -f "mysql-connector-c-6.1.6-src/CMakeLists.txt" ]; then wget "http://dev.mysql.com/get/Downloads/Connector-C/mysql-connector-c-6.1.6-src.tar.gz"; fi 30 | - if [ ! -f "mysql-connector-c-6.1.6-src/CMakeLists.txt" ]; then tar xzf mysql-connector-c-6.1.6-src.tar.gz; fi 31 | 32 | - wget https://dl.bintray.com/boostorg/release/1.64.0/source/boost_1_64_0.tar.gz 33 | - tar xzf boost_1_64_0.tar.gz 34 | 35 | - wget http://files.sa-mp.com/samp037svr_R2-1.tar.gz 36 | - tar xzf samp037svr_R2-1.tar.gz 37 | 38 | - wget http://files.sa-mp.com/samp037_svr_R2-1-1_win32.zip 39 | - unzip samp037_svr_R2-1-1_win32.zip -d $SAMP_SERVER_ROOT pawno/include/* 40 | 41 | - git clone https://github.com/Zeex/samp-server-cli.git 42 | 43 | - wget https://github.com/Zeex/pawn/releases/download/20150531/pawnc-3.10.20150531-linux.tar.gz 44 | - sudo tar xvf pawnc-3.10.20150531-linux.tar.gz --strip-components=1 -C /usr/local 45 | - sudo ldconfig 46 | 47 | install: 48 | - cd mysql-connector-c-6.1.6-src/ 49 | - cmake . -DCMAKE_BUILD_TYPE=${BUILD_TYPE} -DCMAKE_CXX_FLAGS=-m32 -DCMAKE_C_FLAGS=-m32 -DCMAKE_INSTALL_PREFIX=/usr -DINSTALL_LAYOUT=DEB > /dev/null 50 | - make -j2 > /dev/null 51 | - sudo make install > /dev/null 52 | - sudo ldconfig 53 | - cd .. 54 | 55 | before_script: 56 | - mysql -u root -e "CREATE DATABASE IF NOT EXISTS test;" 57 | - mysql -u root -e "CREATE USER 'tester'@'127.0.0.1' IDENTIFIED BY '1234';" 58 | - mysql -u root -e "GRANT ALL PRIVILEGES ON *.* TO 'tester'@'127.0.0.1' WITH GRANT OPTION;" 59 | 60 | - cmake . -DCMAKE_INSTALL_PREFIX=${SAMP_SERVER_ROOT} -DCMAKE_BUILD_TYPE=${BUILD_TYPE} -DBOOST_ROOT=boost_1_64_0 -DBUILD_TESTS=ON -DBUILD_STATIC=${STATIC_BUILD} 61 | 62 | script: 63 | - make -j2 64 | - make install 65 | # tests 66 | - pawncc tests/unit_test.pwn -d3 -Z+ -\; -\( -i${SAMP_SERVER_ROOT}/pawno/include -itests/include -ounit_test 67 | - cp -R tests/test_data/* ${SAMP_SERVER_ROOT} 68 | - cp *.amx ${SAMP_SERVER_ROOT}/gamemodes 69 | - samp-server-cli -o -g unit_test -d mysql.so -T 60 70 | 71 | after_script: cat ${SAMP_SERVER_ROOT}/logs/plugins/mysql.log 72 | 73 | before_cache: 74 | - rm -f mysql-connector-c-6.1.6-src/Docs/INFO_BIN 75 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.1) 2 | project(mysql) 3 | 4 | set(MYSQL_PLUGIN_VERSION "R41-4") 5 | 6 | option (BUILD_TESTS "Build tests" OFF) 7 | 8 | list(APPEND CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/cmake") 9 | list(APPEND CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/libs/cmake") 10 | 11 | if(UNIX) 12 | #force 32bit compilation and hide non-exported symbols 13 | set(CMAKE_CXX_FLAGS "-m32 -fvisibility=hidden") 14 | set(CMAKE_C_FLAGS "-m32 -fvisibility=hidden") 15 | set_property(GLOBAL PROPERTY FIND_LIBRARY_USE_LIB64_PATHS OFF) 16 | endif() 17 | 18 | 19 | find_package(MySQLCAPI REQUIRED) 20 | 21 | set(Boost_USE_STATIC_LIBS ON) 22 | find_package(Boost 1.57 QUIET) 23 | if(NOT Boost_FOUND) 24 | set(BOOST_ROOT "${BOOST_ROOT}" CACHE PATH "Set the Boost root folder.") 25 | mark_as_advanced(CLEAR BOOST_ROOT) 26 | message(FATAL_ERROR "Could NOT find Boost; specify additional path.") 27 | endif() 28 | mark_as_advanced(BOOST_ROOT Boost_DIR) 29 | 30 | add_subdirectory(libs/fmt) 31 | mark_as_advanced(FMT_DOC FMT_INSTALL FMT_PEDANTIC FMT_TEST FMT_USE_CPP11) 32 | 33 | find_package(log-core REQUIRED CONFIG) 34 | 35 | 36 | add_subdirectory(src) 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016, BlueG 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 5 | 6 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 7 | 8 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 9 | 10 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 11 | 12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MySQL Plugin for San Andreas Multiplayer (SA:MP) 2 | 3 | | Travis CI | AppVeyor | Total downloads | Latest release | 4 | | :---: | :---: | :---: | :---: | 5 | | [![Build Status](https://travis-ci.org/pBlueG/SA-MP-MySQL.svg?branch=master)](https://travis-ci.org/pBlueG/SA-MP-MySQL) | [![Build status](https://ci.appveyor.com/api/projects/status/xssdxu7wp8l3q2mk/branch/master?svg=true)](https://ci.appveyor.com/project/maddinat0r/sa-mp-mysql/branch/master) | [![All Releases](https://img.shields.io/github/downloads/pBlueG/SA-MP-MySQL/total.svg?maxAge=86400)](https://github.com/pBlueG/SA-MP-MySQL/releases) | [![latest release](https://img.shields.io/github/release/pBlueG/SA-MP-MySQL.svg?maxAge=86400)](https://github.com/pBlueG/SA-MP-MySQL/releases)
[![Github Releases](https://img.shields.io/github/downloads/pBlueG/SA-MP-MySQL/latest/total.svg?maxAge=86400)](https://github.com/pBlueG/SA-MP-MySQL/releases) | 6 | ------------------------------------------------- 7 | *The best and most famous MySQL plugin for SA:MP out there!* 8 | 9 | **This plugin allows you to use MySQL in PAWN. It's currently being developed by [maddinat0r](https://github.com/maddinat0r).** 10 | 11 | How to install 12 | -------------- 13 | 1. Extract the content of the downloaded archive into the root directory of your SA-MP server. 14 | 2. Edit the server configuration (*server.cfg*) as follows: 15 | - Windows: `plugins mysql` 16 | - Linux: `plugins mysql.so` 17 | 18 | F.A.Q. 19 | ------ 20 | Q: *I get a* `version GLIBCXX_3.4.15' not found` *error (or similar). How can I solve this?* 21 | A: Update your system. If that still didn't work, you'll need to upgrade your Linux distribution to a version which provides the gcc 4.8 (or higher) compiler. For example, if you're on CentOS 6, which only has gcc 4.4, you'll need to upgrade to CentOS 7. 22 | 23 | Q: *I get a* `Failed (libmysqlclient_r.so.18: cannot open shared object file: No such file or directory)` *error, how do I fix this?* 24 | A: You don't have the MySQL client library installed. Install it through your package manager. Make sure you install the 32bit (i386, i686, etc) library, or else the plugin won't run. 25 | 26 | Q: *I can't install the required libmysqlclient library on my Linux distribution. What do I do now?* 27 | A: Use the `mysql_static.so` plugin file. It's statically linked to the libmysqlclient library. 28 | 29 | Q: *I get a* `Failed (plugins/mysql.so: symbol __cxa_pure_virtual, version libmysqlclient_18[...]` *error, is there any way to fix it?* 30 | A: That likely means that you are using a 64bit system and thus a 64bit libmysqlclient library. You'll have to either install the 32bit version of the MySQL client package or use the statically linked version of the plugin, the `mysql_static.so`. 31 | 32 | Q: *The plugin fails to load on Windows, how can I fix this?* 33 | A: You have to install these Microsoft C++ redistributables. You'll need the x86/32bit downloads. 34 | - [2010 (x86)](http://www.microsoft.com/en-us/download/details.aspx?id=5555) 35 | - [2010 SP1 (x86)](http://www.microsoft.com/en-us/download/details.aspx?id=8328) 36 | - [2012 (x86)](http://www.microsoft.com/en-us/download/details.aspx?id=30679) 37 | - [2015 (x86)](https://www.microsoft.com/en-US/download/details.aspx?id=48145) 38 | 39 | Q: *I'm not on Windows 10 and the plugin still fails to load after installing all the redistributables. Is there a solution for this?* 40 | A: Download the [universal Windows CRT](https://www.microsoft.com/en-US/download/details.aspx?id=48234). Requirements for this: 41 | - Windows 8.1 and Windows Server 2012 R2: [KB2919355](https://support.microsoft.com/en-us/kb/2919355) 42 | - Windows 7 and Windows Server 2008 R2: [Service Pack 1](https://support.microsoft.com/en-us/kb/976932) 43 | - Windows Vista and Windows Server 2008: [Service Pack 2](https://support.microsoft.com/en-us/kb/948465) 44 | 45 | Q: *I get a ton of debug messages regarding connections even though I'm calling* `mysql_connect` *only once, why is that so?* 46 | A: That's because the plugin uses multiple direct database connections per connection handle. The number of direct connections (and thus the number of those log messages) is `2 + pool_size`. 47 | 48 | Build instruction 49 | --------------- 50 | *Note*: The plugin has to be a 32-bit library; that means all required libraries have to be compiled in 32-bit and the compiler has to support 32-bit. 51 | #### Windows 52 | 1. install a C++ compiler of your choice 53 | 2. install the [MySQL C Connector (version 6.1.6)](http://dev.mysql.com/downloads/connector/c/) 54 | 3. install the [Boost libraries (version 1.57 or higher)](http://www.boost.org/users/download/) 55 | 4. install [CMake](http://www.cmake.org/) 56 | 5. clone this repository 57 | 6. create a folder named `build` and execute CMake in there 58 | 7. build the generated project files with your C++ compiler 59 | 60 | #### Linux 61 | 1. install a C++ compiler of your choice 62 | 2. install the appropriate MySQL client (version 5.5 or higher) through your package manager 63 | 3. install the [Boost libraries (version 1.57 or higher)](http://www.boost.org/users/download/) 64 | 4. install [CMake](http://www.cmake.org/) 65 | 5. clone this repository 66 | 6. create a folder named `build` and execute CMake in there (`mkdir build && cd build && cmake ..`) 67 | 7. build the generated project files with your C++ compiler 68 | 69 | Thanks to 70 | --------- 71 | - AndreT (testing/several tutorials) 72 | - DamianC (testing reports) 73 | - IstuntmanI (testing) 74 | - JernejL (testing/suggestions) 75 | - Konstantinos (testing) 76 | - krisk (testing/suggestions) 77 | - kurta999 (testing) 78 | - Kye (coding support) 79 | - maddinat0r (developing the plugin as of R8) 80 | - Mow (compiling/testing/hosting) 81 | - nemesis (testing) 82 | - Sergei (testing/suggestions/wiki documentation) 83 | - xxmitsu (testing/compiling) 84 | -------------------------------------------------------------------------------- /a_mysql.inc.in: -------------------------------------------------------------------------------- 1 | /** 2 | * MySQL plugin @MYSQL_PLUGIN_VERSION@ 3 | */ 4 | 5 | 6 | #if defined mysql_included 7 | #endinput 8 | #endif 9 | #define mysql_included 10 | 11 | 12 | /** 13 | * Common error codes 14 | * 15 | * Client: http://dev.mysql.com/doc/refman/5.5/en/error-messages-client.html 16 | * Server: http://dev.mysql.com/doc/refman/5.5/en/error-messages-server.html 17 | */ 18 | 19 | #define ER_DBACCESS_DENIED_ERROR 1044 20 | #define ER_ACCESS_DENIED_ERROR 1045 21 | #define ER_UNKNOWN_TABLE 1109 22 | #define ER_SYNTAX_ERROR 1149 23 | #define CR_SERVER_GONE_ERROR 2006 24 | #define CR_SERVER_LOST 2013 25 | #define CR_COMMAND_OUT_OF_SYNC 2014 26 | #define CR_SERVER_LOST_EXTENDED 2055 27 | 28 | 29 | enum E_ORM_ERROR 30 | { 31 | ERROR_INVALID, 32 | ERROR_OK, 33 | ERROR_NO_DATA 34 | }; 35 | 36 | enum E_MYSQL_GLOBAL_OPTION 37 | { 38 | DUPLICATE_CONNECTIONS, 39 | DUPLICATE_CONNECTION_WARNING 40 | }; 41 | 42 | enum E_MYSQL_OPTION 43 | { 44 | AUTO_RECONNECT, 45 | MULTI_STATEMENTS, 46 | POOL_SIZE, 47 | SERVER_PORT, 48 | SSL_ENABLE, 49 | SSL_KEY_FILE, 50 | SSL_CERT_FILE, 51 | SSL_CA_FILE, 52 | SSL_CA_PATH, 53 | SSL_CIPHER 54 | }; 55 | 56 | enum E_MYSQL_FIELD_TYPE 57 | { 58 | MYSQL_TYPE_INVALID = -1, 59 | MYSQL_TYPE_DECIMAL = 0, 60 | MYSQL_TYPE_TINY, 61 | MYSQL_TYPE_SHORT, 62 | MYSQL_TYPE_LONG, 63 | MYSQL_TYPE_FLOAT, 64 | MYSQL_TYPE_DOUBLE, 65 | MYSQL_TYPE_NULL, 66 | MYSQL_TYPE_TIMESTAMP, 67 | MYSQL_TYPE_LONGLONG, 68 | MYSQL_TYPE_INT24, 69 | MYSQL_TYPE_DATE, 70 | MYSQL_TYPE_TIME, 71 | MYSQL_TYPE_DATETIME, 72 | MYSQL_TYPE_YEAR, 73 | MYSQL_TYPE_NEWDATE, 74 | MYSQL_TYPE_VARCHAR, 75 | MYSQL_TYPE_BIT, 76 | MYSQL_TYPE_TIMESTAMP2, 77 | MYSQL_TYPE_DATETIME2, 78 | MYSQL_TYPE_TIME2, 79 | MYSQL_TYPE_JSON = 245, 80 | MYSQL_TYPE_NEWDECIMAL = 246, 81 | MYSQL_TYPE_ENUM = 247, 82 | MYSQL_TYPE_SET = 248, 83 | MYSQL_TYPE_TINY_BLOB = 249, 84 | MYSQL_TYPE_MEDIUM_BLOB = 250, 85 | MYSQL_TYPE_LONG_BLOB = 251, 86 | MYSQL_TYPE_BLOB = 252, 87 | MYSQL_TYPE_VAR_STRING = 253, 88 | MYSQL_TYPE_STRING = 254, 89 | MYSQL_TYPE_GEOMETRY = 255 90 | }; 91 | 92 | enum E_MYSQL_EXECTIME_UNIT 93 | { 94 | MILLISECONDS, 95 | MICROSECONDS 96 | }; 97 | 98 | #define MYSQL_DEFAULT_HANDLE MySQL:1 99 | #define MYSQL_INVALID_HANDLE MySQL:0 100 | #define MYSQL_INVALID_CACHE Cache:0 101 | #define MYSQL_INVALID_ORM ORM:0 102 | 103 | 104 | 105 | // ORM functions 106 | native ORM:orm_create(const table[], MySQL:handle = MYSQL_DEFAULT_HANDLE); 107 | native orm_destroy(ORM:id); 108 | 109 | native E_ORM_ERROR:orm_errno(ORM:id); 110 | 111 | native orm_apply_cache(ORM:id, row_idx, result_idx = 0); 112 | 113 | native orm_select(ORM:id, const callback[] = "", const format[] = "", {ORM, Float, _}:...); 114 | native orm_update(ORM:id, const callback[] = "", const format[] = "", {ORM, Float, _}:...); 115 | native orm_insert(ORM:id, const callback[] = "", const format[] = "", {ORM, Float, _}:...); 116 | native orm_delete(ORM:id, const callback[] = "", const format[] = "", {ORM, Float, _}:...); 117 | 118 | native orm_load(ORM:id, const callback[] = "", const format[] = "", {ORM, Float, _}:...) = orm_select; 119 | native orm_save(ORM:id, const callback[] = "", const format[] = "", {ORM, Float, _}:...); 120 | 121 | native orm_addvar_int(ORM:id, &var, const columnname[]); 122 | native orm_addvar_float(ORM:id, &Float:var, const columnname[]); 123 | native orm_addvar_string(ORM:id, var[], var_maxlen, const columnname[]); 124 | 125 | native orm_clear_vars(ORM:id); 126 | native orm_delvar(ORM:id, const columnname[]); 127 | native orm_setkey(ORM:id, const columnname[]); 128 | 129 | 130 | // MySQL functions 131 | native MySQL:mysql_connect(const host[], const user[], const password[], const database[], MySQLOpt:option_id = MySQLOpt:0); 132 | native MySQL:mysql_connect_file(const file_name[] = "mysql.ini"); 133 | native mysql_close(MySQL:handle = MYSQL_DEFAULT_HANDLE); 134 | 135 | native mysql_unprocessed_queries(MySQL:handle = MYSQL_DEFAULT_HANDLE); 136 | native mysql_global_options(E_MYSQL_GLOBAL_OPTION:type, value); 137 | 138 | native MySQLOpt:mysql_init_options(); 139 | native mysql_set_option(MySQLOpt:option_id, E_MYSQL_OPTION:type, ...); 140 | 141 | native mysql_pquery(MySQL:handle, const query[], const callback[] = "", const format[] = "", {MySQL, Float,_}:...); 142 | native mysql_tquery(MySQL:handle, const query[], const callback[] = "", const format[] = "", {MySQL, Float,_}:...); 143 | native Cache:mysql_query(MySQL:handle, const query[], bool:use_cache = true); 144 | native mysql_tquery_file(MySQL:handle, const file_path[], const callback[] = "", const format[] = "", {MySQL, Float,_}:...); 145 | native Cache:mysql_query_file(MySQL:handle, const file_path[], bool:use_cache = false); 146 | 147 | native mysql_errno(MySQL:handle = MYSQL_DEFAULT_HANDLE); 148 | native mysql_error(destination[], max_len = sizeof(destination), MySQL:handle = MYSQL_DEFAULT_HANDLE); 149 | native mysql_escape_string(const source[], destination[], max_len = sizeof(destination), MySQL:handle = MYSQL_DEFAULT_HANDLE); 150 | native mysql_format(MySQL:handle, output[], max_len, const format[], {MySQL, Float,_}:...); 151 | native mysql_set_charset(const charset[], MySQL:handle = MYSQL_DEFAULT_HANDLE); 152 | native mysql_get_charset(destination[], max_len = sizeof(destination), MySQL:handle = MYSQL_DEFAULT_HANDLE); 153 | native mysql_stat(destination[], max_len = sizeof(destination), MySQL:handle = MYSQL_DEFAULT_HANDLE); 154 | 155 | 156 | 157 | // Cache functions 158 | native cache_get_row_count(&destination); 159 | native cache_get_field_count(&destination); 160 | native cache_get_result_count(&destination); 161 | native cache_get_field_name(field_index, destination[], max_len = sizeof(destination)); 162 | native E_MYSQL_FIELD_TYPE:cache_get_field_type(field_index); 163 | native cache_set_result(result_index); 164 | 165 | stock cache_num_rows() 166 | { 167 | new row_count; 168 | cache_get_row_count(row_count); 169 | return row_count; 170 | } 171 | stock cache_num_fields() 172 | { 173 | new field_count; 174 | cache_get_field_count(field_count); 175 | return field_count; 176 | } 177 | stock cache_num_results() 178 | { 179 | new result_count; 180 | cache_get_result_count(result_count); 181 | return result_count; 182 | } 183 | 184 | 185 | // overload macros for cache_get_value natives 186 | #define cache_get_value(%1) (_:MSCGV0:MSCGV1:MSCGV2:cache_get_ovrld_value(%1)) 187 | #define MSCGV0:MSCGV1:MSCGV2:cache_get_ovrld_value(%1,"%2",%3) cache_get_value_name(%1,#%2,%3) 188 | #define MSCGV1:MSCGV2:cache_get_ovrld_value(%1,%8string%9:%2,%3) cache_get_value_name(%1,%2,%3) 189 | #define MSCGV2:cache_get_ovrld_value(%1,%2,%3) cache_get_value_index(%1,%2,%3) 190 | 191 | #define cache_get_value_int(%1) (_:MSCGVI0:MSCGVI1:MSCGVI2:cache_get_value_int_ovrld(%1)) 192 | #define MSCGVI0:MSCGVI1:MSCGVI2:cache_get_value_int_ovrld(%1,"%2",%3) cache_get_value_name_int(%1,#%2,%3) 193 | #define MSCGVI1:MSCGVI2:cache_get_value_int_ovrld(%1,%8string%9:%2,%3) cache_get_value_name_int(%1,%2,%3) 194 | #define MSCGVI2:cache_get_value_int_ovrld(%1,%2,%3) cache_get_value_index_int(%1,%2,%3) 195 | 196 | #define cache_get_value_float(%1) (_:MSCGVF0:MSCGVF1:MSCGVF2:cache_get_value_float_ovrld(%1)) 197 | #define MSCGVF0:MSCGVF1:MSCGVF2:cache_get_value_float_ovrld(%1,"%2",%3) cache_get_value_name_float(%1,#%2,%3) 198 | #define MSCGVF1:MSCGVF2:cache_get_value_float_ovrld(%1,%8string%9:%2,%3) cache_get_value_name_float(%1,%2,%3) 199 | #define MSCGVF2:cache_get_value_float_ovrld(%1,%2,%3) cache_get_value_index_float(%1,%2,%3) 200 | 201 | #define cache_get_value_bool(%1) cache_get_value_int(%1) 202 | 203 | #define cache_is_value_null(%1) (_:MSCIVN0:MSCIVN1:MSCIVN2:cache_is_value_null_ovrld(%1)) 204 | #define MSCIVN0:MSCIVN1:MSCIVN2:cache_is_value_null_ovrld(%1,"%2",%3) cache_is_value_name_null(%1,#%2,%3) 205 | #define MSCIVN1:MSCIVN2:cache_is_value_null_ovrld(%1,%8string%9:%2,%3) cache_is_value_name_null(%1,%2,%3) 206 | #define MSCIVN2:cache_is_value_null_ovrld(%1,%2,%3) cache_is_value_index_null(%1,%2,%3) 207 | 208 | 209 | native cache_get_value_index(row_idx, column_idx, destination[], max_len = sizeof(destination)); 210 | native cache_get_value_index_int(row_idx, column_idx, &destination); 211 | native cache_get_value_index_float(row_idx, column_idx, &Float:destination); 212 | native cache_get_value_index_bool(row_idx, column_idx, &bool:destination) = cache_get_value_index_int; 213 | native cache_is_value_index_null(row_idx, column_idx, &bool:destination); 214 | 215 | native cache_get_value_name(row_idx, const column_name[], destination[], max_len = sizeof(destination)); 216 | native cache_get_value_name_int(row_idx, const column_name[], &destination); 217 | native cache_get_value_name_float(row_idx, const column_name[], &Float:destination); 218 | native cache_get_value_name_bool(row_idx, const column_name[], &bool:destination) = cache_get_value_name_int; 219 | native cache_is_value_name_null(row_idx, const column_name[], &bool:destination); 220 | 221 | native Cache:cache_save(); 222 | native cache_delete(Cache:cache_id); 223 | native cache_set_active(Cache:cache_id); 224 | native cache_unset_active(); 225 | native bool:cache_is_any_active(); 226 | native bool:cache_is_valid(Cache:cache_id); 227 | 228 | native cache_affected_rows(); 229 | native cache_insert_id(); 230 | native cache_warning_count(); 231 | 232 | native cache_get_query_exec_time(E_MYSQL_EXECTIME_UNIT:unit = MICROSECONDS); 233 | native cache_get_query_string(destination[], max_len = sizeof(destination)); 234 | 235 | 236 | // Forward declarations 237 | forward OnQueryError(errorid, const error[], const callback[], const query[], MySQL:handle); 238 | 239 | 240 | // Deprecated functions 241 | 242 | #if !defined E_LOGLEVEL 243 | enum E_LOGLEVEL 244 | { 245 | NONE = 0, 246 | DEBUG = 1, 247 | INFO = 2, 248 | WARNING = 4, 249 | ERROR = 8, 250 | ALL = ERROR | WARNING | INFO | DEBUG 251 | }; 252 | #endif 253 | 254 | #pragma deprecated Configuring log levels is now done through the "log-config.yml" file 255 | stock mysql_log(E_LOGLEVEL:loglevel = ERROR | WARNING) 256 | { 257 | #pragma unused loglevel 258 | } 259 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | version: '{build}' 2 | image: Visual Studio 2015 3 | 4 | services: 5 | - mysql 6 | 7 | configuration: 8 | - Debug 9 | - Release 10 | 11 | platform: x86 12 | 13 | clone_depth: 1 14 | 15 | environment: 16 | SAMP_SERVER_ROOT: C:\Projects\sa-mp-mysql\samp03 17 | BOOST_ROOT: C:/Libraries/boost_1_59_0 18 | BOOST_LIBRARYDIR: C:/Libraries/boost_1_59_0/lib32-msvc-14.0 19 | 20 | install: 21 | - git submodule update --init --recursive 22 | 23 | - appveyor DownloadFile https://downloads.mariadb.com/Connectors/c/connector-c-3.0.2/mariadb-connector-c-3.0.2-win32.msi 24 | - msiexec /i mariadb-connector-c-3.0.2-win32.msi /quiet /qn 25 | - del mariadb-connector-c-3.0.2-win32.msi 26 | 27 | - ps: Start-FileDownload http://files.sa-mp.com/samp037_svr_R2-2-1_win32.zip 28 | - 7z x samp037_svr_R2-2-1_win32.zip -osamp03 29 | 30 | - appveyor DownloadFile https://github.com/Zeex/pawn/releases/download/20160702/pawnc-3.10.20160702-windows.zip 31 | - 7z x pawnc-3.10.20160702-windows.zip 32 | - set PATH=%PATH%;%CD%\pawnc-3.10.20160702-windows\bin 33 | 34 | - set PATH=%PATH%;C:\Python27\Scripts 35 | - pip install samp-server-cli 36 | 37 | - set PATH=%PATH%;C:\Program Files\MySql\MySQL Server 5.7\bin 38 | 39 | before_build: 40 | - cmake . -G "Visual Studio 14 2015" -DCMAKE_INSTALL_PREFIX="%SAMP_SERVER_ROOT%" -DBOOST_ROOT="%BOOST_ROOT%" -DBOOST_LIBRARYDIR="%BOOST_LIBRARYDIR%" -DMYSQLCAPI_ROOT_DIR="C:/Program Files (x86)/MariaDB/MariaDB Connector C" 41 | 42 | build_script: 43 | - cmake --build . --config %CONFIGURATION% 44 | - cmake --build . --config %CONFIGURATION% --target install 45 | - cmake --build . --config %CONFIGURATION% --target package 46 | 47 | before_test: 48 | - mysql -e "CREATE DATABASE `test`;" --user=root --password=Password12! 49 | - mysql -e "CREATE USER 'tester'@'127.0.0.1' IDENTIFIED BY '1234';" --user=root --password=Password12! 50 | - mysql -e "GRANT ALL PRIVILEGES ON *.* TO 'tester'@'127.0.0.1' WITH GRANT OPTION;" --user=root --password=Password12! 51 | 52 | test_script: 53 | - pawncc tests\unit_test.pwn -d3 -Z+ -; -( -i%SAMP_SERVER_ROOT%\pawno\include -itests\include 54 | - xcopy tests\test_data %SAMP_SERVER_ROOT% /E 55 | - copy *.amx %SAMP_SERVER_ROOT%\gamemodes 56 | - samp-server-cli -o -g unit_test -d mysql -T 60 57 | 58 | on_failure: 59 | - type %SAMP_SERVER_ROOT%\logs\plugins\mysql.log 60 | 61 | artifacts: 62 | - path: mysql-*-win32.zip 63 | name: MySQL plugin 64 | 65 | - path: samp03\logs 66 | name: MySQL unit test logs 67 | -------------------------------------------------------------------------------- /cmake/FindMySQLCAPI.cmake: -------------------------------------------------------------------------------- 1 | # Look for MySQL C API 2 | # Once done, this will define 3 | # 4 | # MYSQLCAPI_FOUND - system has MySQL C API 5 | # MYSQLCAPI_INCLUDE_DIR - MySQL C API include directories 6 | # MYSQLCAPI_LIBRARY - MySQL C library 7 | # MYSQLCAPI_LIBRARY_SHARED - MySQL C shared library (.dll file on Windows; same as MYSQLCAPI_LIBRARY on Linux) 8 | # MYSQLCAPI_LIBRARY_STATIC - MySQL C static library (Linux only) 9 | # 10 | # The user may wish to set, in the CMake GUI or otherwise, this variable: 11 | # MYSQLCAPI_ROOT_DIR - path to start searching for the module 12 | 13 | set(MYSQLCAPI_ROOT_DIR 14 | "${MYSQLCAPI_ROOT_DIR}" 15 | CACHE 16 | PATH 17 | "Where to start looking for this component." 18 | ) 19 | 20 | if(WIN32) 21 | find_path( 22 | MYSQLCAPI_INCLUDE_DIR 23 | NAMES 24 | "mariadb_version.h" 25 | "mysql_version.h" 26 | HINTS 27 | ${MYSQLCAPI_ROOT_DIR} 28 | PATH_SUFFIXES 29 | include 30 | ) 31 | 32 | find_library( 33 | MYSQLCAPI_LIBRARY 34 | NAMES 35 | "libmariadb.lib" 36 | "libmysql.lib" 37 | HINTS 38 | ${MYSQLCAPI_ROOT_DIR} 39 | PATH_SUFFIXES 40 | lib 41 | ) 42 | 43 | find_file( 44 | MYSQLCAPI_LIBRARY_SHARED 45 | NAMES 46 | "libmariadb.dll" 47 | "libmysql.dll" 48 | HINTS 49 | ${MYSQLCAPI_ROOT_DIR} 50 | PATH_SUFFIXES 51 | lib 52 | ) 53 | else() 54 | find_path( 55 | MYSQLCAPI_INCLUDE_DIR 56 | NAMES 57 | "mysql_version.h" 58 | PATHS 59 | "/usr/include" 60 | HINTS 61 | ${MYSQLCAPI_ROOT_DIR} 62 | PATH_SUFFIXES 63 | mysql 64 | ) 65 | 66 | find_library( 67 | MYSQLCAPI_LIBRARY 68 | NAME 69 | mysqlclient_r 70 | mysqlclient 71 | HINTS 72 | ${MYSQLCAPI_ROOT_DIR} 73 | PATH_SUFFIXES 74 | mysql 75 | i386-linux-gnu 76 | x86_64-linux-gnu 77 | ) 78 | set(MYSQLCAPI_LIBRARY_SHARED ${MYSQLCAPI_LIBRARY}) 79 | 80 | find_library( 81 | MYSQLCAPI_LIBRARY_STATIC 82 | NAME 83 | "libmysqlclient.a" 84 | HINTS 85 | ${MYSQLCAPI_ROOT_DIR} 86 | PATH_SUFFIXES 87 | mysql 88 | i386-linux-gnu 89 | x86_64-linux-gnu 90 | ) 91 | endif() 92 | 93 | mark_as_advanced( 94 | MYSQLCAPI_INCLUDE_DIR 95 | MYSQLCAPI_LIBRARY 96 | MYSQLCAPI_LIBRARY_SHARED 97 | MYSQLCAPI_LIBRARY_STATIC 98 | ) 99 | 100 | include(FindPackageHandleStandardArgs) 101 | find_package_handle_standard_args( 102 | MySQLCAPI 103 | REQUIRED_VARS 104 | MYSQLCAPI_INCLUDE_DIR 105 | MYSQLCAPI_LIBRARY 106 | ) 107 | 108 | if(MYSQLCAPI_FOUND) 109 | mark_as_advanced(MYSQLCAPI_ROOT_DIR) 110 | endif() 111 | -------------------------------------------------------------------------------- /example_scripts/INFO.txt: -------------------------------------------------------------------------------- 1 | - these two scripts are gamemodes 2 | - both of them do the same, the only difference is HOW they do it 3 | - one uses the cache functions, the other one the ORM system 4 | - feel free to use these scripts like you want 5 | - don't forget to edit the MySQL configuration in the gamemodes 6 | - the gamemodes automatically create the correct table(s) 7 | -------------------------------------------------------------------------------- /example_scripts/login_system-cache.pwn: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | // change MAX_PLAYERS to the amount of players (slots) you want 4 | // It is by default 1000 (as of 0.3.7 version) 5 | #undef MAX_PLAYERS 6 | #define MAX_PLAYERS 50 7 | 8 | #include 9 | 10 | // MySQL configuration 11 | #define MYSQL_HOST "127.0.0.1" 12 | #define MYSQL_USER "username" 13 | #define MYSQL_PASSWORD "password" 14 | #define MYSQL_DATABASE "database" 15 | 16 | // how many seconds until it kicks the player for taking too long to login 17 | #define SECONDS_TO_LOGIN 30 18 | 19 | // default spawn point: Las Venturas (The High Roller) 20 | #define DEFAULT_POS_X 1958.3783 21 | #define DEFAULT_POS_Y 1343.1572 22 | #define DEFAULT_POS_Z 15.3746 23 | #define DEFAULT_POS_A 270.1425 24 | 25 | // MySQL connection handle 26 | new MySQL: g_SQL; 27 | 28 | // player data 29 | enum E_PLAYERS 30 | { 31 | ID, 32 | Name[MAX_PLAYER_NAME], 33 | Password[65], // the output of SHA256_PassHash function (which was added in 0.3.7 R1 version) is always 256 bytes in length, or the equivalent of 64 Pawn cells 34 | Salt[17], 35 | Kills, 36 | Deaths, 37 | Float: X_Pos, 38 | Float: Y_Pos, 39 | Float: Z_Pos, 40 | Float: A_Pos, 41 | Interior, 42 | 43 | Cache: Cache_ID, 44 | bool: IsLoggedIn, 45 | LoginAttempts, 46 | LoginTimer 47 | }; 48 | new Player[MAX_PLAYERS][E_PLAYERS]; 49 | 50 | new g_MysqlRaceCheck[MAX_PLAYERS]; 51 | 52 | // dialog data 53 | enum 54 | { 55 | DIALOG_UNUSED, 56 | 57 | DIALOG_LOGIN, 58 | DIALOG_REGISTER 59 | }; 60 | 61 | main() {} 62 | 63 | 64 | public OnGameModeInit() 65 | { 66 | new MySQLOpt: option_id = mysql_init_options(); 67 | 68 | mysql_set_option(option_id, AUTO_RECONNECT, true); // it automatically reconnects when loosing connection to mysql server 69 | 70 | g_SQL = mysql_connect(MYSQL_HOST, MYSQL_USER, MYSQL_PASSWORD, MYSQL_DATABASE, option_id); // AUTO_RECONNECT is enabled for this connection handle only 71 | if (g_SQL == MYSQL_INVALID_HANDLE || mysql_errno(g_SQL) != 0) 72 | { 73 | print("MySQL connection failed. Server is shutting down."); 74 | SendRconCommand("exit"); // close the server if there is no connection 75 | return 1; 76 | } 77 | 78 | print("MySQL connection is successful."); 79 | 80 | // if the table has been created, the "SetupPlayerTable" function does not have any purpose so you may remove it completely 81 | SetupPlayerTable(); 82 | return 1; 83 | } 84 | 85 | public OnGameModeExit() 86 | { 87 | // save all player data before closing connection 88 | for (new i = 0, j = GetPlayerPoolSize(); i <= j; i++) // GetPlayerPoolSize function was added in 0.3.7 version and gets the highest playerid currently in use on the server 89 | { 90 | if (IsPlayerConnected(i)) 91 | { 92 | // reason is set to 1 for normal 'Quit' 93 | OnPlayerDisconnect(i, 1); 94 | } 95 | } 96 | 97 | mysql_close(g_SQL); 98 | return 1; 99 | } 100 | 101 | public OnPlayerConnect(playerid) 102 | { 103 | g_MysqlRaceCheck[playerid]++; 104 | 105 | // reset player data 106 | static const empty_player[E_PLAYERS]; 107 | Player[playerid] = empty_player; 108 | 109 | GetPlayerName(playerid, Player[playerid][Name], MAX_PLAYER_NAME); 110 | 111 | // send a query to recieve all the stored player data from the table 112 | new query[103]; 113 | mysql_format(g_SQL, query, sizeof query, "SELECT * FROM `players` WHERE `username` = '%e' LIMIT 1", Player[playerid][Name]); 114 | mysql_tquery(g_SQL, query, "OnPlayerDataLoaded", "dd", playerid, g_MysqlRaceCheck[playerid]); 115 | return 1; 116 | } 117 | 118 | public OnPlayerDisconnect(playerid, reason) 119 | { 120 | g_MysqlRaceCheck[playerid]++; 121 | 122 | UpdatePlayerData(playerid, reason); 123 | 124 | // if the player was kicked (either wrong password or taking too long) during the login part, remove the data from the memory 125 | if (cache_is_valid(Player[playerid][Cache_ID])) 126 | { 127 | cache_delete(Player[playerid][Cache_ID]); 128 | Player[playerid][Cache_ID] = MYSQL_INVALID_CACHE; 129 | } 130 | 131 | // if the player was kicked before the time expires (30 seconds), kill the timer 132 | if (Player[playerid][LoginTimer]) 133 | { 134 | KillTimer(Player[playerid][LoginTimer]); 135 | Player[playerid][LoginTimer] = 0; 136 | } 137 | 138 | // sets "IsLoggedIn" to false when the player disconnects, it prevents from saving the player data twice when "gmx" is used 139 | Player[playerid][IsLoggedIn] = false; 140 | return 1; 141 | } 142 | 143 | public OnPlayerSpawn(playerid) 144 | { 145 | // spawn the player to their last saved position 146 | SetPlayerInterior(playerid, Player[playerid][Interior]); 147 | SetPlayerPos(playerid, Player[playerid][X_Pos], Player[playerid][Y_Pos], Player[playerid][Z_Pos]); 148 | SetPlayerFacingAngle(playerid, Player[playerid][A_Pos]); 149 | 150 | SetCameraBehindPlayer(playerid); 151 | return 1; 152 | } 153 | 154 | public OnPlayerDeath(playerid, killerid, reason) 155 | { 156 | UpdatePlayerDeaths(playerid); 157 | UpdatePlayerKills(killerid); 158 | return 1; 159 | } 160 | 161 | public OnDialogResponse(playerid, dialogid, response, listitem, inputtext[]) 162 | { 163 | switch (dialogid) 164 | { 165 | case DIALOG_UNUSED: return 1; // Useful for dialogs that contain only information and we do nothing depending on whether they responded or not 166 | 167 | case DIALOG_LOGIN: 168 | { 169 | if (!response) return Kick(playerid); 170 | 171 | new hashed_pass[65]; 172 | SHA256_PassHash(inputtext, Player[playerid][Salt], hashed_pass, 65); 173 | 174 | if (strcmp(hashed_pass, Player[playerid][Password]) == 0) 175 | { 176 | //correct password, spawn the player 177 | ShowPlayerDialog(playerid, DIALOG_UNUSED, DIALOG_STYLE_MSGBOX, "Login", "You have been successfully logged in.", "Okay", ""); 178 | 179 | // sets the specified cache as the active cache so we can retrieve the rest player data 180 | cache_set_active(Player[playerid][Cache_ID]); 181 | 182 | AssignPlayerData(playerid); 183 | 184 | // remove the active cache from memory and unsets the active cache as well 185 | cache_delete(Player[playerid][Cache_ID]); 186 | Player[playerid][Cache_ID] = MYSQL_INVALID_CACHE; 187 | 188 | KillTimer(Player[playerid][LoginTimer]); 189 | Player[playerid][LoginTimer] = 0; 190 | Player[playerid][IsLoggedIn] = true; 191 | 192 | // spawn the player to their last saved position after login 193 | SetSpawnInfo(playerid, NO_TEAM, 0, Player[playerid][X_Pos], Player[playerid][Y_Pos], Player[playerid][Z_Pos], Player[playerid][A_Pos], 0, 0, 0, 0, 0, 0); 194 | SpawnPlayer(playerid); 195 | } 196 | else 197 | { 198 | Player[playerid][LoginAttempts]++; 199 | 200 | if (Player[playerid][LoginAttempts] >= 3) 201 | { 202 | ShowPlayerDialog(playerid, DIALOG_UNUSED, DIALOG_STYLE_MSGBOX, "Login", "You have mistyped your password too often (3 times).", "Okay", ""); 203 | DelayedKick(playerid); 204 | } 205 | else ShowPlayerDialog(playerid, DIALOG_LOGIN, DIALOG_STYLE_PASSWORD, "Login", "Wrong password!\nPlease enter your password in the field below:", "Login", "Abort"); 206 | } 207 | } 208 | case DIALOG_REGISTER: 209 | { 210 | if (!response) return Kick(playerid); 211 | 212 | if (strlen(inputtext) <= 5) return ShowPlayerDialog(playerid, DIALOG_REGISTER, DIALOG_STYLE_PASSWORD, "Registration", "Your password must be longer than 5 characters!\nPlease enter your password in the field below:", "Register", "Abort"); 213 | 214 | // 16 random characters from 33 to 126 (in ASCII) for the salt 215 | for (new i = 0; i < 16; i++) Player[playerid][Salt][i] = random(94) + 33; 216 | SHA256_PassHash(inputtext, Player[playerid][Salt], Player[playerid][Password], 65); 217 | 218 | new query[221]; 219 | mysql_format(g_SQL, query, sizeof query, "INSERT INTO `players` (`username`, `password`, `salt`) VALUES ('%e', '%s', '%e')", Player[playerid][Name], Player[playerid][Password], Player[playerid][Salt]); 220 | mysql_tquery(g_SQL, query, "OnPlayerRegister", "d", playerid); 221 | } 222 | 223 | default: return 0; // dialog ID was not found, search in other scripts 224 | } 225 | return 1; 226 | } 227 | 228 | //----------------------------------------------------- 229 | 230 | forward OnPlayerDataLoaded(playerid, race_check); 231 | public OnPlayerDataLoaded(playerid, race_check) 232 | { 233 | /* race condition check: 234 | player A connects -> SELECT query is fired -> this query takes very long 235 | while the query is still processing, player A with playerid 2 disconnects 236 | player B joins now with playerid 2 -> our laggy SELECT query is finally finished, but for the wrong player 237 | 238 | what do we do against it? 239 | we create a connection count for each playerid and increase it everytime the playerid connects or disconnects 240 | we also pass the current value of the connection count to our OnPlayerDataLoaded callback 241 | then we check if current connection count is the same as connection count we passed to the callback 242 | if yes, everything is okay, if not, we just kick the player 243 | */ 244 | if (race_check != g_MysqlRaceCheck[playerid]) return Kick(playerid); 245 | 246 | new string[115]; 247 | if(cache_num_rows() > 0) 248 | { 249 | // we store the password and the salt so we can compare the password the player inputs 250 | // and save the rest so we won't have to execute another query later 251 | cache_get_value(0, "password", Player[playerid][Password], 65); 252 | cache_get_value(0, "salt", Player[playerid][Salt], 17); 253 | 254 | // saves the active cache in the memory and returns an cache-id to access it for later use 255 | Player[playerid][Cache_ID] = cache_save(); 256 | 257 | format(string, sizeof string, "This account (%s) is registered. Please login by entering your password in the field below:", Player[playerid][Name]); 258 | ShowPlayerDialog(playerid, DIALOG_LOGIN, DIALOG_STYLE_PASSWORD, "Login", string, "Login", "Abort"); 259 | 260 | // from now on, the player has 30 seconds to login 261 | Player[playerid][LoginTimer] = SetTimerEx("OnLoginTimeout", SECONDS_TO_LOGIN * 1000, false, "d", playerid); 262 | } 263 | else 264 | { 265 | format(string, sizeof string, "Welcome %s, you can register by entering your password in the field below:", Player[playerid][Name]); 266 | ShowPlayerDialog(playerid, DIALOG_REGISTER, DIALOG_STYLE_PASSWORD, "Registration", string, "Register", "Abort"); 267 | } 268 | return 1; 269 | } 270 | 271 | forward OnLoginTimeout(playerid); 272 | public OnLoginTimeout(playerid) 273 | { 274 | // reset the variable that stores the timerid 275 | Player[playerid][LoginTimer] = 0; 276 | 277 | ShowPlayerDialog(playerid, DIALOG_UNUSED, DIALOG_STYLE_MSGBOX, "Login", "You have been kicked for taking too long to login successfully to your account.", "Okay", ""); 278 | DelayedKick(playerid); 279 | return 1; 280 | } 281 | 282 | forward OnPlayerRegister(playerid); 283 | public OnPlayerRegister(playerid) 284 | { 285 | // retrieves the ID generated for an AUTO_INCREMENT column by the sent query 286 | Player[playerid][ID] = cache_insert_id(); 287 | 288 | ShowPlayerDialog(playerid, DIALOG_UNUSED, DIALOG_STYLE_MSGBOX, "Registration", "Account successfully registered, you have been automatically logged in.", "Okay", ""); 289 | 290 | Player[playerid][IsLoggedIn] = true; 291 | 292 | Player[playerid][X_Pos] = DEFAULT_POS_X; 293 | Player[playerid][Y_Pos] = DEFAULT_POS_Y; 294 | Player[playerid][Z_Pos] = DEFAULT_POS_Z; 295 | Player[playerid][A_Pos] = DEFAULT_POS_A; 296 | 297 | SetSpawnInfo(playerid, NO_TEAM, 0, Player[playerid][X_Pos], Player[playerid][Y_Pos], Player[playerid][Z_Pos], Player[playerid][A_Pos], 0, 0, 0, 0, 0, 0); 298 | SpawnPlayer(playerid); 299 | return 1; 300 | } 301 | 302 | forward _KickPlayerDelayed(playerid); 303 | public _KickPlayerDelayed(playerid) 304 | { 305 | Kick(playerid); 306 | return 1; 307 | } 308 | 309 | 310 | //----------------------------------------------------- 311 | 312 | AssignPlayerData(playerid) 313 | { 314 | cache_get_value_int(0, "id", Player[playerid][ID]); 315 | 316 | cache_get_value_int(0, "kills", Player[playerid][Kills]); 317 | cache_get_value_int(0, "deaths", Player[playerid][Deaths]); 318 | 319 | cache_get_value_float(0, "x", Player[playerid][X_Pos]); 320 | cache_get_value_float(0, "y", Player[playerid][Y_Pos]); 321 | cache_get_value_float(0, "z", Player[playerid][Z_Pos]); 322 | cache_get_value_float(0, "angle", Player[playerid][A_Pos]); 323 | cache_get_value_int(0, "interior", Player[playerid][Interior]); 324 | return 1; 325 | } 326 | 327 | DelayedKick(playerid, time = 500) 328 | { 329 | SetTimerEx("_KickPlayerDelayed", time, false, "d", playerid); 330 | return 1; 331 | } 332 | 333 | SetupPlayerTable() 334 | { 335 | mysql_tquery(g_SQL, "CREATE TABLE IF NOT EXISTS `players` (`id` int(11) NOT NULL AUTO_INCREMENT,`username` varchar(24) NOT NULL,`password` char(64) NOT NULL,`salt` char(16) NOT NULL,`kills` mediumint(8) NOT NULL DEFAULT '0',`deaths` mediumint(8) NOT NULL DEFAULT '0',`x` float NOT NULL DEFAULT '0',`y` float NOT NULL DEFAULT '0',`z` float NOT NULL DEFAULT '0',`angle` float NOT NULL DEFAULT '0',`interior` tinyint(3) NOT NULL DEFAULT '0', PRIMARY KEY (`id`), UNIQUE KEY `username` (`username`))"); 336 | return 1; 337 | } 338 | 339 | UpdatePlayerData(playerid, reason) 340 | { 341 | if (Player[playerid][IsLoggedIn] == false) return 0; 342 | 343 | // if the client crashed, it's not possible to get the player's position in OnPlayerDisconnect callback 344 | // so we will use the last saved position (in case of a player who registered and crashed/kicked, the position will be the default spawn point) 345 | if (reason == 1) 346 | { 347 | GetPlayerPos(playerid, Player[playerid][X_Pos], Player[playerid][Y_Pos], Player[playerid][Z_Pos]); 348 | GetPlayerFacingAngle(playerid, Player[playerid][A_Pos]); 349 | } 350 | 351 | new query[145]; 352 | mysql_format(g_SQL, query, sizeof query, "UPDATE `players` SET `x` = %f, `y` = %f, `z` = %f, `angle` = %f, `interior` = %d WHERE `id` = %d LIMIT 1", Player[playerid][X_Pos], Player[playerid][Y_Pos], Player[playerid][Z_Pos], Player[playerid][A_Pos], GetPlayerInterior(playerid), Player[playerid][ID]); 353 | mysql_tquery(g_SQL, query); 354 | return 1; 355 | } 356 | 357 | UpdatePlayerDeaths(playerid) 358 | { 359 | if (Player[playerid][IsLoggedIn] == false) return 0; 360 | 361 | Player[playerid][Deaths]++; 362 | 363 | new query[70]; 364 | mysql_format(g_SQL, query, sizeof query, "UPDATE `players` SET `deaths` = %d WHERE `id` = %d LIMIT 1", Player[playerid][Deaths], Player[playerid][ID]); 365 | mysql_tquery(g_SQL, query); 366 | return 1; 367 | } 368 | 369 | UpdatePlayerKills(killerid) 370 | { 371 | // we must check before if the killer wasn't valid (connected) player to avoid run time error 4 372 | if (killerid == INVALID_PLAYER_ID) return 0; 373 | if (Player[killerid][IsLoggedIn] == false) return 0; 374 | 375 | Player[killerid][Kills]++; 376 | 377 | new query[70]; 378 | mysql_format(g_SQL, query, sizeof query, "UPDATE `players` SET `kills` = %d WHERE `id` = %d LIMIT 1", Player[killerid][Kills], Player[killerid][ID]); 379 | mysql_tquery(g_SQL, query); 380 | return 1; 381 | } 382 | -------------------------------------------------------------------------------- /example_scripts/login_system-orm.pwn: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | // change MAX_PLAYERS to the amount of players (slots) you want 4 | // It is by default 1000 (as of 0.3.7 version) 5 | #undef MAX_PLAYERS 6 | #define MAX_PLAYERS 50 7 | 8 | #include 9 | 10 | // MySQL configuration 11 | #define MYSQL_HOST "127.0.0.1" 12 | #define MYSQL_USER "username" 13 | #define MYSQL_PASSWORD "password" 14 | #define MYSQL_DATABASE "database" 15 | 16 | // how many seconds until it kicks the player for taking too long to login 17 | #define SECONDS_TO_LOGIN 30 18 | 19 | // default spawn point: Las Venturas (The High Roller) 20 | #define DEFAULT_POS_X 1958.3783 21 | #define DEFAULT_POS_Y 1343.1572 22 | #define DEFAULT_POS_Z 15.3746 23 | #define DEFAULT_POS_A 270.1425 24 | 25 | // MySQL connection handle 26 | new MySQL: g_SQL; 27 | 28 | // player data 29 | enum E_PLAYERS 30 | { 31 | ORM: ORM_ID, 32 | 33 | ID, 34 | Name[MAX_PLAYER_NAME], 35 | Password[65], // the output of SHA256_PassHash function (which was added in 0.3.7 R1 version) is always 256 bytes in length, or the equivalent of 64 Pawn cells 36 | Salt[17], 37 | Kills, 38 | Deaths, 39 | Float: X_Pos, 40 | Float: Y_Pos, 41 | Float: Z_Pos, 42 | Float: A_Pos, 43 | Interior, 44 | 45 | bool: IsLoggedIn, 46 | LoginAttempts, 47 | LoginTimer 48 | }; 49 | new Player[MAX_PLAYERS][E_PLAYERS]; 50 | 51 | new g_MysqlRaceCheck[MAX_PLAYERS]; 52 | 53 | //dialog data 54 | enum 55 | { 56 | DIALOG_UNUSED, 57 | 58 | DIALOG_LOGIN, 59 | DIALOG_REGISTER 60 | }; 61 | 62 | main() {} 63 | 64 | public OnGameModeInit() 65 | { 66 | new MySQLOpt: option_id = mysql_init_options(); 67 | 68 | mysql_set_option(option_id, AUTO_RECONNECT, true); // it automatically reconnects when loosing connection to mysql server 69 | 70 | g_SQL = mysql_connect(MYSQL_HOST, MYSQL_USER, MYSQL_PASSWORD, MYSQL_DATABASE, option_id); // AUTO_RECONNECT is enabled for this connection handle only 71 | if (g_SQL == MYSQL_INVALID_HANDLE || mysql_errno(g_SQL) != 0) 72 | { 73 | print("MySQL connection failed. Server is shutting down."); 74 | SendRconCommand("exit"); // close the server if there is no connection 75 | return 1; 76 | } 77 | 78 | print("MySQL connection is successful."); 79 | 80 | // if the table has been created, the "SetupPlayerTable" function does not have any purpose so you may remove it completely 81 | SetupPlayerTable(); 82 | return 1; 83 | } 84 | 85 | public OnGameModeExit() 86 | { 87 | // save all player data before closing connection 88 | for (new i = 0, j = GetPlayerPoolSize(); i <= j; i++) // GetPlayerPoolSize function was added in 0.3.7 version and gets the highest playerid currently in use on the server 89 | { 90 | if (IsPlayerConnected(i)) 91 | { 92 | // reason is set to 1 for normal 'Quit' 93 | OnPlayerDisconnect(i, 1); 94 | } 95 | } 96 | 97 | mysql_close(g_SQL); 98 | return 1; 99 | } 100 | 101 | public OnPlayerConnect(playerid) 102 | { 103 | g_MysqlRaceCheck[playerid]++; 104 | 105 | // reset player data 106 | static const empty_player[E_PLAYERS]; 107 | Player[playerid] = empty_player; 108 | 109 | GetPlayerName(playerid, Player[playerid][Name], MAX_PLAYER_NAME); 110 | 111 | // create orm instance and register all needed variables 112 | new ORM: ormid = Player[playerid][ORM_ID] = orm_create("players", g_SQL); 113 | 114 | orm_addvar_int(ormid, Player[playerid][ID], "id"); 115 | orm_addvar_string(ormid, Player[playerid][Name], MAX_PLAYER_NAME, "username"); 116 | orm_addvar_string(ormid, Player[playerid][Password], 65, "password"); 117 | orm_addvar_string(ormid, Player[playerid][Salt], 17, "salt"); 118 | orm_addvar_int(ormid, Player[playerid][Kills], "kills"); 119 | orm_addvar_int(ormid, Player[playerid][Deaths], "deaths"); 120 | orm_addvar_float(ormid, Player[playerid][X_Pos], "x"); 121 | orm_addvar_float(ormid, Player[playerid][Y_Pos], "y"); 122 | orm_addvar_float(ormid, Player[playerid][Z_Pos], "z"); 123 | orm_addvar_float(ormid, Player[playerid][A_Pos], "angle"); 124 | orm_addvar_int(ormid, Player[playerid][Interior], "interior"); 125 | orm_setkey(ormid, "username"); 126 | 127 | // tell the orm system to load all data, assign it to our variables and call our callback when ready 128 | orm_load(ormid, "OnPlayerDataLoaded", "dd", playerid, g_MysqlRaceCheck[playerid]); 129 | return 1; 130 | } 131 | 132 | public OnPlayerDisconnect(playerid, reason) 133 | { 134 | g_MysqlRaceCheck[playerid]++; 135 | 136 | UpdatePlayerData(playerid, reason); 137 | 138 | // if the player was kicked before the time expires (30 seconds), kill the timer 139 | if (Player[playerid][LoginTimer]) 140 | { 141 | KillTimer(Player[playerid][LoginTimer]); 142 | Player[playerid][LoginTimer] = 0; 143 | } 144 | 145 | // sets "IsLoggedIn" to false when the player disconnects, it prevents from saving the player data twice when "gmx" is used 146 | Player[playerid][IsLoggedIn] = false; 147 | return 1; 148 | } 149 | 150 | public OnPlayerSpawn(playerid) 151 | { 152 | // spawn the player to their last saved position 153 | SetPlayerInterior(playerid, Player[playerid][Interior]); 154 | SetPlayerPos(playerid, Player[playerid][X_Pos], Player[playerid][Y_Pos], Player[playerid][Z_Pos]); 155 | SetPlayerFacingAngle(playerid, Player[playerid][A_Pos]); 156 | 157 | SetCameraBehindPlayer(playerid); 158 | return 1; 159 | } 160 | 161 | public OnPlayerDeath(playerid, killerid, reason) 162 | { 163 | UpdatePlayerDeaths(playerid); 164 | UpdatePlayerKills(killerid); 165 | return 1; 166 | } 167 | 168 | public OnDialogResponse(playerid, dialogid, response, listitem, inputtext[]) 169 | { 170 | switch (dialogid) 171 | { 172 | case DIALOG_UNUSED: return 1; // Useful for dialogs that contain only information and we do nothing depending on whether they responded or not 173 | 174 | case DIALOG_LOGIN: 175 | { 176 | if (!response) return Kick(playerid); 177 | 178 | new hashed_pass[65]; 179 | SHA256_PassHash(inputtext, Player[playerid][Salt], hashed_pass, 65); 180 | 181 | if (strcmp(hashed_pass, Player[playerid][Password]) == 0) 182 | { 183 | //correct password, spawn the player 184 | ShowPlayerDialog(playerid, DIALOG_UNUSED, DIALOG_STYLE_MSGBOX, "Login", "You have been successfully logged in.", "Okay", ""); 185 | 186 | KillTimer(Player[playerid][LoginTimer]); 187 | Player[playerid][LoginTimer] = 0; 188 | Player[playerid][IsLoggedIn] = true; 189 | 190 | // spawn the player to their last saved position after login 191 | SetSpawnInfo(playerid, NO_TEAM, 0, Player[playerid][X_Pos], Player[playerid][Y_Pos], Player[playerid][Z_Pos], Player[playerid][A_Pos], 0, 0, 0, 0, 0, 0); 192 | SpawnPlayer(playerid); 193 | } 194 | else 195 | { 196 | Player[playerid][LoginAttempts]++; 197 | 198 | if (Player[playerid][LoginAttempts] >= 3) 199 | { 200 | ShowPlayerDialog(playerid, DIALOG_UNUSED, DIALOG_STYLE_MSGBOX, "Login", "You have mistyped your password too often (3 times).", "Okay", ""); 201 | DelayedKick(playerid); 202 | } 203 | else ShowPlayerDialog(playerid, DIALOG_LOGIN, DIALOG_STYLE_PASSWORD, "Login", "Wrong password!\nPlease enter your password in the field below:", "Login", "Abort"); 204 | } 205 | } 206 | case DIALOG_REGISTER: 207 | { 208 | if (!response) return Kick(playerid); 209 | 210 | if (strlen(inputtext) <= 5) return ShowPlayerDialog(playerid, DIALOG_REGISTER, DIALOG_STYLE_PASSWORD, "Registration", "Your password must be longer than 5 characters!\nPlease enter your password in the field below:", "Register", "Abort"); 211 | 212 | // 16 random characters from 33 to 126 (in ASCII) for the salt 213 | for (new i = 0; i < 16; i++) Player[playerid][Salt][i] = random(94) + 33; 214 | SHA256_PassHash(inputtext, Player[playerid][Salt], Player[playerid][Password], 65); 215 | 216 | // sends an INSERT query 217 | orm_save(Player[playerid][ORM_ID], "OnPlayerRegister", "d", playerid); 218 | } 219 | 220 | default: return 0; // dialog ID was not found, search in other scripts 221 | } 222 | return 1; 223 | } 224 | 225 | //----------------------------------------------------- 226 | 227 | forward OnPlayerDataLoaded(playerid, race_check); 228 | public OnPlayerDataLoaded(playerid, race_check) 229 | { 230 | /* race condition check: 231 | player A connects -> SELECT query is fired -> this query takes very long 232 | while the query is still processing, player A with playerid 2 disconnects 233 | player B joins now with playerid 2 -> our laggy SELECT query is finally finished, but for the wrong player 234 | 235 | what do we do against it? 236 | we create a connection count for each playerid and increase it everytime the playerid connects or disconnects 237 | we also pass the current value of the connection count to our OnPlayerDataLoaded callback 238 | then we check if current connection count is the same as connection count we passed to the callback 239 | if yes, everything is okay, if not, we just kick the player 240 | */ 241 | if (race_check != g_MysqlRaceCheck[playerid]) return Kick(playerid); 242 | 243 | orm_setkey(Player[playerid][ORM_ID], "id"); 244 | 245 | new string[115]; 246 | switch (orm_errno(Player[playerid][ORM_ID])) 247 | { 248 | case ERROR_OK: 249 | { 250 | format(string, sizeof string, "This account (%s) is registered. Please login by entering your password in the field below:", Player[playerid][Name]); 251 | ShowPlayerDialog(playerid, DIALOG_LOGIN, DIALOG_STYLE_PASSWORD, "Login", string, "Login", "Abort"); 252 | 253 | // from now on, the player has 30 seconds to login 254 | Player[playerid][LoginTimer] = SetTimerEx("OnLoginTimeout", SECONDS_TO_LOGIN * 1000, false, "d", playerid); 255 | } 256 | case ERROR_NO_DATA: 257 | { 258 | format(string, sizeof string, "Welcome %s, you can register by entering your password in the field below:", Player[playerid][Name]); 259 | ShowPlayerDialog(playerid, DIALOG_REGISTER, DIALOG_STYLE_PASSWORD, "Registration", string, "Register", "Abort"); 260 | } 261 | } 262 | return 1; 263 | } 264 | 265 | forward OnLoginTimeout(playerid); 266 | public OnLoginTimeout(playerid) 267 | { 268 | // reset the variable that stores the timerid 269 | Player[playerid][LoginTimer] = 0; 270 | 271 | ShowPlayerDialog(playerid, DIALOG_UNUSED, DIALOG_STYLE_MSGBOX, "Login", "You have been kicked for taking too long to login successfully to your account.", "Okay", ""); 272 | DelayedKick(playerid); 273 | return 1; 274 | } 275 | 276 | forward OnPlayerRegister(playerid); 277 | public OnPlayerRegister(playerid) 278 | { 279 | ShowPlayerDialog(playerid, DIALOG_UNUSED, DIALOG_STYLE_MSGBOX, "Registration", "Account successfully registered, you have been automatically logged in.", "Okay", ""); 280 | 281 | Player[playerid][IsLoggedIn] = true; 282 | 283 | Player[playerid][X_Pos] = DEFAULT_POS_X; 284 | Player[playerid][Y_Pos] = DEFAULT_POS_Y; 285 | Player[playerid][Z_Pos] = DEFAULT_POS_Z; 286 | Player[playerid][A_Pos] = DEFAULT_POS_A; 287 | 288 | SetSpawnInfo(playerid, NO_TEAM, 0, Player[playerid][X_Pos], Player[playerid][Y_Pos], Player[playerid][Z_Pos], Player[playerid][A_Pos], 0, 0, 0, 0, 0, 0); 289 | SpawnPlayer(playerid); 290 | return 1; 291 | } 292 | 293 | forward _KickPlayerDelayed(playerid); 294 | public _KickPlayerDelayed(playerid) 295 | { 296 | Kick(playerid); 297 | return 1; 298 | } 299 | 300 | 301 | //----------------------------------------------------- 302 | 303 | DelayedKick(playerid, time = 500) 304 | { 305 | SetTimerEx("_KickPlayerDelayed", time, false, "d", playerid); 306 | return 1; 307 | } 308 | 309 | SetupPlayerTable() 310 | { 311 | mysql_tquery(g_SQL, "CREATE TABLE IF NOT EXISTS `players` (`id` int(11) NOT NULL AUTO_INCREMENT,`username` varchar(24) NOT NULL,`password` char(64) NOT NULL,`salt` char(16) NOT NULL,`kills` mediumint(8) NOT NULL DEFAULT '0',`deaths` mediumint(8) NOT NULL DEFAULT '0',`x` float NOT NULL DEFAULT '0',`y` float NOT NULL DEFAULT '0',`z` float NOT NULL DEFAULT '0',`angle` float NOT NULL DEFAULT '0',`interior` tinyint(3) NOT NULL DEFAULT '0', PRIMARY KEY (`id`), UNIQUE KEY `username` (`username`))"); 312 | return 1; 313 | } 314 | 315 | UpdatePlayerData(playerid, reason) 316 | { 317 | if (Player[playerid][IsLoggedIn] == false) return 0; 318 | 319 | // if the client crashed, it's not possible to get the player's position in OnPlayerDisconnect callback 320 | // so we will use the last saved position (in case of a player who registered and crashed/kicked, the position will be the default spawn point) 321 | if (reason == 1) 322 | { 323 | GetPlayerPos(playerid, Player[playerid][X_Pos], Player[playerid][Y_Pos], Player[playerid][Z_Pos]); 324 | GetPlayerFacingAngle(playerid, Player[playerid][A_Pos]); 325 | } 326 | 327 | // it is important to store everything in the variables registered in ORM instance 328 | Player[playerid][Interior] = GetPlayerInterior(playerid); 329 | 330 | // orm_save sends an UPDATE query 331 | orm_save(Player[playerid][ORM_ID]); 332 | orm_destroy(Player[playerid][ORM_ID]); 333 | return 1; 334 | } 335 | 336 | UpdatePlayerDeaths(playerid) 337 | { 338 | if (Player[playerid][IsLoggedIn] == false) return 0; 339 | 340 | Player[playerid][Deaths]++; 341 | 342 | orm_update(Player[playerid][ORM_ID]); 343 | return 1; 344 | } 345 | 346 | UpdatePlayerKills(killerid) 347 | { 348 | // we must check before if the killer wasn't valid (connected) player to avoid run time error 4 349 | if (killerid == INVALID_PLAYER_ID) return 0; 350 | if (Player[killerid][IsLoggedIn] == false) return 0; 351 | 352 | Player[killerid][Kills]++; 353 | 354 | orm_update(Player[killerid][ORM_ID]); 355 | return 1; 356 | } 357 | -------------------------------------------------------------------------------- /pawn.json: -------------------------------------------------------------------------------- 1 | { 2 | "user": "pBlueG", 3 | "repo": "SA-MP-MySQL", 4 | "resources": [ 5 | { 6 | "name": "^mysql-.+-([Dd]ebian[0-9]?|[Ll]inux)-static\\.tar\\.gz$", 7 | "platform": "linux", 8 | "archive": true, 9 | "includes": ["mysql-.+\\/pawno\\/include"], 10 | "plugins": ["mysql-.+\\/plugins\\/mysql.so"], 11 | "files": { 12 | "mysql-.+\\/log-core.so": "log-core.so" 13 | } 14 | }, 15 | { 16 | "name": "^mysql-.+-win(32)?.zip$", 17 | "platform": "windows", 18 | "archive": true, 19 | "includes": ["mysql-.+\\/pawno\\/include"], 20 | "plugins": ["mysql-.+\\/plugins\\/mysql.dll"], 21 | "files": { 22 | "mysql-.+\\/log-core.dll": "log-core.dll", 23 | "mysql-.+\\/libmariadb.dll": "libmariadb.dll" 24 | } 25 | } 26 | ], 27 | "runtime": { 28 | "plugins": ["pBlueG/SA-MP-MySQL"] 29 | }, 30 | "contributors": [ 31 | "AndreT", 32 | "DamianC", 33 | "IstuntmanI", 34 | "JernejL", 35 | "Konstantinos", 36 | "krisk", 37 | "kurta999", 38 | "Kye", 39 | "maddinat0r", 40 | "Mow", 41 | "nemesis", 42 | "Sergei", 43 | "xxmitsu" 44 | ] 45 | } 46 | -------------------------------------------------------------------------------- /src/CCallback.cpp: -------------------------------------------------------------------------------- 1 | #include "CCallback.hpp" 2 | #include "CLog.hpp" 3 | 4 | #include 5 | 6 | 7 | const string CCallback::ModuleName{ "callback" }; 8 | 9 | 10 | Callback_t CCallback::Create(AMX *amx, const char *name, const char *format, 11 | cell *params, cell param_offset, 12 | CError &error) 13 | { 14 | CLog::Get()->Log(LogLevel::DEBUG, 15 | "CCallback::Create(amx={}, name='{}', format='{}', params={}, param_offset={})", 16 | static_cast(amx), 17 | name ? name : "(nullptr)", 18 | format ? format : "(nullptr)", 19 | static_cast(params), param_offset); 20 | 21 | if (amx == nullptr) 22 | { 23 | error.set(Error::INVALID_AMX, "invalid AMX"); 24 | return nullptr; 25 | } 26 | 27 | if (params == nullptr) 28 | { 29 | error.set(Error::INVALID_PARAMETERS, "invalid parameters"); 30 | return nullptr; 31 | } 32 | 33 | if (name == nullptr || strlen(name) == 0) 34 | { 35 | error.set(Error::EMPTY_NAME, "empty name specified"); 36 | return nullptr; 37 | } 38 | 39 | int cb_idx = -1; 40 | if (amx_FindPublic(amx, name, &cb_idx) != AMX_ERR_NONE || cb_idx < 0) 41 | { 42 | error.set(Error::NOT_FOUND, "callback \"{}\" does not exist", name); 43 | return nullptr; 44 | } 45 | 46 | CLog::Get()->Log(LogLevel::DEBUG, 47 | "CCallback::Create - callback index for '{}': {}", 48 | name, cb_idx); 49 | 50 | 51 | const size_t num_params = (format == nullptr) ? 0 : strlen(format); 52 | if ((params[0] / sizeof(cell) - (param_offset - 1)) != num_params) 53 | { 54 | error.set(Error::INVALID_PARAM_COUNT, 55 | "parameter count does not match format specifier length"); 56 | return nullptr; 57 | } 58 | 59 | ParamList_t param_list; 60 | if (num_params != 0) 61 | { 62 | cell param_idx = 0; 63 | cell *address_ptr = nullptr; 64 | cell *array_addr_ptr = nullptr; 65 | 66 | do 67 | { 68 | if (array_addr_ptr != nullptr && (*format) != 'd' && (*format) != 'i') 69 | { 70 | error.set(Error::EXPECTED_ARRAY_SIZE, 71 | "expected 'd'/'i' specifier for array size (got '{}' instead)", 72 | *format); 73 | return nullptr; 74 | } 75 | 76 | CLog::Get()->Log(LogLevel::DEBUG, 77 | "processing specifier '{}' with parameter index {}", 78 | *format, param_idx); 79 | 80 | switch (*format) 81 | { 82 | case 'd': //decimal 83 | case 'i': //integer 84 | { 85 | amx_GetAddr(amx, params[param_offset + param_idx], &address_ptr); 86 | cell value = *address_ptr; 87 | if (array_addr_ptr != nullptr) 88 | { 89 | CLog::Get()->Log(LogLevel::DEBUG, "expecting array size"); 90 | if (value <= 0) 91 | { 92 | error.set(Error::INVALID_ARRAY_SIZE, 93 | "invalid array size '{}'", value); 94 | return nullptr; 95 | } 96 | cell *copied_array = 97 | static_cast(malloc(value * sizeof(cell))); 98 | memcpy(copied_array, array_addr_ptr, value * sizeof(cell)); 99 | 100 | param_list.push_front( 101 | std::make_tuple( 102 | 'a', std::make_tuple( 103 | copied_array, value))); 104 | CLog::Get()->Log(LogLevel::DEBUG, "pushed array"); 105 | array_addr_ptr = nullptr; 106 | } 107 | 108 | param_list.push_front(std::make_tuple('c', value)); 109 | CLog::Get()->Log(LogLevel::DEBUG, 110 | "retrieved and pushed value '{}'", value); 111 | } 112 | break; 113 | case 'f': //float 114 | case 'b': //bool 115 | amx_GetAddr(amx, params[param_offset + param_idx], &address_ptr); 116 | param_list.push_front(std::make_tuple('c', *address_ptr)); 117 | CLog::Get()->Log(LogLevel::DEBUG, 118 | "retrieved and pushed value '{}'", *address_ptr); 119 | break; 120 | case 's': //string 121 | { 122 | const char *str = nullptr; 123 | amx_StrParam(amx, params[param_offset + param_idx], str); 124 | param_list.push_front(std::make_tuple('s', string(str ? str : ""))); 125 | CLog::Get()->Log(LogLevel::DEBUG, "retrieved and pushed value '{}'", str ? str : ""); 126 | } 127 | break; 128 | case 'a': //array 129 | amx_GetAddr(amx, params[param_offset + param_idx], &array_addr_ptr); 130 | CLog::Get()->Log(LogLevel::DEBUG, "retrieved array '{}'", 131 | static_cast(array_addr_ptr)); 132 | break; 133 | case 'r': //reference 134 | amx_GetAddr(amx, params[param_offset + param_idx], &address_ptr); 135 | param_list.push_front(std::make_tuple('r', address_ptr)); 136 | CLog::Get()->Log(LogLevel::DEBUG, "retrieved and pushed reference '{}'", 137 | static_cast(array_addr_ptr)); 138 | break; 139 | default: 140 | error.set(Error::INVALID_FORMAT_SPECIFIER, 141 | "invalid format specifier '{}'", *format); 142 | return nullptr; 143 | } 144 | param_idx++; 145 | } 146 | while (*(++format) != '\0'); 147 | 148 | if (array_addr_ptr != nullptr) 149 | { 150 | error.set(Error::NO_ARRAY_SIZE, 151 | "no array size specified after 'a' specifier"); 152 | return nullptr; 153 | } 154 | } 155 | 156 | CLog::Get()->Log(LogLevel::INFO, 157 | "Callback '{}' set up for delayed execution.", name); 158 | CLog::Get()->Log(LogLevel::DEBUG, 159 | "created delayed callback with {} parameter{}", 160 | param_list.size(), param_list.size() > 1 ? "s" : ""); 161 | return std::make_shared(amx, cb_idx, std::move(param_list)); 162 | } 163 | 164 | Callback_t CCallback::Create(CError &error, AMX *amx, 165 | const char *name, const char *format, ...) 166 | { 167 | CLog::Get()->Log(LogLevel::DEBUG, 168 | "CCallback::Create(amx={}, name='{}', format='{})", 169 | static_cast(amx), 170 | name ? name : "(nullptr)", 171 | format ? format : "(nullptr)"); 172 | 173 | if (amx == nullptr) 174 | { 175 | error.set(Error::INVALID_AMX, "invalid AMX"); 176 | return nullptr; 177 | } 178 | 179 | if (name == nullptr || strlen(name) == 0) 180 | { 181 | error.set(Error::EMPTY_NAME, "empty name specified"); 182 | return nullptr; 183 | } 184 | 185 | int cb_idx = -1; 186 | if (amx_FindPublic(amx, name, &cb_idx) != AMX_ERR_NONE || cb_idx < 0) 187 | { 188 | error.set(Error::NOT_FOUND, "callback \"{}\" does not exist", name); 189 | return nullptr; 190 | } 191 | 192 | CLog::Get()->Log(LogLevel::DEBUG, 193 | "CCallback::Create - callback index for '{}': {}", 194 | name, cb_idx); 195 | 196 | 197 | size_t num_params = (format == nullptr) ? 0 : strlen(format); 198 | ParamList_t param_list; 199 | if (num_params != 0) 200 | { 201 | va_list args; 202 | va_start(args, format); 203 | 204 | for (; *format != '\0'; ++format) 205 | { 206 | CLog::Get()->Log(LogLevel::DEBUG, 207 | "processing specifier '{}'", *format); 208 | 209 | switch (*format) 210 | { 211 | case 'd': //decimal 212 | case 'i': //integer 213 | case 'b': //bool 214 | { 215 | cell value = va_arg(args, cell); 216 | param_list.push_front(std::make_tuple('c', value)); 217 | CLog::Get()->Log(LogLevel::DEBUG, 218 | "retrieved and pushed value '{}'", value); 219 | } 220 | break; 221 | case 'f': //float 222 | { 223 | float float_value = static_cast(va_arg(args, double)); 224 | param_list.push_front(std::make_tuple('c', amx_ftoc(float_value))); 225 | CLog::Get()->Log(LogLevel::DEBUG, 226 | "retrieved and pushed value '{}'", float_value); 227 | } 228 | break; 229 | case 's': //string 230 | { 231 | const char *value = va_arg(args, const char*); 232 | param_list.push_front(std::make_tuple('s', string(value))); 233 | CLog::Get()->Log(LogLevel::DEBUG, 234 | "retrieved and pushed value '{}'", value); 235 | } 236 | break; 237 | default: 238 | error.set(Error::INVALID_FORMAT_SPECIFIER, 239 | "invalid format specifier '{}'", *format); 240 | return nullptr; 241 | } 242 | } 243 | 244 | va_end(args); 245 | } 246 | 247 | if (param_list.size() != num_params) 248 | { 249 | error.set(Error::INVALID_PARAM_COUNT, 250 | "parameter count does not match format specifier length"); 251 | return nullptr; 252 | } 253 | 254 | CLog::Get()->Log(LogLevel::INFO, 255 | "Callback '{}' set up for delayed execution.", name); 256 | CLog::Get()->Log(LogLevel::DEBUG, 257 | "created delayed callback with {} parameter{}", 258 | param_list.size(), param_list.size() > 1 ? "s" : ""); 259 | 260 | return std::make_shared(amx, cb_idx, std::move(param_list)); 261 | } 262 | 263 | 264 | bool CCallback::Execute() 265 | { 266 | CLog::Get()->Log(LogLevel::DEBUG, 267 | "CCallback::Execute(amx={}, index={}, num_params={})", 268 | static_cast(m_AmxInstance), 269 | m_AmxCallbackIndex, m_Params.size()); 270 | 271 | //the user could unload a filterscript between CCallback creation and 272 | //execution, so we better check if the AMX instance is still valid 273 | if (CCallbackManager::Get()->IsValidAmx(m_AmxInstance) == false) 274 | { 275 | CLog::Get()->Log(LogLevel::ERROR, 276 | "CCallback::Execute - invalid AMX instance"); 277 | return false; 278 | } 279 | 280 | if (CLog::Get()->IsLogLevel(LogLevel::INFO)) 281 | { 282 | char callback_name[sNAMEMAX + 1]; 283 | if (amx_GetPublic(m_AmxInstance, m_AmxCallbackIndex, callback_name) == AMX_ERR_NONE) 284 | { 285 | CLog::Get()->Log(LogLevel::INFO, 286 | "Executing callback '{}' with {} parameter{}...", 287 | callback_name, m_Params.size(), m_Params.size() > 1 ? "s" : ""); 288 | } 289 | } 290 | 291 | cell amx_address = -1; 292 | for (auto &i : m_Params) 293 | { 294 | cell tmp_addr; 295 | boost::any ¶m_val = std::get<1>(i); 296 | char specifier = std::get<0>(i); 297 | 298 | CLog::Get()->Log(LogLevel::DEBUG, 299 | "processing internal specifier '{}'", specifier); 300 | switch (specifier) 301 | { 302 | case 'c': //cell 303 | { 304 | const cell value = boost::any_cast(param_val); 305 | amx_Push(m_AmxInstance, value); 306 | 307 | CLog::Get()->Log(LogLevel::DEBUG, 308 | "pushed value '{}' onto AMX stack", value); 309 | } 310 | break; 311 | case 's': //string 312 | { 313 | const string value = boost::any_cast(param_val); 314 | amx_PushString(m_AmxInstance, &tmp_addr, nullptr, 315 | value.c_str(), 0, 0); 316 | 317 | CLog::Get()->Log(LogLevel::DEBUG, 318 | "pushed value '{}' onto AMX stack", value); 319 | 320 | if (amx_address < 0) 321 | amx_address = tmp_addr; 322 | } 323 | break; 324 | case 'a': //array 325 | { 326 | auto array_tuple = boost::any_cast>(param_val); 327 | cell *array_addr = std::get<0>(array_tuple); 328 | cell array_size = std::get<1>(array_tuple); 329 | amx_PushArray(m_AmxInstance, &tmp_addr, nullptr, array_addr, array_size); 330 | free(array_addr); 331 | 332 | CLog::Get()->Log(LogLevel::DEBUG, 333 | "pushed array '{}' with size '{}' onto AMX stack", 334 | static_cast(array_addr), array_size); 335 | 336 | if (amx_address < 0) 337 | amx_address = tmp_addr; 338 | } 339 | break; 340 | case 'r': //reference 341 | { 342 | cell * const value = boost::any_cast(param_val); 343 | amx_PushAddress(m_AmxInstance, value); 344 | 345 | CLog::Get()->Log(LogLevel::DEBUG, 346 | "pushed reference '{}' onto AMX stack", 347 | static_cast(value)); 348 | } 349 | break; 350 | } 351 | } 352 | 353 | CLog::Get()->Log(LogLevel::DEBUG, 354 | "executing AMX callback with index '{}'", 355 | m_AmxCallbackIndex); 356 | 357 | int error = amx_Exec(m_AmxInstance, nullptr, m_AmxCallbackIndex); 358 | 359 | CLog::Get()->Log(LogLevel::DEBUG, 360 | "AMX callback executed with error '{}'", 361 | error); 362 | 363 | if (amx_address >= 0) 364 | amx_Release(m_AmxInstance, amx_address); 365 | 366 | CLog::Get()->Log(LogLevel::INFO, "Callback successfully executed."); 367 | 368 | return true; 369 | } 370 | -------------------------------------------------------------------------------- /src/CCallback.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "CSingleton.hpp" 4 | #include "sdk.hpp" 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | using std::string; 16 | using std::queue; 17 | using std::function; 18 | using std::list; 19 | using std::unordered_set; 20 | using std::tuple; 21 | 22 | #include "CError.hpp" 23 | #include "types.hpp" 24 | 25 | 26 | class CCallback 27 | { 28 | public: //type definitions 29 | using ParamList_t = list>; 30 | 31 | enum class Error 32 | { 33 | NONE, 34 | INVALID_AMX, 35 | INVALID_PARAMETERS, 36 | INVALID_PARAM_COUNT, 37 | INVALID_FORMAT_SPECIFIER, 38 | EMPTY_NAME, 39 | NOT_FOUND, 40 | EXPECTED_ARRAY_SIZE, 41 | INVALID_ARRAY_SIZE, 42 | NO_ARRAY_SIZE, 43 | }; 44 | static const string ModuleName; 45 | 46 | public: //constructor / destructor 47 | CCallback(AMX *amx, int cb_idx, ParamList_t &¶ms) : 48 | m_AmxInstance(amx), 49 | m_AmxCallbackIndex(cb_idx), 50 | m_Params(params) 51 | { } 52 | ~CCallback() = default; 53 | 54 | private: //variables 55 | AMX *m_AmxInstance = nullptr; 56 | int m_AmxCallbackIndex = -1; 57 | 58 | ParamList_t m_Params; 59 | 60 | public: //functions 61 | bool Execute(); 62 | 63 | public: //factory functions 64 | static Callback_t Create(AMX *amx, const char *name, const char *format, 65 | cell *params, cell param_offset, 66 | CError &error); 67 | 68 | static Callback_t Create(CError &error, 69 | AMX *amx, const char *name, const char *format, ...); 70 | }; 71 | 72 | 73 | class CCallbackManager : public CSingleton 74 | { 75 | friend class CSingleton; 76 | private: //constructor / destructor 77 | CCallbackManager() = default; 78 | ~CCallbackManager() = default; 79 | 80 | 81 | private: //variables 82 | unordered_set m_AmxInstances; 83 | 84 | 85 | public: //functions 86 | inline bool IsValidAmx(const AMX *amx) 87 | { 88 | return m_AmxInstances.count(amx) == 1; 89 | } 90 | 91 | inline void AddAmx(const AMX *amx) 92 | { 93 | m_AmxInstances.insert(amx); 94 | } 95 | inline void RemoveAmx(const AMX *amx) 96 | { 97 | m_AmxInstances.erase(amx); 98 | } 99 | 100 | }; 101 | -------------------------------------------------------------------------------- /src/CConnection.cpp: -------------------------------------------------------------------------------- 1 | #include "mysql.hpp" 2 | 3 | #include "CQuery.hpp" 4 | #include "CConnection.hpp" 5 | #include "CDispatcher.hpp" 6 | #include "COptions.hpp" 7 | #include "CLog.hpp" 8 | 9 | 10 | CConnection::CConnection(const char *host, const char *user, const char *passw, 11 | const char *db, const COptions *options) 12 | { 13 | CLog::Get()->Log(LogLevel::DEBUG, 14 | "CConnection::CConnection(this={}, host='{}', user='{}', passw='****', db='{}', options={})", 15 | static_cast(this), 16 | host ? host : "(nullptr)", 17 | user ? user : "(nullptr)", 18 | db ? db : "(nullptr)", 19 | static_cast(options)); 20 | 21 | assert(options != nullptr); 22 | 23 | //initialize 24 | m_Connection = mysql_init(nullptr); 25 | if (m_Connection == nullptr) 26 | { 27 | CLog::Get()->Log(LogLevel::ERROR, 28 | "CConnection::CConnection - MySQL initialization failed " \ 29 | "(not enough memory available)"); 30 | return; 31 | } 32 | 33 | if (options->GetOption(COptions::Type::SSL_ENABLE)) 34 | { 35 | string 36 | key = options->GetOption(COptions::Type::SSL_KEY_FILE), 37 | cert = options->GetOption(COptions::Type::SSL_CERT_FILE), 38 | ca = options->GetOption(COptions::Type::SSL_CA_FILE), 39 | capath = options->GetOption(COptions::Type::SSL_CA_PATH), 40 | cipher = options->GetOption(COptions::Type::SSL_CIPHER); 41 | 42 | 43 | mysql_ssl_set(m_Connection, 44 | key.empty() ? nullptr : key.c_str(), 45 | cert.empty() ? nullptr : cert.c_str(), 46 | ca.empty() ? nullptr : ca.c_str(), 47 | capath.empty() ? nullptr : capath.c_str(), 48 | cipher.empty() ? nullptr : cipher.c_str()); 49 | } 50 | 51 | //prepare connection flags through passed options 52 | unsigned long connect_flags = 0; 53 | if (options->GetOption(COptions::Type::MULTI_STATEMENTS) == true) 54 | connect_flags |= CLIENT_MULTI_STATEMENTS; 55 | 56 | //connect 57 | auto *result = mysql_real_connect(m_Connection, host, user, passw, db, 58 | options->GetOption(COptions::Type::SERVER_PORT), 59 | nullptr, connect_flags); 60 | 61 | if (result == nullptr) 62 | { 63 | CLog::Get()->Log(LogLevel::ERROR, 64 | "CConnection::CConnection - establishing connection to " \ 65 | "MySQL database failed: #{} '{}'", 66 | mysql_errno(m_Connection), mysql_error(m_Connection)); 67 | return; 68 | } 69 | 70 | m_IsConnected = true; 71 | 72 | //set additional connection options 73 | bool reconnect = options->GetOption(COptions::Type::AUTO_RECONNECT); 74 | mysql_options(m_Connection, MYSQL_OPT_RECONNECT, &reconnect); 75 | 76 | CLog::Get()->Log(LogLevel::DEBUG, 77 | "CConnection::CConnection - new connection = {}", 78 | static_cast(m_Connection)); 79 | } 80 | 81 | CConnection::~CConnection() 82 | { 83 | CLog::Get()->Log(LogLevel::DEBUG, 84 | "CConnection::~CConnection(this={}, connection={})", 85 | static_cast(this), 86 | static_cast(m_Connection)); 87 | 88 | std::lock_guard lock_guard(m_Mutex); 89 | if (IsConnected()) 90 | mysql_close(m_Connection); 91 | } 92 | 93 | 94 | bool CConnection::EscapeString(const char *src, string &dest) 95 | { 96 | CLog::Get()->Log(LogLevel::DEBUG, 97 | "CConnection::EscapeString(src='{}', this={}, connection={})", 98 | src ? src : "(nullptr)", 99 | static_cast(this), 100 | static_cast(m_Connection)); 101 | 102 | if (IsConnected() == false || src == nullptr) 103 | return false; 104 | 105 | const size_t src_len = strlen(src); 106 | 107 | dest.resize(src_len * 2 + 1); 108 | 109 | std::lock_guard lock_guard(m_Mutex); 110 | size_t newsize = mysql_real_escape_string(m_Connection, &dest[0], src, src_len); 111 | dest.resize(newsize); 112 | return true; 113 | } 114 | 115 | bool CConnection::SetCharset(string charset) 116 | { 117 | CLog::Get()->Log(LogLevel::DEBUG, 118 | "CConnection::SetCharset(charset='{}', this={}, connection={})", 119 | charset, static_cast(this), 120 | static_cast(m_Connection)); 121 | 122 | if (IsConnected() == false || charset.empty()) 123 | return false; 124 | 125 | std::lock_guard lock_guard(m_Mutex); 126 | int error = mysql_set_character_set(m_Connection, charset.c_str()); 127 | if (error != 0) 128 | return false; 129 | 130 | return true; 131 | } 132 | 133 | bool CConnection::GetCharset(string &charset) 134 | { 135 | CLog::Get()->Log(LogLevel::DEBUG, "CConnection::GetCharset(this={}, connection={})", 136 | static_cast(this), 137 | static_cast(m_Connection)); 138 | 139 | if (IsConnected() == false) 140 | return false; 141 | 142 | std::lock_guard lock_guard(m_Mutex); 143 | charset = mysql_character_set_name(m_Connection); 144 | 145 | return true; 146 | } 147 | 148 | bool CConnection::Execute(Query_t query) 149 | { 150 | CLog::Get()->Log(LogLevel::DEBUG, 151 | "CConnection::Execute(query={}, this={}, connection={})", 152 | static_cast(query.get()), 153 | static_cast(this), 154 | static_cast(m_Connection)); 155 | 156 | std::lock_guard lock_guard(m_Mutex); 157 | return IsConnected() && query->Execute(m_Connection); 158 | } 159 | 160 | bool CConnection::GetError(unsigned int &id, string &msg) 161 | { 162 | CLog::Get()->Log(LogLevel::DEBUG, 163 | "CConnection::GetError(this={}, connection={})", 164 | static_cast(this), 165 | static_cast(m_Connection)); 166 | 167 | if (m_Connection == nullptr) 168 | return false; 169 | 170 | std::lock_guard lock_guard(m_Mutex); 171 | id = mysql_errno(m_Connection); 172 | msg = mysql_error(m_Connection); 173 | 174 | return true; 175 | } 176 | 177 | bool CConnection::GetStatus(string &stat) 178 | { 179 | CLog::Get()->Log(LogLevel::DEBUG, 180 | "CConnection::GetStatus(this={}, connection={})", 181 | static_cast(this), 182 | static_cast(m_Connection)); 183 | 184 | if (IsConnected() == false) 185 | return false; 186 | 187 | std::lock_guard lock_guard(m_Mutex); 188 | const char *stat_raw = mysql_stat(m_Connection); 189 | 190 | if (stat_raw == nullptr) 191 | return false; 192 | 193 | stat = stat_raw; 194 | return true; 195 | } 196 | 197 | 198 | 199 | CThreadedConnection::CThreadedConnection( 200 | const char *host, const char *user, const char *passw, const char *db, 201 | const COptions *options) 202 | : 203 | m_Connection(host, user, passw, db, options), 204 | m_UnprocessedQueries(0), 205 | m_WorkerThreadActive(true), 206 | m_WorkerThread(std::bind(&CThreadedConnection::WorkerFunc, this)) 207 | { 208 | CLog::Get()->Log(LogLevel::DEBUG, 209 | "CThreadedConnection::CThreadedConnection(this={}, connection={})", 210 | static_cast(this), 211 | static_cast(&m_Connection)); 212 | } 213 | 214 | void CThreadedConnection::WorkerFunc() 215 | { 216 | CLog::Get()->Log(LogLevel::DEBUG, 217 | "CThreadedConnection::WorkerFunc(this={}, connection={})", 218 | static_cast(this), 219 | static_cast(&m_Connection)); 220 | 221 | std::unique_lock lock(m_QueueNotifierMutex); 222 | 223 | mysql_thread_init(); 224 | 225 | while (m_WorkerThreadActive) 226 | { 227 | m_QueueNotifier.wait(lock); 228 | Query_t query; 229 | while (m_Queue.pop(query)) 230 | { 231 | DispatchFunction_t func; 232 | if (m_Connection.Execute(query)) 233 | { 234 | func = std::bind(&CQuery::CallCallback, query); 235 | } 236 | else 237 | { 238 | unsigned int errorid = 0; 239 | string error; 240 | m_Connection.GetError(errorid, error); 241 | func = std::bind(&CQuery::CallErrorCallback, query, errorid, error); 242 | } 243 | 244 | --m_UnprocessedQueries; 245 | CDispatcher::Get()->Dispatch(std::move(func)); 246 | } 247 | } 248 | 249 | CLog::Get()->Log(LogLevel::DEBUG, 250 | "CThreadedConnection::WorkerFunc(this={}, connection={}) - shutting down", 251 | static_cast(this), 252 | static_cast(&m_Connection)); 253 | 254 | mysql_thread_end(); 255 | } 256 | 257 | bool CThreadedConnection::Queue(Query_t query) 258 | { 259 | if (!m_Queue.push(query)) 260 | return false; 261 | 262 | ++m_UnprocessedQueries; 263 | m_QueueNotifier.notify_one(); 264 | return true; 265 | } 266 | 267 | CThreadedConnection::~CThreadedConnection() 268 | { 269 | CLog::Get()->Log(LogLevel::DEBUG, 270 | "CThreadedConnection::~CThreadedConnection(this={}, connection={})", 271 | static_cast(this), 272 | static_cast(&m_Connection)); 273 | 274 | { 275 | std::lock_guard lock_guard(m_QueueNotifierMutex); 276 | m_WorkerThreadActive = false; 277 | } 278 | m_QueueNotifier.notify_one(); 279 | m_WorkerThread.join(); 280 | } 281 | 282 | 283 | 284 | CConnectionPool::CConnectionPool( 285 | const size_t size, const char *host, const char *user, const char *passw, 286 | const char *db, const COptions *options) 287 | { 288 | CLog::Get()->Log(LogLevel::DEBUG, 289 | "CConnectionPool::CConnectionPool(size={}, this={})", 290 | size, static_cast(this)); 291 | 292 | assert(size != 0); 293 | 294 | std::lock_guard lock_guard(m_PoolMutex); 295 | 296 | SConnectionNode *node = m_CurrentNode = new SConnectionNode; 297 | 298 | for (size_t i = 0; i != size; ++i) 299 | { 300 | SConnectionNode *old_node = node; 301 | old_node->Connection = new CThreadedConnection(host, user, passw, 302 | db, options); 303 | old_node->Next = ((i + 1) == size) ? m_CurrentNode : (node = new SConnectionNode); 304 | } 305 | } 306 | 307 | bool CConnectionPool::Queue(Query_t query) 308 | { 309 | CLog::Get()->Log(LogLevel::DEBUG, 310 | "CConnectionPool::Queue(query={}, this={})", 311 | static_cast(query.get()), 312 | static_cast(this)); 313 | 314 | std::lock_guard lock_guard(m_PoolMutex); 315 | auto *connection = m_CurrentNode->Connection; 316 | 317 | m_CurrentNode = m_CurrentNode->Next; 318 | assert(m_CurrentNode != nullptr && connection != nullptr); 319 | 320 | return connection->Queue(query); 321 | } 322 | 323 | bool CConnectionPool::SetCharset(string charset) 324 | { 325 | CLog::Get()->Log(LogLevel::DEBUG, 326 | "CConnectionPool::SetCharset(charset='{}', this={})", 327 | charset, static_cast(this)); 328 | 329 | std::lock_guard lock_guard(m_PoolMutex); 330 | SConnectionNode *node = m_CurrentNode; 331 | 332 | do 333 | { 334 | if (node->Connection->SetCharset(charset) == false) 335 | return false; 336 | 337 | } 338 | while ((node = node->Next) != m_CurrentNode); 339 | 340 | return true; 341 | } 342 | 343 | unsigned int CConnectionPool::GetUnprocessedQueryCount() 344 | { 345 | CLog::Get()->Log(LogLevel::DEBUG, 346 | "CConnectionPool::GetUnprocessedQueryCount(this={})", 347 | static_cast(this)); 348 | 349 | std::lock_guard lock_guard(m_PoolMutex); 350 | SConnectionNode *node = m_CurrentNode; 351 | 352 | unsigned int count = 0; 353 | do 354 | { 355 | count += node->Connection->GetUnprocessedQueryCount(); 356 | } 357 | while ((node = node->Next) != m_CurrentNode); 358 | 359 | return count; 360 | } 361 | 362 | CConnectionPool::~CConnectionPool() 363 | { 364 | CLog::Get()->Log(LogLevel::DEBUG, 365 | "CConnectionPool::~CConnectionPool(this={})", 366 | static_cast(this)); 367 | 368 | std::lock_guard lock_guard(m_PoolMutex); 369 | SConnectionNode *node = m_CurrentNode; 370 | 371 | do 372 | { 373 | delete node->Connection; 374 | 375 | auto *old_node = node; 376 | node = node->Next; 377 | delete old_node; 378 | } 379 | while (node != m_CurrentNode); 380 | } 381 | -------------------------------------------------------------------------------- /src/CConnection.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | using std::string; 12 | 13 | #include "types.hpp" 14 | 15 | 16 | class CConnection 17 | { 18 | public: //constructor / deconstructor 19 | CConnection( 20 | const char *host, const char *user, const char *passw, const char *db, 21 | const COptions *options); 22 | ~CConnection(); 23 | CConnection(const CConnection &rhs) = delete; 24 | 25 | private: //variables 26 | std::mutex m_Mutex; //protect every MySQL C API call 27 | 28 | MYSQL *m_Connection = nullptr; 29 | bool m_IsConnected = false; 30 | 31 | public: //functions 32 | inline bool IsConnected() const 33 | { 34 | return m_Connection != nullptr && m_IsConnected; 35 | } 36 | bool EscapeString(const char *src, string &dest); 37 | bool SetCharset(string charset); 38 | bool GetCharset(string &charset); 39 | bool Execute(Query_t query); 40 | bool GetError(unsigned int &id, string &msg); 41 | bool GetStatus(string &stat); 42 | 43 | }; 44 | 45 | class CThreadedConnection 46 | { 47 | public: 48 | CThreadedConnection( 49 | const char *host, const char *user, const char *passw, const char *db, 50 | const COptions *options); 51 | ~CThreadedConnection(); 52 | CThreadedConnection(const CThreadedConnection &rhs) = delete; 53 | 54 | private: 55 | CConnection m_Connection; 56 | 57 | std::mutex m_QueueNotifierMutex; 58 | std::condition_variable m_QueueNotifier; 59 | boost::lockfree::spsc_queue < Query_t, 60 | boost::lockfree::fixed_sized < true >, 61 | boost::lockfree::capacity < 65536 >> m_Queue; 62 | 63 | std::atomic m_UnprocessedQueries; 64 | 65 | std::atomic m_WorkerThreadActive; 66 | std::thread m_WorkerThread; 67 | 68 | private: 69 | void WorkerFunc(); 70 | 71 | public: 72 | bool Queue(Query_t query); 73 | inline bool SetCharset(string charset) 74 | { 75 | return m_Connection.SetCharset(charset); 76 | } 77 | inline unsigned int GetUnprocessedQueryCount() 78 | { 79 | return m_UnprocessedQueries; 80 | } 81 | 82 | }; 83 | 84 | class CConnectionPool 85 | { 86 | public: 87 | CConnectionPool( 88 | const size_t size, const char *host, const char *user, 89 | const char *passw, const char *db, 90 | const COptions *options); 91 | ~CConnectionPool(); 92 | CConnectionPool(const CConnectionPool &rhs) = delete; 93 | 94 | private: 95 | std::mutex m_PoolMutex; 96 | 97 | struct SConnectionNode 98 | { 99 | CThreadedConnection *Connection; 100 | SConnectionNode *Next; 101 | }; 102 | 103 | SConnectionNode *m_CurrentNode; 104 | 105 | public: 106 | bool Queue(Query_t query); 107 | bool SetCharset(string charset); 108 | unsigned int GetUnprocessedQueryCount(); 109 | 110 | }; 111 | -------------------------------------------------------------------------------- /src/CDispatcher.cpp: -------------------------------------------------------------------------------- 1 | #include "CDispatcher.hpp" 2 | 3 | 4 | void CDispatcher::Dispatch(DispatchFunction_t &&func) 5 | { 6 | std::lock_guard lock_guard(m_QueueMtx); 7 | return m_Queue.push(std::move(func)); 8 | } 9 | 10 | void CDispatcher::Process() 11 | { 12 | std::lock_guard lock_guard(m_QueueMtx); 13 | while (m_Queue.empty() == false) 14 | { 15 | m_Queue.front()(); 16 | m_Queue.pop(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/CDispatcher.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "CSingleton.hpp" 4 | 5 | #include 6 | #include 7 | 8 | #include "types.hpp" 9 | 10 | 11 | class CDispatcher : public CSingleton < CDispatcher > 12 | { 13 | friend class CSingleton < CDispatcher >; 14 | public: //type definitions 15 | 16 | private: //constructor / destructor 17 | CDispatcher() = default; 18 | ~CDispatcher() = default; 19 | 20 | private: //variables 21 | std::queue m_Queue; 22 | std::mutex m_QueueMtx; 23 | 24 | public: //functions 25 | void Dispatch(DispatchFunction_t &&func); 26 | void Process(); 27 | 28 | }; 29 | -------------------------------------------------------------------------------- /src/CError.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | using std::string; 7 | 8 | 9 | template 10 | class CError 11 | { 12 | using ErrorType = typename T::Error; 13 | public: 14 | CError() : 15 | m_Type(ErrorType::NONE) 16 | { } 17 | CError(ErrorType type, string &&msg) : 18 | m_Type(type), 19 | m_Message(std::move(msg)) 20 | { } 21 | template 22 | CError(ErrorType type, string &&format, Args &&...args) : 23 | m_Type(type), 24 | m_Message(fmt::format(format, std::forward(args)...)) 25 | { } 26 | ~CError() = default; 27 | 28 | operator bool() const 29 | { 30 | return m_Type != ErrorType::NONE; 31 | } 32 | 33 | const string &msg() const 34 | { 35 | return m_Message; 36 | } 37 | const ErrorType type() const 38 | { 39 | return m_Type; 40 | } 41 | const string &module() const 42 | { 43 | return T::ModuleName; 44 | } 45 | 46 | void set(ErrorType type, string &&msg) 47 | { 48 | m_Type = type; 49 | m_Message.assign(std::move(msg)); 50 | } 51 | template 52 | void set(ErrorType type, string &&format, Args &&...args) 53 | { 54 | m_Type = type; 55 | m_Message.assign(fmt::format(format, std::forward(args)...)); 56 | } 57 | 58 | private: 59 | ErrorType m_Type; 60 | string m_Message; 61 | }; 62 | -------------------------------------------------------------------------------- /src/CHandle.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #pragma warning (disable: 4348) //silence boost spirit warnings 3 | #include 4 | 5 | #include "mysql.hpp" 6 | 7 | #include "CQuery.hpp" 8 | #include "CHandle.hpp" 9 | #include "CConnection.hpp" 10 | #include "COptions.hpp" 11 | #include "misc.hpp" 12 | 13 | #include 14 | 15 | 16 | const string CHandle::ModuleName{ "handle" }; 17 | 18 | 19 | CHandle::~CHandle() 20 | { 21 | CLog::Get()->Log(LogLevel::DEBUG, "CHandle::~CHandle(this={})", 22 | static_cast(this)); 23 | 24 | if (m_MainConnection != nullptr) 25 | delete m_MainConnection; 26 | 27 | if (m_ThreadedConnection != nullptr) 28 | delete m_ThreadedConnection; 29 | 30 | if (m_ConnectionPool != nullptr) 31 | delete m_ConnectionPool; 32 | } 33 | 34 | bool CHandle::Execute(ExecutionType type, Query_t query) 35 | { 36 | CLog::Get()->Log(LogLevel::DEBUG, "CHandle::Execute(this={}, type={}, query={})", 37 | static_cast(this), 38 | static_cast::type>(type), 39 | static_cast(query.get())); 40 | 41 | bool return_val = false; 42 | if (query) 43 | { 44 | switch (type) 45 | { 46 | case ExecutionType::THREADED: 47 | if (m_ThreadedConnection != nullptr) 48 | return_val = m_ThreadedConnection->Queue(query); 49 | break; 50 | case ExecutionType::PARALLEL: 51 | if (m_ConnectionPool != nullptr) 52 | return_val = m_ConnectionPool->Queue(query); 53 | break; 54 | case ExecutionType::UNTHREADED: 55 | if (m_MainConnection != nullptr) 56 | return_val = m_MainConnection->Execute(query); 57 | break; 58 | } 59 | } 60 | 61 | CLog::Get()->Log(LogLevel::DEBUG, 62 | "CHandle::Execute - return value: {}", return_val); 63 | return return_val; 64 | } 65 | 66 | bool CHandle::GetErrorId(unsigned int &errorid) 67 | { 68 | CLog::Get()->Log(LogLevel::DEBUG, "CHandle::GetErrorId(this={})", 69 | static_cast(this)); 70 | 71 | if (m_MainConnection == nullptr) 72 | return false; 73 | 74 | string unused_errormsg; 75 | bool return_val = m_MainConnection->GetError(errorid, unused_errormsg); 76 | 77 | CLog::Get()->Log(LogLevel::DEBUG, 78 | "CHandle::GetErrorId - " \ 79 | "return value: {}, error id: '{}', error msg: '{}'", 80 | return_val, errorid, unused_errormsg); 81 | 82 | return return_val; 83 | } 84 | 85 | bool CHandle::GetErrorMessage(string &error) 86 | { 87 | CLog::Get()->Log(LogLevel::DEBUG, "CHandle::GetErrorMessage(this={})", 88 | static_cast(this)); 89 | 90 | if (m_MainConnection == nullptr) 91 | return false; 92 | 93 | unsigned int unused_errorid; 94 | bool return_val = m_MainConnection->GetError(unused_errorid, error); 95 | 96 | CLog::Get()->Log(LogLevel::DEBUG, 97 | "CHandle::GetErrorMessage - " \ 98 | "return value: {}, error id: '{}', error msg: '{}'", 99 | return_val, unused_errorid, error); 100 | 101 | return return_val; 102 | } 103 | 104 | bool CHandle::EscapeString(const char *src, string &dest) 105 | { 106 | CLog::Get()->Log(LogLevel::DEBUG, "CHandle::EscapeString(this={}, src='{}')", 107 | static_cast(this), src ? src : "(nullptr)"); 108 | 109 | if (m_MainConnection == nullptr) 110 | return false; 111 | 112 | bool return_val = m_MainConnection->EscapeString(src, dest); 113 | 114 | CLog::Get()->Log(LogLevel::DEBUG, 115 | "CHandle::EscapeString - return value: {}, escaped string: '{}'", 116 | return_val, dest); 117 | 118 | return return_val; 119 | } 120 | 121 | bool CHandle::SetCharacterSet(string charset) 122 | { 123 | CLog::Get()->Log(LogLevel::DEBUG, 124 | "CHandle::SetCharacterSet(this={}, charset='{}')", 125 | static_cast(this), charset); 126 | 127 | if (m_MainConnection == nullptr) 128 | return false; 129 | 130 | return 131 | m_MainConnection->SetCharset(charset) 132 | && m_ThreadedConnection->SetCharset(charset) 133 | && ((m_ConnectionPool != nullptr) ? m_ConnectionPool->SetCharset(charset) : true); 134 | } 135 | 136 | bool CHandle::GetCharacterSet(string &charset) 137 | { 138 | CLog::Get()->Log(LogLevel::DEBUG, "CHandle::GetCharacterSet(this={})", 139 | static_cast(this)); 140 | 141 | if (m_MainConnection == nullptr) 142 | return false; 143 | 144 | return m_MainConnection->GetCharset(charset); 145 | } 146 | 147 | bool CHandle::GetStatus(string &stat) 148 | { 149 | CLog::Get()->Log(LogLevel::DEBUG, "CHandle::GetStatus(this={})", 150 | static_cast(this)); 151 | 152 | if (m_MainConnection == nullptr) 153 | return false; 154 | 155 | return m_MainConnection->GetStatus(stat); 156 | } 157 | 158 | unsigned int CHandle::GetUnprocessedQueryCount() 159 | { 160 | CLog::Get()->Log(LogLevel::DEBUG, "CHandle::GetUnprocessedQueryCount(this={})", 161 | static_cast(this)); 162 | 163 | unsigned int count = m_ThreadedConnection->GetUnprocessedQueryCount(); 164 | if (m_ConnectionPool != nullptr) 165 | count += m_ConnectionPool->GetUnprocessedQueryCount(); 166 | 167 | return count; 168 | } 169 | 170 | 171 | 172 | Handle_t CHandleManager::Create(const char *host, const char *user, 173 | const char *pass, const char *db, 174 | const COptions *options, CError &error) 175 | { 176 | CLog::Get()->Log(LogLevel::DEBUG, 177 | "CHandleManager::Create(this={}, host='{}', user='{}', pass='****', db='{}', options={})", 178 | static_cast(this), 179 | host ? host : "(nullptr)", 180 | user ? user : "(nullptr)", 181 | db ? db : "(nullptr)", 182 | static_cast(options)); 183 | CLog::Get()->Log(LogLevel::INFO, "Creating new connection handle..."); 184 | 185 | if (host == nullptr || strlen(host) == 0) 186 | { 187 | error.set(CHandle::Error::EMPTY_HOST, "no hostname specified"); 188 | return nullptr; 189 | } 190 | 191 | if (user == nullptr || strlen(user) == 0) 192 | { 193 | error.set(CHandle::Error::EMPTY_USER, "no username specified"); 194 | return nullptr; 195 | } 196 | 197 | if (pass == nullptr) 198 | pass = ""; 199 | 200 | if (strlen(pass) == 0) 201 | CLog::Get()->LogNative(LogLevel::WARNING, "no password specified"); 202 | 203 | if (db == nullptr || strlen(db) == 0) 204 | { 205 | error.set(CHandle::Error::EMPTY_DATABASE, "no database specified"); 206 | return nullptr; 207 | } 208 | 209 | if (options == nullptr) 210 | { 211 | error.set(CHandle::Error::INVALID_OPTIONS, "invalid option-handler"); 212 | return nullptr; 213 | } 214 | 215 | 216 | static const std::hash do_hash; 217 | size_t full_hash = 0; 218 | boost::hash_combine(full_hash, do_hash(host)); 219 | boost::hash_combine(full_hash, do_hash(user)); 220 | boost::hash_combine(full_hash, do_hash(db)); 221 | 222 | if (COptionManager::Get()->GetGlobalOption( 223 | COptionManager::GlobalOption::DUPLICATE_CONNECTIONS) == false) 224 | { 225 | for (auto &h : m_Handles) 226 | { 227 | if (h.second->m_MyHash == full_hash) 228 | { 229 | if (COptionManager::Get()->GetGlobalOption( 230 | COptionManager::GlobalOption::DUPLICATE_CONNECTION_WARNING)) 231 | { 232 | CLog::Get()->Log(LogLevel::WARNING, 233 | "duplicate connection detected: " \ 234 | "handle id {} already exists with " \ 235 | "host = '{}', username = '{}' and database = '{}'", 236 | h.first, host, user, db); 237 | } 238 | return h.second; 239 | } 240 | } 241 | } 242 | 243 | HandleId_t id = 1; 244 | while (m_Handles.find(id) != m_Handles.end()) 245 | ++id; 246 | 247 | Handle_t handle = new CHandle(id, full_hash); 248 | 249 | handle->m_MainConnection = new CConnection(host, user, pass, db, options); 250 | handle->m_ThreadedConnection = new CThreadedConnection(host, user, pass, 251 | db, options); 252 | 253 | auto pool_size = options->GetOption(COptions::Type::POOL_SIZE); 254 | if (pool_size != 0) 255 | handle->m_ConnectionPool = new CConnectionPool(pool_size, host, user, 256 | pass, db, options); 257 | 258 | m_Handles.emplace(id, handle); 259 | 260 | CLog::Get()->Log(LogLevel::INFO, 261 | "Connection handle with id '{}' successfully created.", id); 262 | CLog::Get()->Log(LogLevel::DEBUG, 263 | "CHandleManager::Create - new handle = {}", 264 | static_cast(handle)); 265 | 266 | return handle; 267 | } 268 | 269 | Handle_t CHandleManager::CreateFromFile(string file_path, CError &error) 270 | { 271 | CLog::Get()->Log(LogLevel::DEBUG, 272 | "CHandleManager::CreateFromFile(this={}, file_path='{}')", 273 | static_cast(this), file_path); 274 | 275 | std::ifstream file(file_path); 276 | if (file.fail()) 277 | { 278 | error.set(CHandle::Error::INVALID_FILE, 279 | "invalid connection file specified (file: \"{}\")", file_path); 280 | return nullptr; 281 | } 282 | 283 | string hostname, username, password, database; 284 | auto options_id = COptionManager::Get()->Create(); 285 | COptions *options = COptionManager::Get()->GetOptionHandle(options_id); 286 | 287 | const std::unordered_map> assign_map{ 288 | { "hostname", [&](string &val_str) 289 | { 290 | hostname = val_str; 291 | } 292 | }, 293 | { "username", [&](string &val_str) 294 | { 295 | username = val_str; 296 | } 297 | }, 298 | { "password", [&](string &val_str) 299 | { 300 | password = val_str; 301 | } 302 | }, 303 | { "database", [&](string &val_str) 304 | { 305 | database = val_str; 306 | } 307 | }, 308 | { "auto_reconnect", [&](string &val_str) 309 | { 310 | bool val; 311 | if (ConvertStrToData(val_str, val)) 312 | options->SetOption(COptions::Type::AUTO_RECONNECT, val); 313 | } 314 | }, 315 | { "multi_statements", [&](string &val_str) 316 | { 317 | bool val; 318 | if (ConvertStrToData(val_str, val)) 319 | options->SetOption(COptions::Type::MULTI_STATEMENTS, val); 320 | } 321 | }, 322 | { "pool_size", [&](string &val_str) 323 | { 324 | unsigned int val = 0; 325 | if (ConvertStrToData(val_str, val)) 326 | options->SetOption(COptions::Type::POOL_SIZE, val); 327 | } 328 | }, 329 | { "server_port", [&](string &val_str) 330 | { 331 | unsigned int val = 0; 332 | if (ConvertStrToData(val_str, val)) 333 | options->SetOption(COptions::Type::SERVER_PORT, val); 334 | } 335 | }, 336 | { "ssl_enable", [&](string &val_str) 337 | { 338 | bool val; 339 | if (ConvertStrToData(val_str, val)) 340 | options->SetOption(COptions::Type::SSL_ENABLE, val); 341 | } 342 | }, 343 | { "ssl_key_file", [&](string &val_str) 344 | { 345 | options->SetOption(COptions::Type::SSL_KEY_FILE, val_str); 346 | } 347 | }, 348 | { "ssl_cert_file", [&](string &val_str) 349 | { 350 | options->SetOption(COptions::Type::SSL_CERT_FILE, val_str); 351 | } 352 | }, 353 | { "ssl_ca_file", [&](string &val_str) 354 | { 355 | options->SetOption(COptions::Type::SSL_CA_FILE, val_str); 356 | } 357 | }, 358 | { "ssl_ca_path", [&](string &val_str) 359 | { 360 | options->SetOption(COptions::Type::SSL_CA_PATH, val_str); 361 | } 362 | }, 363 | { "ssl_cipher", [&](string &val_str) 364 | { 365 | options->SetOption(COptions::Type::SSL_CIPHER, val_str); 366 | } 367 | }, 368 | }; 369 | 370 | while (file.good()) 371 | { 372 | string line; 373 | std::getline(file, line); 374 | 375 | //erase prepending whitespace 376 | size_t first_char_pos = line.find_first_not_of(" \t"); 377 | if (first_char_pos != string::npos) 378 | line.erase(0, first_char_pos); 379 | 380 | //erase comment from line 381 | size_t comment_pos = line.find_first_of("#;"); 382 | if (comment_pos != string::npos) 383 | line.erase(comment_pos); 384 | 385 | if (line.empty()) 386 | continue; 387 | 388 | std::string field, data; 389 | if (qi::parse(line.begin(), line.end(), 390 | qi::skip(qi::space)[ 391 | qi::as_string[+qi::char_("a-z_")] >> qi::lit('=') 392 | >> qi::as_string[+qi::graph] 393 | ], 394 | field, data)) 395 | { 396 | auto field_it = assign_map.find(field); 397 | if (field_it != assign_map.end() && data.empty() == false) 398 | { 399 | field_it->second(data); 400 | } 401 | else 402 | { 403 | error.set(CHandle::Error::UNKNOWN_FIELD, 404 | "unknown field in connection file (field: \"{}\")", 405 | field); 406 | return nullptr; 407 | } 408 | } 409 | else 410 | { 411 | error.set(CHandle::Error::SYNTAX_ERROR, 412 | "syntax error in connection file (line: \"{}\")", line); 413 | return nullptr; 414 | } 415 | } 416 | 417 | CLog::Get()->Log(LogLevel::DEBUG, 418 | "CHandleManager::CreateFromFile - new options = {} (id '{}')", 419 | static_cast(options), options_id); 420 | 421 | return Create(hostname.c_str(), username.c_str(), password.c_str(), 422 | database.c_str(), options, error); 423 | } 424 | 425 | bool CHandleManager::Destroy(Handle_t &handle) 426 | { 427 | CLog::Get()->Log(LogLevel::DEBUG, "CHandleManager::Destroy(this={}, handle={})", 428 | static_cast(this), 429 | static_cast(handle)); 430 | 431 | if (handle == nullptr) 432 | return false; 433 | 434 | if (m_Handles.erase(handle->GetId()) == 0) 435 | return false; 436 | 437 | 438 | delete handle; 439 | return true; 440 | } 441 | -------------------------------------------------------------------------------- /src/CHandle.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "CSingleton.hpp" 4 | 5 | #include 6 | #include 7 | 8 | using std::string; 9 | using std::unordered_map; 10 | 11 | #include "CError.hpp" 12 | #include "types.hpp" 13 | 14 | class CConnection; 15 | class CThreadedConnection; 16 | class CConnectionPool; 17 | 18 | 19 | class CHandle 20 | { 21 | friend class CHandleManager; 22 | public: //type definitions 23 | enum class ExecutionType 24 | { 25 | INVALID, 26 | THREADED, 27 | PARALLEL, 28 | UNTHREADED 29 | }; 30 | 31 | enum class Error 32 | { 33 | NONE, 34 | EMPTY_HOST, 35 | EMPTY_USER, 36 | EMPTY_DATABASE, 37 | INVALID_OPTIONS, 38 | //file errors 39 | INVALID_FILE, 40 | SYNTAX_ERROR, 41 | UNKNOWN_FIELD, 42 | }; 43 | static const string ModuleName; 44 | 45 | 46 | private: //constructor / deconstructor 47 | CHandle(HandleId_t id, size_t myhash) : 48 | m_Id(id), 49 | m_MyHash(myhash) 50 | { } 51 | ~CHandle(); 52 | 53 | private: //variables 54 | const HandleId_t m_Id; 55 | const size_t m_MyHash; 56 | 57 | CConnection *m_MainConnection = nullptr; 58 | CThreadedConnection *m_ThreadedConnection = nullptr; 59 | CConnectionPool *m_ConnectionPool = nullptr; 60 | 61 | public: //functions 62 | inline HandleId_t GetId() const 63 | { 64 | return m_Id; 65 | } 66 | 67 | bool Execute(ExecutionType type, Query_t query); 68 | bool GetErrorId(unsigned int &errorid); 69 | bool GetErrorMessage(string &error); 70 | bool EscapeString(const char *src, string &dest); 71 | bool SetCharacterSet(string charset); 72 | bool GetCharacterSet(string &charset); 73 | bool GetStatus(string &stat); 74 | unsigned int GetUnprocessedQueryCount(); 75 | }; 76 | 77 | class CHandleManager : public CSingleton 78 | { 79 | friend class CSingleton; 80 | private: //constructor / deconstructor 81 | CHandleManager() = default; 82 | ~CHandleManager() = default; 83 | 84 | private: //variables 85 | unordered_map m_Handles; 86 | 87 | public: //functions 88 | Handle_t Create(const char *host, const char *user, const char *pass, 89 | const char *db, const COptions *options, 90 | CError &error); 91 | Handle_t CreateFromFile(string file_path, CError &error); 92 | bool Destroy(Handle_t &handle); 93 | 94 | inline bool IsValidHandle(const HandleId_t id) 95 | { 96 | return m_Handles.find(id) != m_Handles.end(); 97 | } 98 | inline Handle_t GetHandle(const HandleId_t id) 99 | { 100 | return IsValidHandle(id) ? m_Handles.at(id) : nullptr; 101 | } 102 | 103 | }; 104 | -------------------------------------------------------------------------------- /src/CLog.cpp: -------------------------------------------------------------------------------- 1 | #include "CLog.hpp" 2 | 3 | #include 4 | 5 | 6 | void CDebugInfoManager::Update(AMX * const amx, const char *func) 7 | { 8 | m_Amx = amx; 9 | m_NativeName = func; 10 | m_Info.clear(); 11 | m_Available = samplog::Api::Get()->GetAmxFunctionCallTrace(amx, m_Info); 12 | } 13 | 14 | void CDebugInfoManager::Clear() 15 | { 16 | m_Amx = nullptr; 17 | m_NativeName = nullptr; 18 | m_Available = false; 19 | } 20 | 21 | CScopedDebugInfo::CScopedDebugInfo(AMX * const amx, const char *func, 22 | cell * const params, const char *params_format /* = ""*/) 23 | { 24 | CDebugInfoManager::Get()->Update(amx, func); 25 | 26 | auto &logger = CLog::Get()->m_Logger; 27 | if (logger.IsLogLevel(LogLevel::DEBUG)) 28 | logger.LogNativeCall(amx, params, func, params_format); 29 | } 30 | -------------------------------------------------------------------------------- /src/CLog.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include "CSingleton.hpp" 5 | #include "CError.hpp" 6 | 7 | #include 8 | 9 | using samplog::PluginLogger_t; 10 | using samplog::LogLevel; 11 | using samplog::AmxFuncCallInfo; 12 | 13 | 14 | class CDebugInfoManager : public CSingleton 15 | { 16 | friend class CSingleton; 17 | friend class CScopedDebugInfo; 18 | private: 19 | CDebugInfoManager() = default; 20 | ~CDebugInfoManager() = default; 21 | 22 | private: 23 | bool m_Available = false; 24 | 25 | AMX *m_Amx = nullptr; 26 | std::vector m_Info; 27 | const char *m_NativeName = nullptr; 28 | 29 | private: 30 | void Update(AMX * const amx, const char *func); 31 | void Clear(); 32 | 33 | public: 34 | inline AMX * const GetCurrentAmx() 35 | { 36 | return m_Amx; 37 | } 38 | inline const decltype(m_Info) &GetCurrentInfo() 39 | { 40 | return m_Info; 41 | } 42 | inline bool IsInfoAvailable() 43 | { 44 | return m_Available; 45 | } 46 | inline const char *GetCurrentNativeName() 47 | { 48 | return m_NativeName; 49 | } 50 | }; 51 | 52 | 53 | class CLog : public CSingleton 54 | { 55 | friend class CSingleton; 56 | friend class CScopedDebugInfo; 57 | private: 58 | CLog() : 59 | m_Logger("mysql") 60 | { } 61 | ~CLog() = default; 62 | 63 | public: 64 | inline bool IsLogLevel(LogLevel level) 65 | { 66 | return m_Logger.IsLogLevel(level); 67 | } 68 | 69 | template 70 | inline void Log(LogLevel level, const char *format, Args &&...args) 71 | { 72 | if (!IsLogLevel(level)) 73 | return; 74 | 75 | string str = format; 76 | if (sizeof...(args) != 0) 77 | str = fmt::format(format, std::forward(args)...); 78 | 79 | m_Logger.Log(level, str.c_str()); 80 | } 81 | 82 | template 83 | inline void Log(LogLevel level, std::vector const &callinfo, 84 | const char *format, Args &&...args) 85 | { 86 | if (!IsLogLevel(level)) 87 | return; 88 | 89 | string str = format; 90 | if (sizeof...(args) != 0) 91 | str = fmt::format(format, std::forward(args)...); 92 | 93 | m_Logger.Log(level, str.c_str(), callinfo); 94 | } 95 | 96 | // should only be called in native functions 97 | template 98 | void LogNative(LogLevel level, const char *fmt, Args &&...args) 99 | { 100 | if (!IsLogLevel(level)) 101 | return; 102 | 103 | if (CDebugInfoManager::Get()->GetCurrentAmx() == nullptr) 104 | return; //do nothing, since we're not called from within a native func 105 | 106 | string msg = fmt::format("{}: {}", 107 | CDebugInfoManager::Get()->GetCurrentNativeName(), 108 | fmt::format(fmt, std::forward(args)...)); 109 | 110 | if (CDebugInfoManager::Get()->IsInfoAvailable()) 111 | Log(level, CDebugInfoManager::Get()->GetCurrentInfo(), msg.c_str()); 112 | else 113 | Log(level, msg.c_str()); 114 | } 115 | 116 | template 117 | inline void LogNative(const CError &error) 118 | { 119 | LogNative(LogLevel::ERROR, "{} error: {}", 120 | error.module(), error.msg()); 121 | } 122 | 123 | private: 124 | PluginLogger_t m_Logger; 125 | 126 | }; 127 | 128 | 129 | class CScopedDebugInfo 130 | { 131 | public: 132 | CScopedDebugInfo(AMX * const amx, const char *func, 133 | cell * const params, const char *params_format = ""); 134 | ~CScopedDebugInfo() 135 | { 136 | CDebugInfoManager::Get()->Clear(); 137 | } 138 | CScopedDebugInfo(const CScopedDebugInfo &rhs) = delete; 139 | }; 140 | -------------------------------------------------------------------------------- /src/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | include(AMXConfig) 2 | include(AddSAMPPlugin) 3 | 4 | if(NOT WIN32 AND MYSQLCAPI_LIBRARY_STATIC) 5 | option(BUILD_STATIC "Build static library" OFF) 6 | endif() 7 | 8 | set(SAMP_SDK_ROOT "${PROJECT_SOURCE_DIR}/libs/sdk") 9 | find_package(SAMPSDK REQUIRED) 10 | 11 | set(MYSQL_PLUGIN_CONFIG_INCLUDE_DIR "${CMAKE_CURRENT_BINARY_DIR}/config_headers") 12 | include_directories( 13 | ${SAMPSDK_INCLUDE_DIR} 14 | ${MYSQL_PLUGIN_CONFIG_INCLUDE_DIR} 15 | ${PROJECT_SOURCE_DIR}/libs/samp-log-core/include 16 | ) 17 | 18 | configure_file( 19 | "../a_mysql.inc.in" 20 | "a_mysql.inc" 21 | @ONLY 22 | ) 23 | 24 | configure_file( 25 | "version.hpp.in" 26 | "${MYSQL_PLUGIN_CONFIG_INCLUDE_DIR}/version.hpp" 27 | @ONLY 28 | ) 29 | 30 | add_samp_plugin(mysql 31 | ${SAMPSDK_DIR}/amxplugin.cpp 32 | ${SAMPSDK_DIR}/amxplugin2.cpp 33 | ${SAMPSDK_DIR}/amx/getch.c 34 | CCallback.cpp 35 | CCallback.hpp 36 | CConnection.cpp 37 | CConnection.hpp 38 | CDispatcher.cpp 39 | CDispatcher.hpp 40 | CError.hpp 41 | CHandle.cpp 42 | CHandle.hpp 43 | CLog.cpp 44 | CLog.hpp 45 | COptions.cpp 46 | COptions.hpp 47 | COrm.cpp 48 | COrm.hpp 49 | CQuery.cpp 50 | CQuery.hpp 51 | CResult.cpp 52 | CResult.hpp 53 | natives.cpp 54 | natives.hpp 55 | CSingleton.hpp 56 | main.cpp 57 | misc.hpp 58 | mysql.hpp 59 | sdk.hpp 60 | types.hpp 61 | plugin.def 62 | ) 63 | 64 | target_include_directories(mysql PRIVATE 65 | ${Boost_INCLUDE_DIR} 66 | ${MYSQLCAPI_INCLUDE_DIR} 67 | ${LOGCORE_INCLUDE_DIR} 68 | ) 69 | 70 | target_compile_features(mysql PUBLIC 71 | cxx_auto_type 72 | cxx_decltype 73 | cxx_defaulted_functions 74 | cxx_default_function_template_args 75 | cxx_delegating_constructors 76 | cxx_deleted_functions 77 | cxx_enum_forward_declarations 78 | cxx_explicit_conversions 79 | cxx_extended_friend_declarations 80 | cxx_lambdas 81 | cxx_inheriting_constructors 82 | cxx_noexcept 83 | cxx_nonstatic_member_init 84 | cxx_nullptr 85 | cxx_range_for 86 | cxx_reference_qualified_functions 87 | cxx_right_angle_brackets 88 | cxx_rvalue_references 89 | cxx_strong_enums 90 | cxx_variadic_templates 91 | ) 92 | 93 | add_dependencies(mysql log-core fmt) 94 | 95 | if(MSVC) 96 | add_definitions(-D_CRT_SECURE_NO_WARNINGS -DNOMINMAX -D_ENABLE_ATOMIC_ALIGNMENT_FIX) 97 | endif() 98 | 99 | target_link_libraries(mysql log-core fmt) 100 | 101 | if(UNIX) 102 | if(NOT APPLE) 103 | target_link_libraries(mysql rt) 104 | endif() 105 | 106 | set_target_properties(mysql PROPERTIES 107 | INSTALL_RPATH "$ORIGIN/..:$ORIGIN/") 108 | endif() 109 | 110 | if(BUILD_STATIC) 111 | target_link_libraries(mysql "${MYSQLCAPI_LIBRARY_STATIC}") 112 | else() 113 | target_link_libraries(mysql "${MYSQLCAPI_LIBRARY}") 114 | endif() 115 | 116 | install(TARGETS mysql DESTINATION "plugins/") 117 | get_target_property(LOGCORE_LOCATION log-core LOCATION) 118 | install(FILES "${LOGCORE_LOCATION}" DESTINATION "./") 119 | install(FILES "${CMAKE_CURRENT_BINARY_DIR}/a_mysql.inc" DESTINATION "pawno/include/") 120 | install(FILES "${PROJECT_SOURCE_DIR}/LICENSE" DESTINATION "./") 121 | if(WIN32 AND MYSQLCAPI_LIBRARY_SHARED) 122 | install(FILES "${MYSQLCAPI_LIBRARY_SHARED}" DESTINATION "./") 123 | endif() 124 | 125 | set(CPACK_PACKAGE_VERSION ${MYSQL_PLUGIN_VERSION}) 126 | if(WIN32) 127 | set(CPACK_GENERATOR ZIP) 128 | else() 129 | set(CPACK_GENERATOR TGZ) 130 | endif() 131 | 132 | include(CPack) 133 | -------------------------------------------------------------------------------- /src/COptions.cpp: -------------------------------------------------------------------------------- 1 | #include "COptions.hpp" 2 | #include "CLog.hpp" 3 | 4 | 5 | COptions::COptions() 6 | { 7 | m_Options[Type::AUTO_RECONNECT] = true; 8 | m_Options[Type::MULTI_STATEMENTS] = false; 9 | m_Options[Type::POOL_SIZE] = 2u; 10 | m_Options[Type::SERVER_PORT] = 3306u; 11 | m_Options[Type::SSL_ENABLE] = false; 12 | m_Options[Type::SSL_KEY_FILE] = string(); 13 | m_Options[Type::SSL_CERT_FILE] = string(); 14 | m_Options[Type::SSL_CA_FILE] = string(); 15 | m_Options[Type::SSL_CA_PATH] = string(); 16 | m_Options[Type::SSL_CIPHER] = string(); 17 | } 18 | 19 | 20 | COptionManager::COptionManager() 21 | { 22 | m_GlobalOptions[GlobalOption::DUPLICATE_CONNECTIONS] = false; 23 | m_GlobalOptions[GlobalOption::DUPLICATE_CONNECTION_WARNING] = true; 24 | 25 | //create default options instance with id = 0 26 | m_Options.emplace(0, new COptions); 27 | } 28 | 29 | COptionManager::~COptionManager() 30 | { 31 | for (auto o : m_Options) 32 | delete o.second; 33 | } 34 | 35 | OptionsId_t COptionManager::Create() 36 | { 37 | OptionsId_t id = 1; 38 | while (m_Options.find(id) != m_Options.end()) 39 | id++; 40 | 41 | m_Options.emplace(id, new COptions); 42 | CLog::Get()->Log(LogLevel::INFO, 43 | "Options instance with id '{}' successfully created.", id); 44 | 45 | return id; 46 | } 47 | -------------------------------------------------------------------------------- /src/COptions.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "CSingleton.hpp" 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | using std::string; 11 | using std::map; 12 | using std::unordered_map; 13 | 14 | #include "types.hpp" 15 | 16 | 17 | class COptions 18 | { 19 | friend class COptionManager; 20 | public: 21 | enum class Type 22 | { 23 | AUTO_RECONNECT, //true 24 | MULTI_STATEMENTS, //false 25 | POOL_SIZE, //2 26 | SERVER_PORT, //3306 27 | SSL_ENABLE, //false 28 | SSL_KEY_FILE, //nullptr 29 | SSL_CERT_FILE, //nullptr 30 | SSL_CA_FILE, //nullptr 31 | SSL_CA_PATH, //nullptr 32 | SSL_CIPHER, //nullptr 33 | }; 34 | 35 | private: 36 | COptions(); 37 | ~COptions() = default; 38 | 39 | private: 40 | map> m_Options; 41 | 42 | private: //helper function 43 | template 44 | bool IsValidOptionType(Type option) 45 | { 46 | static const map type_map 47 | { 48 | { Type::AUTO_RECONNECT, typeid(bool).hash_code() }, 49 | { Type::MULTI_STATEMENTS, typeid(bool).hash_code() }, 50 | { Type::POOL_SIZE, typeid(unsigned int).hash_code() }, 51 | { Type::SERVER_PORT, typeid(unsigned int).hash_code() }, 52 | { Type::SSL_ENABLE, typeid(bool).hash_code() }, 53 | { Type::SSL_KEY_FILE, typeid(string).hash_code() }, 54 | { Type::SSL_CERT_FILE, typeid(string).hash_code() }, 55 | { Type::SSL_CA_FILE, typeid(string).hash_code() }, 56 | { Type::SSL_CA_PATH, typeid(string).hash_code() }, 57 | { Type::SSL_CIPHER, typeid(string).hash_code() } 58 | }; 59 | 60 | return type_map.at(option) == typeid(T).hash_code(); 61 | } 62 | 63 | public: 64 | template 65 | inline bool SetOption(Type option, T value) 66 | { 67 | if (IsValidOptionType(option)) 68 | { 69 | m_Options[option] = value; 70 | return true; 71 | } 72 | return false; 73 | } 74 | template 75 | inline T GetOption(Type option) const 76 | { 77 | return boost::get(m_Options.at(option)); 78 | } 79 | 80 | }; 81 | 82 | class COptionManager : public CSingleton 83 | { 84 | friend class CSingleton; 85 | public: 86 | enum class GlobalOption 87 | { 88 | DUPLICATE_CONNECTIONS, //false [bool] 89 | DUPLICATE_CONNECTION_WARNING, //true [bool] 90 | }; 91 | 92 | private: 93 | COptionManager(); 94 | ~COptionManager(); 95 | 96 | private: 97 | unordered_map m_Options; 98 | map m_GlobalOptions; 99 | 100 | public: 101 | OptionsId_t Create(); 102 | 103 | inline bool IsValidOptionHandle(const OptionsId_t id) 104 | { 105 | return m_Options.find(id) != m_Options.end(); 106 | } 107 | inline COptions *GetOptionHandle(OptionsId_t id) 108 | { 109 | return IsValidOptionHandle(id) ? m_Options.at(id) : nullptr; 110 | } 111 | inline const COptions *GetDefaultOptionHandle() 112 | { 113 | return m_Options.at(0); 114 | } 115 | 116 | inline bool GetGlobalOption(GlobalOption option) 117 | { 118 | return m_GlobalOptions[option]; 119 | } 120 | inline void SetGlobalOption(GlobalOption option, bool value) 121 | { 122 | m_GlobalOptions[option] = value; 123 | } 124 | }; 125 | -------------------------------------------------------------------------------- /src/COrm.cpp: -------------------------------------------------------------------------------- 1 | #include "COrm.hpp" 2 | #include "CHandle.hpp" 3 | #include "CResult.hpp" 4 | #include "CLog.hpp" 5 | #include "misc.hpp" 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #ifdef NO_DATA 12 | #undef NO_DATA //thanks M$ 13 | #endif 14 | #ifdef DELETE 15 | #undef DELETE //goddammit 16 | #endif 17 | 18 | 19 | const string COrm::ModuleName{ "orm" }; 20 | 21 | bool COrm::Variable::GetValueAsString(string &dest, Handle_t handle_escape) 22 | { 23 | CLog::Get()->Log(LogLevel::DEBUG, 24 | "COrm::Variable::GetValueAsString(this={}, handle={})", 25 | static_cast(this), 26 | static_cast(handle_escape)); 27 | 28 | switch (m_Type) 29 | { 30 | case COrm::Variable::Type::INVALID: 31 | dest = "INVALID"; 32 | break; 33 | case COrm::Variable::Type::INT: 34 | dest = fmt::FormatInt(*(m_VariableAddr)).str(); 35 | break; 36 | case COrm::Variable::Type::FLOAT: 37 | if (!ConvertDataToStr(amx_ctof(*m_VariableAddr), dest)) 38 | return false; 39 | break; 40 | case COrm::Variable::Type::STRING: 41 | { 42 | int var_len = 0; 43 | amx_StrLen(m_VariableAddr, &var_len); 44 | string var(var_len, ' '); 45 | amx_GetString(&var[0], m_VariableAddr, 0, m_VarMaxLen); 46 | if (var_len > 0 && !handle_escape->EscapeString(var.c_str(), dest)) 47 | return false; 48 | } break; 49 | } 50 | return true; 51 | } 52 | 53 | void COrm::Variable::SetValue(const char *value) 54 | { 55 | CLog::Get()->Log(LogLevel::DEBUG, 56 | "COrm::Variable::SetValue(this={}, value='{}')", 57 | static_cast(this), value ? value : "(nullptr)"); 58 | 59 | switch (m_Type) 60 | { 61 | case COrm::Variable::Type::INT: 62 | ConvertStrToData(value, (*m_VariableAddr)); 63 | break; 64 | case COrm::Variable::Type::FLOAT: 65 | { 66 | float dest = 0.0f; 67 | if (ConvertStrToData(value, dest)) 68 | (*m_VariableAddr) = amx_ftoc(dest); 69 | } break; 70 | case COrm::Variable::Type::STRING: 71 | amx_SetString(m_VariableAddr, 72 | value != nullptr ? value : "NULL", 0, 0, m_VarMaxLen); 73 | break; 74 | } 75 | } 76 | 77 | CError COrm::AddVariable(Variable::Type type, 78 | const char *name, cell *var_addr, size_t var_maxlen) 79 | { 80 | CLog::Get()->Log(LogLevel::DEBUG, 81 | "COrm::AddVariable(this={}, type={}, name='{}', var_addr={}, var_maxlen={})", 82 | static_cast(this), 83 | static_cast::type>(type), 84 | name ? name : "(nullptr)", 85 | static_cast(var_addr), var_maxlen); 86 | 87 | if (type == Variable::Type::INVALID) 88 | return{ Error::INVALID_VARIABLE_TYPE, "invalid variable type" }; 89 | 90 | if (name == nullptr || strlen(name) == 0) 91 | return{ Error::EMPTY_VARIABLE_NAME, "empty variable name" }; 92 | 93 | if (var_addr == nullptr) 94 | return{ Error::INVALID_PAWN_ADDRESS, "invalid variable PAWN address" }; 95 | 96 | if (type == Variable::Type::STRING && var_maxlen <= 0) 97 | return{ Error::INVALID_MAX_LEN, 98 | "invalid maximal length for string type variable" }; 99 | 100 | bool duplicate = false; 101 | for (auto &v : m_Variables) 102 | { 103 | if (v.GetName().compare(name) == 0) 104 | { 105 | duplicate = true; 106 | break; 107 | } 108 | } 109 | 110 | if (duplicate || m_KeyVariable.GetName().compare(name) == 0) 111 | return{ Error::DUPLICATE_VARIABLE, "variable is already registered" }; 112 | 113 | 114 | m_Variables.push_back(Variable(type, name, var_addr, var_maxlen)); 115 | return{ }; 116 | } 117 | 118 | CError COrm::RemoveVariable(const char *name) 119 | { 120 | CLog::Get()->Log(LogLevel::DEBUG, "COrm::RemoveVariable(this={}, name='{}')", 121 | static_cast(this), name ? name : "(nullptr)"); 122 | 123 | if (name == nullptr || strlen(name) == 0) 124 | return{ Error::EMPTY_VARIABLE_NAME, "empty variable name" }; 125 | 126 | if (m_KeyVariable.GetName().compare(name) == 0) 127 | { 128 | m_KeyVariable = Variable(); // unset/clear key variable 129 | } 130 | else 131 | { 132 | auto v = std::find_if(m_Variables.begin(), m_Variables.end(), 133 | [name](const Variable &v) -> bool 134 | { 135 | return v.GetName().compare(name) == 0; 136 | }); 137 | 138 | if (v == m_Variables.end()) 139 | return{ Error::UNKNOWN_VARIABLE, "variable not found" }; 140 | 141 | m_Variables.erase(v); 142 | } 143 | return{ }; 144 | } 145 | 146 | void COrm::ClearAllVariables() 147 | { 148 | CLog::Get()->Log(LogLevel::DEBUG, "COrm::ClearAllVariables(this={})", 149 | static_cast(this)); 150 | 151 | for (auto &v : m_Variables) 152 | v.Clear(); 153 | 154 | m_KeyVariable.Clear(); 155 | } 156 | 157 | CError COrm::SetKeyVariable(const char *name) 158 | { 159 | CLog::Get()->Log(LogLevel::DEBUG, "COrm::SetKeyVariable(this={}, name='{}')", 160 | static_cast(this), name ? name : "(nullptr)"); 161 | 162 | if (name == nullptr || strlen(name) == 0) 163 | return{ Error::EMPTY_VARIABLE_NAME, "empty variable name" }; 164 | 165 | auto v = std::find_if(m_Variables.begin(), m_Variables.end(), 166 | [name](const Variable &v) -> bool 167 | { 168 | return v.GetName().compare(name) == 0; 169 | }); 170 | 171 | if (v == m_Variables.end()) 172 | return{ Error::UNKNOWN_VARIABLE, "variable not found" }; 173 | 174 | //add old key variable back to normal variables 175 | if (m_KeyVariable) 176 | m_Variables.push_back(m_KeyVariable); 177 | 178 | m_KeyVariable = *v; 179 | m_Variables.erase(v); 180 | return{ }; 181 | } 182 | 183 | CError COrm::GenerateQuery(COrm::QueryType type, string &dest) 184 | { 185 | CLog::Get()->Log(LogLevel::DEBUG, "COrm::GenerateQuery(this={}, type={})", 186 | static_cast(this), 187 | static_cast::type>(type)); 188 | 189 | switch (type) 190 | { 191 | case COrm::QueryType::SELECT: 192 | return GenerateSelectQuery(dest); 193 | case COrm::QueryType::UPDATE: 194 | return GenerateUpdateQuery(dest); 195 | case COrm::QueryType::INSERT: 196 | return GenerateInsertQuery(dest); 197 | case COrm::QueryType::DELETE: 198 | return GenerateDeleteQuery(dest); 199 | } 200 | return{ COrm::Error::INVALID_QUERY_TYPE, "invalid query type" }; 201 | } 202 | 203 | COrm::QueryType COrm::GetSaveQueryType() 204 | { 205 | CLog::Get()->Log(LogLevel::DEBUG, "COrm::GetSaveQueryType(this={})", 206 | static_cast(this)); 207 | 208 | if (m_KeyVariable && m_KeyVariable.GetValueAsCell() != 0) //works for integer and strings 209 | return QueryType::UPDATE; 210 | return QueryType::INSERT; 211 | } 212 | 213 | CError COrm::GenerateSelectQuery(string &dest) 214 | { 215 | CLog::Get()->Log(LogLevel::DEBUG, "COrm::GenerateSelectQuery(this={})", 216 | static_cast(this)); 217 | 218 | if (m_Variables.empty()) 219 | return{ COrm::Error::NO_VARIABLES, "no registered variables" }; 220 | 221 | if (!m_KeyVariable) 222 | return{ COrm::Error::NO_KEY_VARIABLE, "no key variable set" }; 223 | 224 | fmt::MemoryWriter writer; 225 | writer << "SELECT "; 226 | WriteVariableNamesAsList(writer); 227 | 228 | auto handle = CHandleManager::Get()->GetHandle(GetHandleId()); 229 | if (handle == nullptr) 230 | { 231 | return{ Error::INVALID_CONNECTION_HANDLE, 232 | "invalid connection handle" }; 233 | } 234 | 235 | string key_var_value; 236 | if (!m_KeyVariable.GetValueAsString(key_var_value, handle)) 237 | { 238 | return{ Error::INVALID_STRING_REPRESENTATION, 239 | "can't represent variable value as string" }; 240 | } 241 | 242 | writer << " FROM `" << m_Table << "` WHERE `" << m_KeyVariable.GetName() 243 | << "`='" << key_var_value << "' LIMIT 1"; 244 | 245 | dest.assign(writer.str()); 246 | return{ }; 247 | } 248 | 249 | CError COrm::GenerateUpdateQuery(string &dest) 250 | { 251 | CLog::Get()->Log(LogLevel::DEBUG, "COrm::GenerateUpdateQuery(this={})", 252 | static_cast(this)); 253 | 254 | if (m_Variables.empty()) 255 | return{ COrm::Error::NO_VARIABLES, "no registered variables" }; 256 | 257 | if (!m_KeyVariable) 258 | return{ COrm::Error::NO_KEY_VARIABLE, "no key variable set" }; 259 | 260 | auto handle = CHandleManager::Get()->GetHandle(GetHandleId()); 261 | if (handle == nullptr) 262 | { 263 | return{ Error::INVALID_CONNECTION_HANDLE, 264 | "invalid connection handle" }; 265 | } 266 | 267 | fmt::MemoryWriter writer; 268 | writer << "UPDATE `" << m_Table << "` SET `"; 269 | for (size_t i = 0; i != m_Variables.size(); ++i) 270 | { 271 | if (i != 0) 272 | writer << "',`"; 273 | Variable &var = m_Variables.at(i); 274 | 275 | string var_value; 276 | if (!var.GetValueAsString(var_value, handle)) 277 | { 278 | return{ Error::INVALID_STRING_REPRESENTATION, 279 | "can't represent variable value as string" }; 280 | } 281 | 282 | writer << var.GetName() << "`='" << var_value; 283 | } 284 | 285 | string key_var_value; 286 | if (!m_KeyVariable.GetValueAsString(key_var_value, handle)) 287 | { 288 | return{ Error::INVALID_STRING_REPRESENTATION, 289 | "can't represent variable value as string" }; 290 | } 291 | 292 | writer << "' WHERE `" 293 | << m_KeyVariable.GetName() << "`='" << key_var_value 294 | << "' LIMIT 1"; 295 | 296 | dest.assign(writer.str()); 297 | return{ }; 298 | } 299 | 300 | CError COrm::GenerateInsertQuery(string &dest) 301 | { 302 | CLog::Get()->Log(LogLevel::DEBUG, "COrm::GenerateInsertQuery(this={})", 303 | static_cast(this)); 304 | 305 | if (m_Variables.empty()) 306 | return{ COrm::Error::NO_VARIABLES, "no registered variables" }; 307 | 308 | if (!m_KeyVariable) 309 | return{ COrm::Error::NO_KEY_VARIABLE, "no key variable set" }; 310 | 311 | auto handle = CHandleManager::Get()->GetHandle(GetHandleId()); 312 | if (handle == nullptr) 313 | { 314 | return{ Error::INVALID_CONNECTION_HANDLE, 315 | "invalid connection handle" }; 316 | } 317 | 318 | fmt::MemoryWriter writer; 319 | writer << "INSERT INTO `" << m_Table << "` ("; 320 | WriteVariableNamesAsList(writer); 321 | writer << ") VALUES ('"; 322 | for (size_t i = 0; i != m_Variables.size(); ++i) 323 | { 324 | if (i != 0) 325 | writer << "','"; 326 | 327 | string var_value; 328 | if (!m_Variables.at(i).GetValueAsString(var_value, handle)) 329 | { 330 | return{ Error::INVALID_STRING_REPRESENTATION, 331 | "can't represent variable value as string" }; 332 | } 333 | writer << var_value; 334 | } 335 | writer << "')"; 336 | 337 | dest.assign(writer.str()); 338 | return{ }; 339 | } 340 | 341 | CError COrm::GenerateDeleteQuery(string &dest) 342 | { 343 | CLog::Get()->Log(LogLevel::DEBUG, "COrm::GenerateDeleteQuery(this={})", 344 | static_cast(this)); 345 | 346 | if (!m_KeyVariable) 347 | return{ COrm::Error::NO_KEY_VARIABLE, "no key variable set" }; 348 | 349 | Handle_t handle = CHandleManager::Get()->GetHandle(GetHandleId()); 350 | if (handle == nullptr) 351 | { 352 | return{ Error::INVALID_CONNECTION_HANDLE, 353 | "invalid connection handle" }; 354 | } 355 | 356 | string key_var_value; 357 | if (!m_KeyVariable.GetValueAsString(key_var_value, handle)) 358 | { 359 | return{ Error::INVALID_STRING_REPRESENTATION, 360 | "can't represent variable value as string" }; 361 | } 362 | 363 | dest = fmt::format("DELETE FROM `{}` WHERE `{}`='{}' LIMIT 1", 364 | m_Table, m_KeyVariable.GetName(), key_var_value); 365 | return{ }; 366 | } 367 | 368 | void COrm::ApplyResult(const Result_t result, unsigned int rowidx /*= 0*/) 369 | { 370 | CLog::Get()->Log(LogLevel::DEBUG, 371 | "COrm::ApplyResult(this={}, result={}, rowidx={})", 372 | static_cast(this), 373 | static_cast(result), rowidx); 374 | 375 | if (result == nullptr || rowidx >= result->GetRowCount()) 376 | { 377 | m_Error = PawnError::NO_DATA; 378 | return; 379 | } 380 | 381 | // we don't apply anything to the key variable here, since 382 | // 1. it's not possible because variables are assigned sequentially by field index 383 | // 2. there is no key value in the result data since no function generates a query 384 | // where it also selects the key 385 | // 3. it's not really needed, because only the ORM-generated SELECT query uses this 386 | // function in its query result callback and the SELECT query selects data by 387 | // the key's value (WHERE clause) 388 | 389 | const char *data = nullptr; 390 | for (size_t i = 0; i != m_Variables.size(); ++i) 391 | { 392 | Variable &var = m_Variables.at(i); 393 | if (result->GetRowData(rowidx, i, &data)) 394 | { 395 | var.SetValue(data); 396 | } 397 | else 398 | { 399 | CLog::Get()->Log(LogLevel::WARNING, 400 | "COrm::ApplyResult - no data to apply to " \ 401 | "variable linked with field '{}'", var.GetName()); 402 | } 403 | } 404 | m_Error = PawnError::OK; 405 | } 406 | 407 | bool COrm::ApplyResultByName(const Result_t result, unsigned int rowidx /*= 0*/) 408 | { 409 | CLog::Get()->Log(LogLevel::DEBUG, "COrm::ApplyResultByName(this={}, result={}, rowidx={})", 410 | static_cast(this), 411 | static_cast(result), rowidx); 412 | 413 | if (result == nullptr || rowidx >= result->GetRowCount()) 414 | return false; 415 | 416 | const char *data = nullptr; 417 | if (m_KeyVariable) 418 | { 419 | if (result->GetRowDataByName(rowidx, m_KeyVariable.GetName(), &data)) 420 | { 421 | m_KeyVariable.SetValue(data); 422 | } 423 | else 424 | { 425 | CLog::Get()->Log(LogLevel::WARNING, 426 | "COrm::ApplyResultByName - no data to apply to " \ 427 | "key variable linked with field '{}'", 428 | m_KeyVariable.GetName()); 429 | } 430 | } 431 | 432 | for (auto &v : m_Variables) 433 | { 434 | if (result->GetRowDataByName(rowidx, v.GetName(), &data)) 435 | { 436 | v.SetValue(data); 437 | } 438 | else 439 | { 440 | CLog::Get()->Log(LogLevel::WARNING, 441 | "COrm::ApplyResultByName - no data to apply to " \ 442 | "variable linked with field '{}'", 443 | v.GetName()); 444 | } 445 | } 446 | return true; 447 | } 448 | 449 | bool COrm::UpdateKeyValue(const Result_t result) 450 | { 451 | CLog::Get()->Log(LogLevel::DEBUG, "COrm::UpdateKeyValue(this={}, result={})", 452 | static_cast(this), static_cast(result)); 453 | 454 | if (result == nullptr) 455 | { 456 | CLog::Get()->Log(LogLevel::ERROR, "COrm::UpdateKeyValue - no result"); 457 | return false; 458 | } 459 | 460 | if (result->InsertId() == 0) 461 | { 462 | CLog::Get()->Log(LogLevel::ERROR, "COrm::UpdateKeyValue - inserted id is zero"); 463 | return false; 464 | } 465 | 466 | if (!m_KeyVariable) 467 | { 468 | CLog::Get()->Log(LogLevel::ERROR, 469 | "COrm::UpdateKeyValue - no key variable registered"); 470 | return false; 471 | } 472 | 473 | m_KeyVariable.SetValue(static_cast(result->InsertId())); 474 | return true; 475 | } 476 | 477 | void COrm::WriteVariableNamesAsList(fmt::MemoryWriter &writer) 478 | { 479 | CLog::Get()->Log(LogLevel::DEBUG, "COrm::WriteVariableNamesAsList(this={})", 480 | static_cast(this)); 481 | 482 | writer << '`'; 483 | for (size_t i = 0; i != m_Variables.size(); ++i) 484 | { 485 | if (i != 0) 486 | writer << "`,`"; 487 | writer << m_Variables.at(i).GetName(); 488 | } 489 | writer << '`'; 490 | } 491 | 492 | 493 | OrmId_t COrmManager::Create(HandleId_t handleid, const char *table, 494 | CError &error) 495 | { 496 | CLog::Get()->Log(LogLevel::DEBUG, 497 | "COrmManager::Create(handleid={}, table='{}')", 498 | handleid, table ? table : "(nullptr)"); 499 | 500 | if (CHandleManager::Get()->IsValidHandle(handleid) == false) 501 | { 502 | error.set(COrm::Error::INVALID_CONNECTION_HANDLE, 503 | "invalid connection handle"); 504 | return 0; 505 | } 506 | 507 | if (table == nullptr || strlen(table) == 0) 508 | { 509 | error.set(COrm::Error::EMPTY_TABLE, "empty table name"); 510 | return 0; 511 | } 512 | 513 | 514 | OrmId_t id = 1; 515 | while (m_Instances.find(id) != m_Instances.end()) 516 | ++id; 517 | 518 | m_Instances.emplace(id, std::make_shared(handleid, table)); 519 | 520 | return id; 521 | } 522 | -------------------------------------------------------------------------------- /src/COrm.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "types.hpp" 4 | #include "CSingleton.hpp" 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | using std::string; 12 | 13 | #include 14 | #include "CError.hpp" 15 | 16 | 17 | #ifdef NO_DATA 18 | #undef NO_DATA //thanks M$ 19 | #endif 20 | #ifdef DELETE 21 | #undef DELETE //goddammit 22 | #endif 23 | 24 | 25 | class COrm 26 | { 27 | public: 28 | class Variable 29 | { 30 | public: 31 | enum class Type 32 | { 33 | INVALID, 34 | INT, 35 | FLOAT, 36 | STRING 37 | }; 38 | 39 | public: 40 | Variable(Type type, string name, 41 | cell *variable, size_t var_len = 0) : 42 | m_Type(type), 43 | m_Name(name), 44 | m_VariableAddr(variable), 45 | m_VarMaxLen(var_len) 46 | { } 47 | Variable() = default; 48 | ~Variable() = default; 49 | 50 | private: 51 | Type m_Type = Type::INVALID; 52 | string m_Name; 53 | cell *m_VariableAddr = nullptr; 54 | size_t m_VarMaxLen = 0; 55 | 56 | public: 57 | inline string const & GetName() const 58 | { 59 | return m_Name; 60 | } 61 | 62 | bool GetValueAsString(string &dest, Handle_t handle_escape); 63 | inline cell GetValueAsCell() const 64 | { 65 | return *m_VariableAddr; 66 | } 67 | void SetValue(const char *value); 68 | inline void SetValue(cell value) 69 | { 70 | (*m_VariableAddr) = value; 71 | } 72 | inline void Clear() 73 | { 74 | if (m_VariableAddr != nullptr) 75 | (*m_VariableAddr) = 0; 76 | } 77 | 78 | inline operator bool() const 79 | { 80 | return m_Type != Type::INVALID; 81 | } 82 | }; 83 | 84 | public: 85 | enum class Error 86 | { 87 | NONE, 88 | 89 | //query generation 90 | EMPTY_TABLE, 91 | INVALID_CONNECTION_HANDLE, 92 | NO_VARIABLES, 93 | NO_KEY_VARIABLE, 94 | INVALID_QUERY_TYPE, 95 | 96 | //variables 97 | INVALID_VARIABLE_TYPE, 98 | EMPTY_VARIABLE_NAME, 99 | INVALID_PAWN_ADDRESS, 100 | INVALID_MAX_LEN, 101 | DUPLICATE_VARIABLE, 102 | UNKNOWN_VARIABLE, 103 | INVALID_STRING_REPRESENTATION, 104 | }; 105 | 106 | enum class PawnError //errors for Pawn 107 | { 108 | INVALID, 109 | OK, 110 | NO_DATA, 111 | }; 112 | 113 | enum class QueryType 114 | { 115 | INVALID, 116 | SELECT, 117 | UPDATE, 118 | INSERT, 119 | DELETE, 120 | SAVE, //not used to generate query 121 | }; 122 | 123 | static const string ModuleName; 124 | 125 | public: 126 | COrm(HandleId_t handleid, const char *tablename) : 127 | m_HandleId(handleid), 128 | m_Table(tablename) 129 | { } 130 | ~COrm() = default; 131 | 132 | private: 133 | HandleId_t m_HandleId; 134 | string m_Table; 135 | 136 | std::vector m_Variables; 137 | Variable m_KeyVariable; 138 | 139 | PawnError m_Error = PawnError::OK; 140 | 141 | public: 142 | inline HandleId_t GetHandleId() const 143 | { 144 | return m_HandleId; 145 | } 146 | 147 | CError AddVariable(Variable::Type type, const char *name, 148 | cell *var_addr, size_t var_maxlen = 0); 149 | CError RemoveVariable(const char *name); 150 | void ClearAllVariables(); 151 | CError SetKeyVariable(const char *name); 152 | 153 | CError GenerateQuery(QueryType type, string &dest); 154 | QueryType GetSaveQueryType(); 155 | 156 | void ApplyResult(const Result_t result, 157 | unsigned int rowidx = 0U); 158 | bool ApplyResultByName(const Result_t result, 159 | unsigned int rowidx = 0U); 160 | bool UpdateKeyValue(const Result_t result); 161 | 162 | inline PawnError GetError() const 163 | { 164 | return m_Error; 165 | } 166 | inline void ResetError() 167 | { 168 | m_Error = PawnError::OK; 169 | } 170 | private: 171 | CError GenerateSelectQuery(string &dest); 172 | CError GenerateUpdateQuery(string &dest); 173 | CError GenerateInsertQuery(string &dest); 174 | CError GenerateDeleteQuery(string &dest); 175 | 176 | void WriteVariableNamesAsList(fmt::MemoryWriter &writer); 177 | }; 178 | 179 | class COrmManager : public CSingleton 180 | { 181 | friend class CSingleton; 182 | private: 183 | COrmManager() = default; 184 | ~COrmManager() = default; 185 | 186 | private: 187 | std::unordered_map m_Instances; 188 | 189 | public: 190 | OrmId_t Create(HandleId_t handleid, const char *table, 191 | CError &error); 192 | inline bool IsValid(OrmId_t id) 193 | { 194 | return m_Instances.find(id) != m_Instances.end(); 195 | } 196 | inline Orm_t Find(OrmId_t id) 197 | { 198 | return IsValid(id) ? m_Instances.at(id) : nullptr; 199 | } 200 | inline bool Delete(OrmId_t id) 201 | { 202 | return m_Instances.erase(id) == 1; 203 | } 204 | }; 205 | -------------------------------------------------------------------------------- /src/CQuery.cpp: -------------------------------------------------------------------------------- 1 | #include "CResult.hpp" 2 | #include "mysql.hpp" 3 | #include "CQuery.hpp" 4 | 5 | 6 | bool CQuery::Execute(MYSQL *connection) 7 | { 8 | CLog::Get()->Log(LogLevel::DEBUG, "CQuery::Execute(this={}, connection={})", 9 | static_cast(this), 10 | static_cast(connection)); 11 | 12 | int error = 0; 13 | 14 | default_clock::time_point exec_timepoint = default_clock::now(); 15 | error = mysql_real_query(connection, m_Query.c_str(), m_Query.length()); 16 | default_clock::duration exec_time = default_clock::now() - exec_timepoint; 17 | 18 | if (error != 0) 19 | { 20 | const char *error_str = mysql_error(connection); 21 | string msg = fmt::format("error #{} while executing query \"{}\": {}", 22 | mysql_errno(connection), m_Query, 23 | error_str ? error_str : "(nullptr)"); 24 | 25 | if (!m_DbgInfo.empty()) 26 | CLog::Get()->Log(LogLevel::ERROR, m_DbgInfo, msg.c_str()); 27 | else 28 | CLog::Get()->Log(LogLevel::ERROR, msg.c_str()); 29 | return false; 30 | } 31 | 32 | auto 33 | query_exec_time_milli = std::chrono::duration_cast(exec_time).count(), 34 | query_exec_time_micro = std::chrono::duration_cast(exec_time).count(); 35 | 36 | CLog::Get()->Log(LogLevel::INFO, 37 | "query \"{}\" successfully executed within {}.{} milliseconds", 38 | m_Query, query_exec_time_milli, 39 | query_exec_time_micro - (query_exec_time_milli * 1000)); 40 | 41 | m_Result = CResultSet::Create(connection, exec_time, m_Query); 42 | return m_Result != nullptr; 43 | } 44 | -------------------------------------------------------------------------------- /src/CQuery.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | using std::string; 7 | using std::function; 8 | 9 | #include "CLog.hpp" 10 | #include "types.hpp" 11 | #include "mysql.hpp" 12 | 13 | 14 | class CQuery 15 | { 16 | public: //constructor / deconstructor 17 | explicit CQuery(string &&query) : 18 | m_Query(query) 19 | { 20 | if (CDebugInfoManager::Get()->IsInfoAvailable()) 21 | m_DbgInfo = CDebugInfoManager::Get()->GetCurrentInfo(); 22 | } 23 | ~CQuery() = default; 24 | 25 | private: //variables 26 | string m_Query; 27 | function m_Callback; 28 | function m_ErrorCallback; 29 | ResultSet_t m_Result = nullptr; 30 | std::vector m_DbgInfo; 31 | 32 | public: //functions 33 | bool Execute(MYSQL *connection); 34 | ResultSet_t GetResult() 35 | { 36 | return m_Result; 37 | } 38 | 39 | inline void OnExecutionFinished(decltype(m_Callback) && cb) 40 | { 41 | m_Callback = std::move(cb); 42 | } 43 | inline void CallCallback() 44 | { 45 | if (m_Callback) 46 | m_Callback(m_Result); 47 | } 48 | 49 | inline void OnError(decltype(m_ErrorCallback) && cb) 50 | { 51 | m_ErrorCallback = std::move(cb); 52 | } 53 | inline void CallErrorCallback(unsigned int errorid, string error) 54 | { 55 | if (m_ErrorCallback) 56 | m_ErrorCallback(errorid, std::move(error)); 57 | } 58 | 59 | public: //factory function 60 | static inline Query_t Create(string query) 61 | { 62 | return std::make_shared(std::move(query)); 63 | } 64 | 65 | }; 66 | -------------------------------------------------------------------------------- /src/CResult.cpp: -------------------------------------------------------------------------------- 1 | #include "CResult.hpp" 2 | #include "CLog.hpp" 3 | 4 | #include 5 | 6 | #include "mysql.hpp" 7 | 8 | 9 | CResult::~CResult() 10 | { 11 | if (m_Data != nullptr) 12 | free(m_Data); 13 | } 14 | 15 | bool CResult::GetFieldName(unsigned int idx, string &dest) const 16 | { 17 | if (idx < m_NumFields) 18 | { 19 | dest = m_Fields.at(idx).Name; 20 | return true; 21 | } 22 | return false; 23 | } 24 | 25 | bool CResult::GetFieldType(unsigned int idx, enum_field_types &dest) const 26 | { 27 | if (idx < m_NumFields) 28 | { 29 | dest = m_Fields.at(idx).Type; 30 | return true; 31 | } 32 | return false; 33 | } 34 | 35 | bool CResult::GetRowData(unsigned int row, 36 | unsigned int fieldidx, const char **dest) const 37 | { 38 | if (row < m_NumRows && fieldidx < m_NumFields) 39 | { 40 | *dest = m_Data[row][fieldidx]; 41 | return true; 42 | } 43 | return false; 44 | } 45 | 46 | bool CResult::GetRowDataByName(unsigned int row, 47 | const string &field, const char **dest) const 48 | { 49 | if (row >= m_NumRows) 50 | return false; 51 | 52 | if (field.empty()) 53 | return false; 54 | 55 | 56 | for (unsigned int i = 0; i != m_NumFields; ++i) 57 | { 58 | if (m_Fields.at(i).Name.compare(field) == 0) 59 | { 60 | *dest = m_Data[row][i]; 61 | return true; 62 | } 63 | } 64 | 65 | return false; 66 | } 67 | 68 | CResultSet::~CResultSet() 69 | { 70 | for (auto *r : m_Results) 71 | delete r; 72 | } 73 | 74 | ResultSet_t CResultSet::Create(MYSQL *connection, 75 | default_clock::duration &exec_time, 76 | string query_str) 77 | { 78 | CLog::Get()->Log(LogLevel::DEBUG, 79 | "CResultSet::Create(connection={}, query_str='{}')", 80 | static_cast(connection), query_str); 81 | 82 | if (connection == nullptr) 83 | return nullptr; 84 | 85 | 86 | ResultSet_t resultset = ResultSet_t(new CResultSet); 87 | CLog::Get()->Log(LogLevel::DEBUG, "created new resultset '{}'", 88 | static_cast(resultset.get())); 89 | bool error = false; 90 | MYSQL_RES *raw_result = nullptr; 91 | do 92 | { 93 | raw_result = mysql_store_result(connection); 94 | CLog::Get()->Log(LogLevel::DEBUG, "fetched MySQL result '{}'", 95 | static_cast(raw_result)); 96 | 97 | if (raw_result == nullptr) //result empty: non-SELECT-type query or error 98 | { 99 | if (mysql_field_count(connection) == 0) //query is non-SELECT-type query 100 | { 101 | CResult *result = new CResult; 102 | 103 | resultset->m_Results.push_back(result); 104 | 105 | result->m_WarningCount = mysql_warning_count(connection); 106 | result->m_AffectedRows = mysql_affected_rows(connection); 107 | result->m_InsertId = mysql_insert_id(connection); 108 | } 109 | else //error 110 | { 111 | error = true; 112 | break; 113 | } 114 | } 115 | else //SELECT-type query 116 | { 117 | CResult *result = new CResult; 118 | 119 | resultset->m_Results.push_back(result); 120 | 121 | result->m_WarningCount = mysql_warning_count(connection); 122 | 123 | MYSQL_FIELD *mysql_field; 124 | MYSQL_ROW mysql_row; 125 | 126 | const my_ulonglong num_rows 127 | = result->m_NumRows = mysql_num_rows(raw_result); 128 | const unsigned int num_fields 129 | = result->m_NumFields = mysql_num_fields(raw_result); 130 | 131 | result->m_Fields.reserve(num_fields + 1); 132 | 133 | 134 | size_t row_data_size = 0; 135 | while ((mysql_field = mysql_fetch_field(raw_result))) 136 | { 137 | result->m_Fields.push_back({ mysql_field->name, mysql_field->type }); 138 | row_data_size += mysql_field->max_length + 1; 139 | } 140 | 141 | 142 | size_t 143 | mem_head_size = sizeof(char **) * static_cast(num_rows), 144 | mem_row_size = (sizeof(char *) * (num_fields + 1)) + ((row_data_size) * sizeof(char)); 145 | //+ 1 because there is another value in memory pointing to behind the last MySQL field 146 | 147 | //mem_row_size has to be a multiple of 8 148 | mem_row_size += 8 - (mem_row_size % 8); 149 | 150 | const size_t mem_size = mem_head_size + static_cast(num_rows) * mem_row_size; 151 | char ***mem_data = result->m_Data = static_cast(malloc(mem_size)); 152 | if (mem_data == nullptr) //error while allocating memory 153 | { 154 | error = true; 155 | break; 156 | } 157 | 158 | CLog::Get()->Log(LogLevel::DEBUG, 159 | "allocated {} bytes for PAWN result", mem_size); 160 | 161 | char **mem_offset = reinterpret_cast(&mem_data[num_rows]); 162 | for (size_t r = 0; r != num_rows; ++r) 163 | { 164 | mysql_row = mysql_fetch_row(raw_result); 165 | 166 | //copy mysql result data to our location 167 | mem_data[r] = mem_offset; 168 | mem_offset += mem_row_size / sizeof(char **); 169 | size_t copy_size = mysql_row[num_fields] - reinterpret_cast(mysql_row); 170 | memcpy(mem_data[r], mysql_row, copy_size); 171 | 172 | //correct the pointers of the copied mysql result data 173 | for (size_t f = 0; f != num_fields; ++f) 174 | { 175 | if (mysql_row[f] == nullptr) 176 | continue; 177 | size_t dist = mysql_row[f] - reinterpret_cast(mysql_row); 178 | mem_data[r][f] = reinterpret_cast(mem_data[r]) + dist; 179 | } 180 | //useless field we had to copy 181 | //set it to nullptr to avoid invalid memory access errors 182 | //(very unlikely to happen in first place though) 183 | mem_data[r][num_fields] = nullptr; 184 | } 185 | 186 | mysql_free_result(raw_result); 187 | raw_result = nullptr; 188 | } 189 | } 190 | while (mysql_next_result(connection) == 0); 191 | 192 | if (error) 193 | { 194 | mysql_free_result(raw_result); 195 | 196 | // go through all results to avoid "out of sync" error 197 | while (mysql_next_result(connection) == 0) 198 | mysql_free_result(mysql_store_result(connection)); 199 | } 200 | resultset->m_ExecTimeMilli = static_cast( 201 | std::chrono::duration_cast(exec_time).count()); 202 | resultset->m_ExecTimeMicro = static_cast( 203 | std::chrono::duration_cast(exec_time).count()); 204 | 205 | resultset->m_ExecQuery = std::move(query_str); 206 | 207 | return resultset; 208 | } 209 | 210 | ResultSet_t CResultSet::Merge(const std::vector &results) 211 | { 212 | ResultSet_t resultset = ResultSet_t(new CResultSet); 213 | for (ResultSet_t rset : results) 214 | { 215 | if (rset) 216 | { 217 | for (Result_t r : rset->m_Results) 218 | resultset->m_Results.push_back(r); 219 | 220 | rset->m_Results.clear(); 221 | } 222 | } 223 | return resultset; 224 | } 225 | 226 | 227 | 228 | ResultSetId_t CResultSetManager::StoreActiveResultSet() 229 | { 230 | if (m_ActiveResultSet == nullptr) 231 | return 0; 232 | 233 | ResultSetId_t id = 1; 234 | while (m_StoredResults.find(id) != m_StoredResults.end()) 235 | id++; 236 | 237 | m_StoredResults.emplace(id, m_ActiveResultSet); 238 | return id; 239 | } 240 | 241 | bool CResultSetManager::DeleteResultSet(ResultSetId_t id) 242 | { 243 | if (IsValidResultSet(id) == false) 244 | return false; 245 | 246 | if (GetResultSet(id) == GetActiveResultSet()) 247 | SetActiveResultSet(nullptr); 248 | 249 | return m_StoredResults.erase(id) == 1; 250 | } 251 | -------------------------------------------------------------------------------- /src/CResult.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "CSingleton.hpp" 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | using std::vector; 12 | using std::string; 13 | using std::unordered_map; 14 | using default_clock = std::chrono::steady_clock; 15 | 16 | #include "types.hpp" 17 | #include "mysql.hpp" 18 | 19 | 20 | class CResult 21 | { 22 | friend class CResultSet; 23 | public: 24 | struct FieldInfo 25 | { 26 | string Name; 27 | enum_field_types Type; 28 | }; 29 | 30 | private: //constructor / destructor 31 | CResult() = default; 32 | ~CResult(); 33 | 34 | private: //variables 35 | unsigned int m_NumFields = 0; 36 | my_ulonglong m_NumRows = 0; 37 | 38 | char ***m_Data = nullptr; 39 | vector m_Fields; 40 | 41 | my_ulonglong 42 | m_InsertId = 0, 43 | m_AffectedRows = 0; 44 | 45 | unsigned int m_WarningCount = 0; 46 | 47 | public: //functions 48 | inline my_ulonglong GetRowCount() const 49 | { 50 | return m_NumRows; 51 | } 52 | inline unsigned int GetFieldCount() const 53 | { 54 | return m_NumFields; 55 | } 56 | 57 | bool GetFieldName(unsigned int idx, string &dest) const; 58 | bool GetFieldType(unsigned int idx, enum_field_types &dest) const; 59 | bool GetRowData(unsigned int row, unsigned int fieldidx, 60 | const char **dest) const; 61 | inline bool GetRowData(unsigned int row, unsigned int fieldidx, 62 | string &dest) const 63 | { 64 | const char *cdest = nullptr; 65 | bool result = GetRowData(row, fieldidx, &cdest); 66 | if (result && cdest != nullptr) 67 | dest = cdest; 68 | return result; 69 | } 70 | bool GetRowDataByName(unsigned int row, const string &field, 71 | const char **dest) const; 72 | bool GetRowDataByName(unsigned int row, const string &field, 73 | string &dest) const 74 | { 75 | const char *cdest = nullptr; 76 | bool result = GetRowDataByName(row, field, &cdest); 77 | if (result && cdest != nullptr) 78 | dest = cdest; 79 | return result; 80 | } 81 | 82 | inline my_ulonglong InsertId() const 83 | { 84 | return m_InsertId; 85 | } 86 | inline my_ulonglong AffectedRows() const 87 | { 88 | return m_AffectedRows; 89 | } 90 | inline unsigned int WarningCount() const 91 | { 92 | return m_WarningCount; 93 | } 94 | }; 95 | 96 | class CResultSet 97 | { 98 | public: 99 | enum class TimeType 100 | { 101 | MILLISECONDS, 102 | MICROSECONDS, 103 | }; 104 | 105 | private: 106 | CResultSet() = default; 107 | 108 | public: 109 | ~CResultSet(); 110 | 111 | private: 112 | vector m_Results; 113 | Result_t m_ActiveResult = nullptr; 114 | 115 | unsigned int 116 | m_ExecTimeMilli = 0, 117 | m_ExecTimeMicro = 0; 118 | 119 | string m_ExecQuery; 120 | 121 | public: 122 | inline const Result_t GetActiveResult() 123 | { 124 | if (m_ActiveResult == nullptr && !m_Results.empty()) 125 | m_ActiveResult = m_Results.front(); 126 | return m_ActiveResult; 127 | } 128 | bool SetActiveResult(size_t result_idx) 129 | { 130 | if (result_idx < GetResultCount()) 131 | { 132 | m_ActiveResult = m_Results.at(result_idx); 133 | return true; 134 | } 135 | return false; 136 | } 137 | inline size_t GetResultCount() 138 | { 139 | return m_Results.size(); 140 | } 141 | inline const Result_t GetResultByIndex(size_t idx) 142 | { 143 | return idx < GetResultCount() ? m_Results.at(idx) : nullptr; 144 | } 145 | 146 | inline unsigned int GetExecutionTime(TimeType type) const 147 | { 148 | if (type == TimeType::MILLISECONDS) 149 | return m_ExecTimeMilli; 150 | return m_ExecTimeMicro; 151 | } 152 | inline const string &GetExecutedQuery() const 153 | { 154 | return m_ExecQuery; 155 | } 156 | 157 | public: //factory function 158 | static ResultSet_t Create(MYSQL *connection, 159 | default_clock::duration &exec_time, 160 | string query_str); 161 | 162 | public: //helper functions 163 | static ResultSet_t Merge(const std::vector &results); 164 | 165 | }; 166 | 167 | class CResultSetManager : public CSingleton< CResultSetManager > 168 | { 169 | friend class CSingleton< CResultSetManager >; 170 | private: 171 | CResultSetManager() = default; 172 | ~CResultSetManager() = default; 173 | 174 | private: 175 | ResultSet_t m_ActiveResultSet; 176 | unordered_map m_StoredResults; 177 | 178 | public: 179 | inline void SetActiveResultSet(ResultSet_t resultset) 180 | { 181 | m_ActiveResultSet = resultset; 182 | } 183 | inline ResultSet_t GetActiveResultSet() 184 | { 185 | return m_ActiveResultSet; 186 | } 187 | 188 | inline bool IsValidResultSet(ResultSetId_t id) 189 | { 190 | return m_StoredResults.find(id) != m_StoredResults.end(); 191 | } 192 | ResultSetId_t StoreActiveResultSet(); 193 | bool DeleteResultSet(ResultSetId_t id); 194 | inline ResultSet_t GetResultSet(ResultSetId_t id) 195 | { 196 | return IsValidResultSet(id) ? m_StoredResults.at(id) : nullptr; 197 | } 198 | 199 | }; 200 | -------------------------------------------------------------------------------- /src/CSingleton.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | 4 | template 5 | class CSingleton 6 | { 7 | protected: 8 | static T *m_Instance; 9 | 10 | public: 11 | CSingleton() 12 | { } 13 | virtual ~CSingleton() 14 | { } 15 | 16 | inline static T *Get() 17 | { 18 | if (m_Instance == nullptr) 19 | m_Instance = new T; 20 | return m_Instance; 21 | } 22 | 23 | inline static void Destroy() 24 | { 25 | if (m_Instance != nullptr) 26 | { 27 | delete m_Instance; 28 | m_Instance = nullptr; 29 | } 30 | } 31 | }; 32 | 33 | template 34 | T* CSingleton::m_Instance = nullptr; 35 | -------------------------------------------------------------------------------- /src/main.cpp: -------------------------------------------------------------------------------- 1 | #include "sdk.hpp" 2 | #include "natives.hpp" 3 | #include "CHandle.hpp" 4 | #include "CCallback.hpp" 5 | #include "CResult.hpp" 6 | #include "CDispatcher.hpp" 7 | #include "COptions.hpp" 8 | #include "COrm.hpp" 9 | #include "CLog.hpp" 10 | #include "version.hpp" 11 | 12 | #include "mysql.hpp" 13 | 14 | 15 | 16 | extern void *pAMXFunctions; 17 | logprintf_t logprintf; 18 | 19 | 20 | PLUGIN_EXPORT unsigned int PLUGIN_CALL Supports() 21 | { 22 | return SUPPORTS_VERSION | SUPPORTS_AMX_NATIVES | SUPPORTS_PROCESS_TICK; 23 | } 24 | 25 | PLUGIN_EXPORT bool PLUGIN_CALL Load(void **ppData) 26 | { 27 | pAMXFunctions = ppData[PLUGIN_DATA_AMX_EXPORTS]; 28 | logprintf = (logprintf_t) ppData[PLUGIN_DATA_LOGPRINTF]; 29 | 30 | if (mysql_library_init(0, NULL, NULL)) 31 | { 32 | logprintf(" >> plugin.mysql: can't initialize MySQL library."); 33 | return false; 34 | } 35 | 36 | logprintf(" >> plugin.mysql: " MYSQL_VERSION " successfully loaded."); 37 | return true; 38 | } 39 | 40 | PLUGIN_EXPORT void PLUGIN_CALL Unload() 41 | { 42 | logprintf("plugin.mysql: Unloading plugin..."); 43 | 44 | COrmManager::CSingleton::Destroy(); 45 | CHandleManager::CSingleton::Destroy(); 46 | CCallbackManager::CSingleton::Destroy(); 47 | CResultSetManager::CSingleton::Destroy(); 48 | CDispatcher::CSingleton::Destroy(); 49 | COptionManager::CSingleton::Destroy(); 50 | CLog::CSingleton::Destroy(); 51 | samplog::Api::Destroy(); 52 | 53 | mysql_library_end(); 54 | 55 | logprintf("plugin.mysql: Plugin unloaded."); 56 | } 57 | 58 | PLUGIN_EXPORT void PLUGIN_CALL ProcessTick() 59 | { 60 | CDispatcher::Get()->Process(); 61 | } 62 | 63 | 64 | extern "C" const AMX_NATIVE_INFO native_list[] = 65 | { 66 | AMX_DEFINE_NATIVE(orm_create) 67 | AMX_DEFINE_NATIVE(orm_destroy) 68 | 69 | AMX_DEFINE_NATIVE(orm_errno) 70 | 71 | AMX_DEFINE_NATIVE(orm_apply_cache) 72 | 73 | AMX_DEFINE_NATIVE(orm_select) 74 | AMX_DEFINE_NATIVE(orm_update) 75 | AMX_DEFINE_NATIVE(orm_insert) 76 | AMX_DEFINE_NATIVE(orm_delete) 77 | 78 | AMX_DEFINE_NATIVE(orm_save) 79 | 80 | AMX_DEFINE_NATIVE(orm_addvar_int) 81 | AMX_DEFINE_NATIVE(orm_addvar_float) 82 | AMX_DEFINE_NATIVE(orm_addvar_string) 83 | 84 | AMX_DEFINE_NATIVE(orm_clear_vars) 85 | AMX_DEFINE_NATIVE(orm_delvar) 86 | AMX_DEFINE_NATIVE(orm_setkey) 87 | 88 | 89 | AMX_DEFINE_NATIVE(mysql_connect) 90 | AMX_DEFINE_NATIVE(mysql_connect_file) 91 | AMX_DEFINE_NATIVE(mysql_close) 92 | 93 | AMX_DEFINE_NATIVE(mysql_unprocessed_queries) 94 | AMX_DEFINE_NATIVE(mysql_global_options) 95 | 96 | AMX_DEFINE_NATIVE(mysql_init_options) 97 | AMX_DEFINE_NATIVE(mysql_set_option) 98 | 99 | AMX_DEFINE_NATIVE(mysql_pquery) 100 | AMX_DEFINE_NATIVE(mysql_tquery) 101 | AMX_DEFINE_NATIVE(mysql_query) 102 | AMX_DEFINE_NATIVE(mysql_tquery_file) 103 | AMX_DEFINE_NATIVE(mysql_query_file) 104 | 105 | AMX_DEFINE_NATIVE(mysql_errno) 106 | AMX_DEFINE_NATIVE(mysql_error) 107 | AMX_DEFINE_NATIVE(mysql_escape_string) 108 | AMX_DEFINE_NATIVE(mysql_format) 109 | AMX_DEFINE_NATIVE(mysql_get_charset) 110 | AMX_DEFINE_NATIVE(mysql_set_charset) 111 | AMX_DEFINE_NATIVE(mysql_stat) 112 | 113 | 114 | AMX_DEFINE_NATIVE(cache_get_row_count) 115 | AMX_DEFINE_NATIVE(cache_get_field_count) 116 | AMX_DEFINE_NATIVE(cache_get_result_count) 117 | AMX_DEFINE_NATIVE(cache_get_field_name) 118 | AMX_DEFINE_NATIVE(cache_get_field_type) 119 | AMX_DEFINE_NATIVE(cache_set_result) 120 | 121 | AMX_DEFINE_NATIVE(cache_get_value_index) 122 | AMX_DEFINE_NATIVE(cache_get_value_index_int) 123 | AMX_DEFINE_NATIVE(cache_get_value_index_float) 124 | AMX_DEFINE_NATIVE(cache_is_value_index_null) 125 | 126 | AMX_DEFINE_NATIVE(cache_get_value_name) 127 | AMX_DEFINE_NATIVE(cache_get_value_name_int) 128 | AMX_DEFINE_NATIVE(cache_get_value_name_float) 129 | AMX_DEFINE_NATIVE(cache_is_value_name_null) 130 | 131 | AMX_DEFINE_NATIVE(cache_save) 132 | AMX_DEFINE_NATIVE(cache_delete) 133 | AMX_DEFINE_NATIVE(cache_set_active) 134 | AMX_DEFINE_NATIVE(cache_unset_active) 135 | AMX_DEFINE_NATIVE(cache_is_any_active) 136 | AMX_DEFINE_NATIVE(cache_is_valid) 137 | 138 | AMX_DEFINE_NATIVE(cache_affected_rows) 139 | AMX_DEFINE_NATIVE(cache_insert_id) 140 | AMX_DEFINE_NATIVE(cache_warning_count) 141 | 142 | AMX_DEFINE_NATIVE(cache_get_query_exec_time) 143 | AMX_DEFINE_NATIVE(cache_get_query_string) 144 | { NULL, NULL } 145 | }; 146 | 147 | PLUGIN_EXPORT int PLUGIN_CALL AmxLoad(AMX *amx) 148 | { 149 | samplog::Api::Get()->RegisterAmx(amx); 150 | CCallbackManager::Get()->AddAmx(amx); 151 | return amx_Register(amx, native_list, -1); 152 | } 153 | 154 | PLUGIN_EXPORT int PLUGIN_CALL AmxUnload(AMX *amx) 155 | { 156 | samplog::Api::Get()->EraseAmx(amx); 157 | CCallbackManager::Get()->RemoveAmx(amx); 158 | return AMX_ERR_NONE; 159 | } 160 | -------------------------------------------------------------------------------- /src/misc.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #pragma warning (push) 7 | #pragma warning (disable: 4244 4018 4348) 8 | 9 | #include 10 | #include 11 | 12 | using std::string; 13 | namespace qi = boost::spirit::qi; 14 | namespace karma = boost::spirit::karma; 15 | 16 | 17 | template 18 | bool ConvertStrToData(const string &src, T &dest) 19 | { 20 | return qi::parse(src.begin(), src.end(), 21 | typename std::conditional< 22 | std::is_floating_point::value, 23 | qi::real_parser, 24 | qi::int_parser 25 | >::type(), 26 | dest); 27 | } 28 | 29 | template 30 | bool ConvertStrToData(const char *src, T &dest) 31 | { 32 | return src != nullptr && qi::parse(src, src + strlen(src), 33 | typename std::conditional< 34 | std::is_floating_point::value, 35 | qi::real_parser, 36 | qi::int_parser 37 | >::type(), 38 | dest); 39 | } 40 | 41 | 42 | template 43 | bool ConvertDataToStr(T src, string &dest) 44 | { 45 | return karma::generate(std::back_inserter(dest), 46 | typename std::conditional< 47 | std::is_floating_point::value, 48 | karma::real_generator, 49 | typename std::conditional< 50 | std::is_signed::value, 51 | karma::int_generator, 52 | karma::uint_generator 53 | >::type 54 | >::type(), 55 | src); 56 | } 57 | 58 | template<> //bool specialization 59 | inline bool ConvertStrToData(const string &src, bool &dest) 60 | { 61 | return qi::parse(src.begin(), src.end(), qi::bool_, dest); 62 | } 63 | 64 | template<> //bool specialization 65 | inline bool ConvertDataToStr(bool src, string &dest) 66 | { 67 | return karma::generate(std::back_inserter(dest), karma::bool_, src); 68 | } 69 | 70 | 71 | #pragma warning (pop) 72 | -------------------------------------------------------------------------------- /src/mysql.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #ifdef WIN32 4 | # include 5 | # undef ERROR 6 | #endif 7 | 8 | #ifdef LINUX 9 | # include 10 | #else 11 | # include 12 | #endif 13 | -------------------------------------------------------------------------------- /src/natives.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "sdk.hpp" 4 | 5 | #define AMX_DECLARE_NATIVE(native) \ 6 | cell AMX_NATIVE_CALL native(AMX *amx, cell *params) 7 | 8 | #define AMX_DEFINE_NATIVE(native) \ 9 | {#native, Native::native}, 10 | 11 | 12 | namespace Native 13 | { 14 | //ORM natives 15 | AMX_DECLARE_NATIVE(orm_create); 16 | AMX_DECLARE_NATIVE(orm_destroy); 17 | 18 | AMX_DECLARE_NATIVE(orm_errno); 19 | 20 | AMX_DECLARE_NATIVE(orm_apply_cache); 21 | 22 | AMX_DECLARE_NATIVE(orm_select); 23 | AMX_DECLARE_NATIVE(orm_update); 24 | AMX_DECLARE_NATIVE(orm_insert); 25 | AMX_DECLARE_NATIVE(orm_delete); 26 | 27 | AMX_DECLARE_NATIVE(orm_save); 28 | 29 | AMX_DECLARE_NATIVE(orm_addvar_int); 30 | AMX_DECLARE_NATIVE(orm_addvar_float); 31 | AMX_DECLARE_NATIVE(orm_addvar_string); 32 | 33 | AMX_DECLARE_NATIVE(orm_clear_vars); 34 | AMX_DECLARE_NATIVE(orm_delvar); 35 | AMX_DECLARE_NATIVE(orm_setkey); 36 | 37 | 38 | //MySQL natives 39 | AMX_DECLARE_NATIVE(mysql_connect); 40 | AMX_DECLARE_NATIVE(mysql_connect_file); 41 | AMX_DECLARE_NATIVE(mysql_close); 42 | 43 | AMX_DECLARE_NATIVE(mysql_unprocessed_queries); 44 | AMX_DECLARE_NATIVE(mysql_global_options); 45 | 46 | AMX_DECLARE_NATIVE(mysql_init_options); 47 | AMX_DECLARE_NATIVE(mysql_set_option); 48 | 49 | AMX_DECLARE_NATIVE(mysql_pquery); 50 | AMX_DECLARE_NATIVE(mysql_tquery); 51 | AMX_DECLARE_NATIVE(mysql_query); 52 | AMX_DECLARE_NATIVE(mysql_tquery_file); 53 | AMX_DECLARE_NATIVE(mysql_query_file); 54 | 55 | AMX_DECLARE_NATIVE(mysql_errno); 56 | AMX_DECLARE_NATIVE(mysql_error); 57 | AMX_DECLARE_NATIVE(mysql_escape_string); 58 | AMX_DECLARE_NATIVE(mysql_format); 59 | AMX_DECLARE_NATIVE(mysql_set_charset); 60 | AMX_DECLARE_NATIVE(mysql_get_charset); 61 | AMX_DECLARE_NATIVE(mysql_stat); 62 | 63 | 64 | //Cache natives 65 | AMX_DECLARE_NATIVE(cache_get_row_count); 66 | AMX_DECLARE_NATIVE(cache_get_field_count); 67 | AMX_DECLARE_NATIVE(cache_get_result_count); 68 | AMX_DECLARE_NATIVE(cache_get_field_name); 69 | AMX_DECLARE_NATIVE(cache_get_field_type); 70 | AMX_DECLARE_NATIVE(cache_set_result); 71 | 72 | AMX_DECLARE_NATIVE(cache_get_value_index); 73 | AMX_DECLARE_NATIVE(cache_get_value_index_int); 74 | AMX_DECLARE_NATIVE(cache_get_value_index_float); 75 | AMX_DECLARE_NATIVE(cache_is_value_index_null); 76 | 77 | AMX_DECLARE_NATIVE(cache_get_value_name); 78 | AMX_DECLARE_NATIVE(cache_get_value_name_int); 79 | AMX_DECLARE_NATIVE(cache_get_value_name_float); 80 | AMX_DECLARE_NATIVE(cache_is_value_name_null); 81 | 82 | AMX_DECLARE_NATIVE(cache_save); 83 | AMX_DECLARE_NATIVE(cache_delete); 84 | AMX_DECLARE_NATIVE(cache_set_active); 85 | AMX_DECLARE_NATIVE(cache_unset_active); 86 | AMX_DECLARE_NATIVE(cache_is_any_active); 87 | AMX_DECLARE_NATIVE(cache_is_valid); 88 | 89 | AMX_DECLARE_NATIVE(cache_affected_rows); 90 | AMX_DECLARE_NATIVE(cache_insert_id); 91 | AMX_DECLARE_NATIVE(cache_warning_count); 92 | 93 | AMX_DECLARE_NATIVE(cache_get_query_exec_time); 94 | AMX_DECLARE_NATIVE(cache_get_query_string); 95 | }; 96 | -------------------------------------------------------------------------------- /src/plugin.def: -------------------------------------------------------------------------------- 1 | LIBRARY "mysql" 2 | EXPORTS 3 | Supports 4 | Load 5 | Unload 6 | AmxLoad 7 | AmxUnload 8 | ProcessTick -------------------------------------------------------------------------------- /src/sdk.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | typedef void(*logprintf_t)(const char* format, ...); 8 | -------------------------------------------------------------------------------- /src/types.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | using std::shared_ptr; 8 | using std::unique_ptr; 9 | using std::tuple; 10 | 11 | class CHandle; 12 | class CQuery; 13 | class CCallback; 14 | class CResultSet; 15 | class CResult; 16 | class COptions; 17 | class COrm; 18 | 19 | 20 | using Handle_t = CHandle *; 21 | using HandleId_t = unsigned int; 22 | 23 | using Query_t = shared_ptr; 24 | 25 | using Callback_t = shared_ptr; 26 | 27 | using ResultSet_t = shared_ptr; 28 | using Result_t = CResult *; 29 | using ResultSetId_t = unsigned int; 30 | 31 | using OptionsId_t = unsigned int; 32 | 33 | using DispatchFunction_t = std::function < void() >; 34 | 35 | using Orm_t = shared_ptr; 36 | using OrmId_t = unsigned int; 37 | -------------------------------------------------------------------------------- /src/version.hpp.in: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #define MYSQL_VERSION "@MYSQL_PLUGIN_VERSION@" 4 | -------------------------------------------------------------------------------- /tests/speed_test.pwn: -------------------------------------------------------------------------------- 1 | 2 | //#define IS_R40 3 | #define BENCH_QUERY 4 | //#define BENCH_RESULT_BY_INDEX 5 | //#define BENCH_RESULT_BY_NAME 6 | 7 | #define NUM_RUNS 10 8 | #define ITERATIONS 1000 9 | 10 | 11 | //----------------- 12 | #if defined BENCH_RESULT_BY_INDEX && defined BENCH_RESULT_BY_NAME 13 | #error Doesn't make much sense testing by index and by name 14 | #endif 15 | 16 | #include 17 | 18 | #if defined IS_R40 19 | #include 20 | #define MYSQL_VERSION_STRING "R40" 21 | #else 22 | #include 23 | #define MYSQL_VERSION_STRING "R39-2" 24 | #define DEFAULT_MYSQL_HANDLE 1 25 | #endif 26 | 27 | enum E_TIME_POINT 28 | { 29 | START, 30 | END 31 | } 32 | new TimeData[NUM_RUNS][E_TIME_POINT]; 33 | new AverageTime = 0; 34 | new CurrentRun = 0; 35 | 36 | #if defined BENCH_RESULT_BY_INDEX || defined BENCH_RESULT_BY_NAME 37 | static const FieldStruct[3][2][] = 38 | { 39 | {0, "id"}, 40 | {1, "username"}, 41 | {2, "health"} 42 | }; 43 | #endif 44 | 45 | main() { } 46 | 47 | public OnGameModeInit() 48 | { 49 | #if defined IS_R40 50 | mysql_log(NONE); 51 | #else 52 | mysql_log(LOG_NONE); 53 | #endif 54 | 55 | mysql_connect("127.0.0.1", "root", "mysql_test", "1234"); 56 | if (mysql_errno(DEFAULT_MYSQL_HANDLE) != 0) 57 | return print("Failed to connect!"); 58 | else 59 | printf("Connection established, starting benchmark..."); 60 | 61 | SetTimer("Deferred_MainStart", 1000, false); //to avoid that "Number of vehicles" message 62 | 63 | return 1; 64 | } 65 | 66 | forward Deferred_MainStart(); 67 | public Deferred_MainStart() 68 | { 69 | for(new i=0; i < 5; i++) 70 | printf(""); 71 | printf("["MYSQL_VERSION_STRING"] running benchmark(s) [runs: "#NUM_RUNS"] [iterations: "#ITERATIONS"]"); 72 | #if defined BENCH_QUERY 73 | printf("\tquery exec speed"); 74 | #endif 75 | #if defined BENCH_RESULT_BY_INDEX 76 | printf("\tresult fetching by index"); 77 | #endif 78 | #if defined BENCH_RESULT_BY_NAME 79 | printf("\tresult fetching by name"); 80 | #endif 81 | 82 | StartRun(2000); 83 | return 1; 84 | } 85 | 86 | StartRun(delay = 1000) 87 | { 88 | SetTimer("Deferred_StartRun", delay, false); 89 | } 90 | forward Deferred_StartRun(); 91 | public Deferred_StartRun() 92 | { 93 | #if defined BENCH_QUERY 94 | TimeData[CurrentRun][START] = GetTickCount(); 95 | #endif 96 | 97 | for (new i = 1; i <= ITERATIONS; i++) 98 | { 99 | mysql_tquery(DEFAULT_MYSQL_HANDLE, 100 | "SELECT * FROM `speedtable`", 101 | "OnTableFetch", "d", i); 102 | } 103 | return 1; 104 | } 105 | 106 | 107 | forward public OnTableFetch(iteration); 108 | public OnTableFetch(iteration) 109 | { 110 | #if defined BENCH_RESULT_BY_INDEX || defined BENCH_RESULT_BY_NAME 111 | if(iteration == 1) 112 | TimeData[CurrentRun][START] = GetTickCount(); 113 | 114 | new dest[32]; 115 | new rows, fields; 116 | cache_get_data(rows, fields); 117 | for(new f = 0; f < fields; ++f) 118 | { 119 | for (new r = 0; r < rows; ++r) 120 | { 121 | #if defined BENCH_RESULT_BY_INDEX 122 | cache_get_row(r, FieldStruct[f][0][0], dest); 123 | #else 124 | cache_get_field_content(r, FieldStruct[f][1], dest); 125 | #endif 126 | } 127 | } 128 | #endif 129 | 130 | if (iteration == ITERATIONS) 131 | { 132 | TimeData[CurrentRun][END] = GetTickCount(); 133 | 134 | new time = TimeData[CurrentRun][END] - TimeData[CurrentRun][START]; 135 | printf("\t[%d]: %d (%.4f/iteration)", CurrentRun+1, time, float(time)/float(ITERATIONS)); 136 | AverageTime += time; 137 | 138 | if(++CurrentRun != NUM_RUNS) 139 | { 140 | StartRun(); 141 | } 142 | else 143 | { 144 | printf(""); 145 | printf("["MYSQL_VERSION_STRING"] benchmark done"); 146 | AverageTime /= NUM_RUNS; 147 | printf("--->\t %d (%.4f/iteration)", AverageTime, float(AverageTime)/float(ITERATIONS)); 148 | for(new i=0; i < 5; ++i) 149 | printf(""); 150 | } 151 | } 152 | return 1; 153 | } 154 | 155 | public OnGameModeExit() 156 | { 157 | mysql_close(DEFAULT_MYSQL_HANDLE); 158 | return 1; 159 | } 160 | -------------------------------------------------------------------------------- /tests/test_data/mysql-invalid.ini: -------------------------------------------------------------------------------- 1 | hostname = 127.0.0.1 2 | username = invalid-user 3 | password = 1234 4 | database = test -------------------------------------------------------------------------------- /tests/test_data/mysql-invalid1.ini: -------------------------------------------------------------------------------- 1 | hostname = localhost 2 | username = root 3 | password = 1234 4 | databaaase = test -------------------------------------------------------------------------------- /tests/test_data/mysql-invalid2.ini: -------------------------------------------------------------------------------- 1 | hostname = localhost 2 | uuusername = root 3 | password = 1234 4 | database = test -------------------------------------------------------------------------------- /tests/test_data/mysql-invalid3.ini: -------------------------------------------------------------------------------- 1 | hooostname = localhost 2 | username = root 3 | password = 1234 4 | database = test -------------------------------------------------------------------------------- /tests/test_data/mysql-invalid4.ini: -------------------------------------------------------------------------------- 1 | hostname = localhost 2 | username = root 3 | passwordddd = 1234 4 | database = test -------------------------------------------------------------------------------- /tests/test_data/mysql-invalid5.ini: -------------------------------------------------------------------------------- 1 | hostname = localhost 2 | username = root 3 | password x 1234 4 | database = test -------------------------------------------------------------------------------- /tests/test_data/mysql.ini: -------------------------------------------------------------------------------- 1 | hostname = 127.0.0.1 2 | username = tester 3 | password = 1234 4 | database = test 5 | # auto_reconnect = true 6 | multi_statements = true 7 | # pool_size = 3 8 | ; server_port = 3306 -------------------------------------------------------------------------------- /tests/test_data/scriptfiles/test.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE `test` ( 2 | `number` int(11) NOT NULL AUTO_INCREMENT, 3 | `text` varchar(45) DEFAULT NULL, 4 | `float` float DEFAULT NULL, 5 | PRIMARY KEY (`number`) 6 | ) ENGINE=InnoDB AUTO_INCREMENT=100 DEFAULT CHARSET=utf8; 7 | 8 | INSERT INTO `test` VALUES 9 | (1, 'asdf', 3.142341), 10 | (2, 'asdf', 3.142341), 11 | (3, 'asdf', 3.142341), 12 | (4, 'asdf', 3.142341), 13 | (5, 'asdf', 3.142341), 14 | (6, 'asdf', 3.142341), 15 | (7, 'asdf', 3.142341), 16 | (8, 'asdf', 3.142341), 17 | (9, 'asdf', 3.142341), 18 | (10, 'asdf', 3.142341); 19 | 20 | CREATE TABLE `test2` ( 21 | `id` INT NOT NULL AUTO_INCREMENT, 22 | `text` VARCHAR(45) NULL DEFAULT NULL, 23 | PRIMARY KEY (`id`)); 24 | 25 | INSERT INTO `test2` VALUES (); 26 | INSERT INTO `test2` (`text`) VALUE ('asdf'); 27 | --------------------------------------------------------------------------------