├── .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 | | [](https://travis-ci.org/pBlueG/SA-MP-MySQL) | [](https://ci.appveyor.com/project/maddinat0r/sa-mp-mysql/branch/master) | [](https://github.com/pBlueG/SA-MP-MySQL/releases) | [](https://github.com/pBlueG/SA-MP-MySQL/releases)
[](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