├── Addons
└── NetworkZones
│ ├── NetworkZones_MySql.cs
│ └── Readme.md
├── Database_Compat.cs
├── Database_MySql.cs
├── LICENSE
├── MySql.Data.dll
└── Readme.md
/Addons/NetworkZones/NetworkZones_MySql.cs:
--------------------------------------------------------------------------------
1 | using UnityEngine;
2 | using Mirror;
3 | using System;
4 | using System.IO;
5 | using System.Linq;
6 | using System.Collections.Generic;
7 | using System.Data;
8 | using MySql.Data; // From MySql.Data.dll in Plugins folder
9 | using MySql.Data.MySqlClient; // From MySql.Data.dll in Plugins folder
10 |
11 |
12 | using SqlParameter = MySql.Data.MySqlClient.MySqlParameter;
13 |
14 |
15 | ///
16 | /// Adds support for NeworkZones addon.
17 | /// you can delete this file if you don't use NetworkZones
18 | ///
19 | public partial class Database
20 | {
21 |
22 | static void Initialize_Zone()
23 | {
24 | ExecuteNonQueryMySql(@"
25 | CREATE TABLE IF NOT EXISTS character_scene (
26 | `character` VARCHAR(16) NOT NULL,
27 | scenepath VARCHAR(255) NOT NULL,
28 | PRIMARY KEY(`character`),
29 | FOREIGN KEY(`character`)
30 | REFERENCES characters(name)
31 | ON DELETE CASCADE ON UPDATE CASCADE
32 | ) CHARACTER SET=utf8mb4");
33 |
34 |
35 | ExecuteNonQueryMySql(@"
36 | CREATE TABLE IF NOT EXISTS zones_online (
37 | id INT NOT NULL AUTO_INCREMENT,
38 | PRIMARY KEY(id),
39 | online TIMESTAMP NOT NULL
40 | ) CHARACTER SET=utf8mb4");
41 | }
42 |
43 |
44 | public static bool IsCharacterOnlineAnywhere(string characterName)
45 | {
46 | var obj = ExecuteScalarMySql("SELECT online FROM characters WHERE name=@name", new SqlParameter("@name", characterName));
47 | if (obj != null)
48 | {
49 | var time = (DateTime)obj;
50 | double elapsedSeconds = (DateTime.Now - time).TotalSeconds;
51 | float saveInterval = ((NetworkManagerMMO)NetworkManager.singleton).saveInterval;
52 | //UnityEngine.Debug.Log("datetime=" + time + " elapsed=" + elapsedSeconds + " saveinterval=" + saveInterval);
53 | return elapsedSeconds < saveInterval * 2;
54 | }
55 | return false;
56 | }
57 |
58 | public static bool AnyAccountCharacterOnline(string account)
59 | {
60 | List characters = CharactersForAccount(account);
61 | return characters.Any(IsCharacterOnlineAnywhere);
62 | }
63 |
64 | public static string GetCharacterScenePath(string characterName)
65 | {
66 | object obj = ExecuteScalarMySql("SELECT scenepath FROM character_scene WHERE `character`=@character", new SqlParameter("@character", characterName));
67 | return obj != null ? (string)obj : "";
68 | }
69 |
70 | public static void SaveCharacterScenePath(string characterName, string scenePath)
71 | {
72 | var query = @"
73 | INSERT INTO character_scene
74 | SET
75 | `character`=@character,
76 | scenepath=@scenepath
77 | ON DUPLICATE KEY UPDATE
78 | scenepath=@scenepath";
79 |
80 | ExecuteNonQueryMySql(query,
81 | new SqlParameter("@character", characterName),
82 | new SqlParameter("@scenepath", scenePath));
83 | }
84 |
85 | // a zone is online if the online string is not empty and if the time
86 | // difference is less than the write interval * multiplier
87 | // (* multiplier to have some tolerance)
88 | public static double TimeElapsedSinceMainZoneOnline()
89 | {
90 | var obj = ExecuteScalarMySql("SELECT online FROM zones_online");
91 | if (obj != null)
92 | {
93 | var time = (DateTime)obj;
94 | return (DateTime.Now - time).TotalSeconds;
95 | }
96 | return Mathf.Infinity;
97 | }
98 |
99 | // should only be called by main zone
100 | public static void SaveMainZoneOnlineTime()
101 | {
102 | var query = @"
103 | INSERT INTO zones_online
104 | SET
105 | id=@id,
106 | online=@online
107 | ON DUPLICATE KEY UPDATE
108 | online=@online";
109 |
110 | ExecuteNonQueryMySql(query,
111 | new SqlParameter("@id", 1),
112 | new SqlParameter("@online", DateTime.Now));
113 | }
114 | }
115 |
--------------------------------------------------------------------------------
/Addons/NetworkZones/Readme.md:
--------------------------------------------------------------------------------
1 | # Network Zones
2 |
3 | This class implements MySQL backend for the networkzones addon version 2018-03-22.
4 |
5 | ## Installation instructions:
6 | 1. Add NetworkZones_MySql.cs in to your Addons/NetworkZones folder.
7 | 2. Search for "public partial class Database" in NetworkZones.cs and delete this class or comment it out.
8 |
9 | The same as paulpach says applies here too
10 |
11 | "This is provided as is, no warranty, I am not responsible if it offers your first born child in sacrifice to the devil.
12 | I am simply offering it for free for anyone who might want it."
13 |
--------------------------------------------------------------------------------
/Database_Compat.cs:
--------------------------------------------------------------------------------
1 | using UnityEngine;
2 | using System.Collections;
3 | using System.Linq;
4 |
5 | using MySql.Data.MySqlClient;
6 |
7 | using Mono.Data.Sqlite;
8 | using System.Collections.Generic;
9 |
10 |
11 | // These methods are for having backwards compatibility with sqlite
12 | // you can delete this class if you don't have plugins that
13 | // use database methods
14 | public partial class Database
15 | {
16 | private static MySqlParameter[] ToMysqlParameters(SqliteParameter[] args)
17 | {
18 | return args.Select(x => new MySqlParameter(x.ParameterName, x.Value)).ToArray();
19 | }
20 |
21 | ///
22 | /// Backwards compatible method.
23 | /// Execute a statement that is not a query, it translates sqliteparameters
24 | /// into mysqlparameters
25 | ///
26 | /// Sql.
27 | /// Arguments.
28 | public static void ExecuteNonQuery(string sql, params SqliteParameter[] args)
29 | {
30 | ExecuteNonQueryMySql(sql, ToMysqlParameters(args));
31 | }
32 |
33 | ///
34 | /// Backwards compatible method.
35 | /// Execute a scalar query, it translates sqliteparameters
36 | /// into mysqlparameters
37 | ///
38 | /// Sql.
39 | /// Arguments.
40 | public static object ExecuteScalar(string sql, params SqliteParameter[] args)
41 | {
42 | return ExecuteScalarMySql(sql, ToMysqlParameters(args));
43 | }
44 |
45 | ///
46 | /// Backwards compatible method.
47 | /// Execute a query, it translates sqliteparameters
48 | /// into mysqlparameters
49 | ///
50 | /// Sql.
51 | /// Arguments.
52 | public static List> ExecuteReader(string sql, params SqliteParameter[] args)
53 | {
54 | return ExecuteReaderMySql(sql, ToMysqlParameters(args));
55 | }
56 |
57 | }
58 |
--------------------------------------------------------------------------------
/Database_MySql.cs:
--------------------------------------------------------------------------------
1 |
2 | using UnityEngine;
3 | using Mirror;
4 | using System;
5 | using System.IO;
6 | using System.Linq;
7 | using System.Collections.Generic;
8 | using System.Data;
9 | using MySql.Data; // From MySql.Data.dll in Plugins folder
10 | using MySql.Data.MySqlClient; // From MySql.Data.dll in Plugins folder
11 |
12 |
13 | using SqlParameter = MySql.Data.MySqlClient.MySqlParameter;
14 |
15 |
16 | ///
17 | /// Database class for mysql
18 | /// Port of the sqlite database class from ummorpg
19 | ///
20 | public partial class Database
21 | {
22 |
23 | private static string connectionString = null;
24 |
25 | ///
26 | /// produces the connection string based on environment variables
27 | ///
28 | /// The connection string
29 | private static string ConnectionString
30 | {
31 | get
32 | {
33 |
34 | if (connectionString == null)
35 | {
36 | var connectionStringBuilder = new MySqlConnectionStringBuilder
37 | {
38 | Server = GetEnv("MYSQL_HOST") ?? "localhost",
39 | Database = GetEnv("MYSQL_DATABASE") ?? "ummorpg",
40 | UserID = GetEnv("MYSQL_USER") ?? "ummorpg",
41 | Password = GetEnv("MYSQL_PASSWORD") ?? "",
42 | Port = GetUIntEnv("MYSQL_PORT", 3306),
43 | CharacterSet = "utf8",
44 | OldGuids=true
45 | };
46 | connectionString = connectionStringBuilder.ConnectionString;
47 | }
48 |
49 | return connectionString;
50 | }
51 | }
52 |
53 | private static void Transaction(Action action)
54 | {
55 | using (var connection = new MySqlConnection(ConnectionString))
56 | {
57 |
58 | connection.Open();
59 | MySqlTransaction transaction = null;
60 |
61 | try
62 | {
63 |
64 | transaction = connection.BeginTransaction();
65 |
66 | MySqlCommand command = new MySqlCommand();
67 | command.Connection = connection;
68 | command.Transaction = transaction;
69 |
70 | action(command);
71 |
72 | transaction.Commit();
73 |
74 | }
75 | catch (Exception ex)
76 | {
77 | if (transaction != null)
78 | transaction.Rollback();
79 | throw ex;
80 | }
81 | }
82 | }
83 |
84 | private static String GetEnv(String name)
85 | {
86 | return Environment.GetEnvironmentVariable(name);
87 |
88 | }
89 |
90 | private static uint GetUIntEnv(String name, uint defaultValue = 0)
91 | {
92 | var value = Environment.GetEnvironmentVariable(name);
93 |
94 | if (value == null)
95 | return defaultValue;
96 |
97 | uint result;
98 |
99 | if (uint.TryParse(value, out result))
100 | return result;
101 |
102 | return defaultValue;
103 | }
104 |
105 | private static void InitializeSchema()
106 | {
107 | ExecuteNonQueryMySql(@"
108 | CREATE TABLE IF NOT EXISTS guild_info(
109 | name VARCHAR(16) NOT NULL,
110 | notice TEXT NOT NULL,
111 | PRIMARY KEY(name)
112 | ) CHARACTER SET=utf8mb4");
113 |
114 |
115 | ExecuteNonQueryMySql(@"
116 | CREATE TABLE IF NOT EXISTS accounts (
117 | name VARCHAR(16) NOT NULL,
118 | password CHAR(40) NOT NULL,
119 | banned BOOLEAN NOT NULL DEFAULT 0,
120 | PRIMARY KEY(name)
121 | ) CHARACTER SET=utf8mb4");
122 |
123 | ExecuteNonQueryMySql(@"
124 | CREATE TABLE IF NOT EXISTS characters(
125 | name VARCHAR(16) NOT NULL,
126 | account VARCHAR(16) NOT NULL,
127 |
128 | class VARCHAR(16) NOT NULL,
129 | x FLOAT NOT NULL,
130 | y FLOAT NOT NULL,
131 | z FLOAT NOT NULL,
132 | level INT NOT NULL DEFAULT 1,
133 | health INT NOT NULL,
134 | mana INT NOT NULL,
135 | strength INT NOT NULL DEFAULT 0,
136 | intelligence INT NOT NULL DEFAULT 0,
137 | experience BIGINT NOT NULL DEFAULT 0,
138 | skillExperience BIGINT NOT NULL DEFAULT 0,
139 | gold BIGINT NOT NULL DEFAULT 0,
140 | coins BIGINT NOT NULL DEFAULT 0,
141 | online TIMESTAMP,
142 |
143 | deleted BOOLEAN NOT NULL,
144 |
145 | guild VARCHAR(16),
146 | `rank` INT,
147 |
148 | PRIMARY KEY (name),
149 | INDEX(account),
150 | INDEX(guild),
151 | FOREIGN KEY(account)
152 | REFERENCES accounts(name)
153 | ON DELETE CASCADE ON UPDATE CASCADE,
154 | FOREIGN KEY(guild)
155 | REFERENCES guild_info(name)
156 | ON DELETE SET NULL ON UPDATE CASCADE
157 | ) CHARACTER SET=utf8mb4");
158 |
159 |
160 | ExecuteNonQueryMySql(@"
161 | CREATE TABLE IF NOT EXISTS character_inventory(
162 | `character` VARCHAR(16) NOT NULL,
163 | slot INT NOT NULL,
164 | name VARCHAR(50) NOT NULL,
165 | amount INT NOT NULL,
166 | summonedHealth INT NOT NULL,
167 | summonedLevel INT NOT NULL,
168 | summonedExperience BIGINT NOT NULL,
169 |
170 | primary key(`character`, slot),
171 | FOREIGN KEY(`character`)
172 | REFERENCES characters(name)
173 | ON DELETE CASCADE ON UPDATE CASCADE
174 | ) CHARACTER SET=utf8mb4");
175 |
176 | ExecuteNonQueryMySql(@"
177 | CREATE TABLE IF NOT EXISTS character_equipment(
178 | `character` VARCHAR(16) NOT NULL,
179 | slot INT NOT NULL,
180 | name VARCHAR(50) NOT NULL,
181 | amount INT NOT NULL,
182 |
183 | primary key(`character`, slot),
184 | FOREIGN KEY(`character`)
185 | REFERENCES characters(name)
186 | ON DELETE CASCADE ON UPDATE CASCADE
187 | ) CHARACTER SET=utf8mb4");
188 |
189 | ExecuteNonQueryMySql(@"
190 | CREATE TABLE IF NOT EXISTS character_skills(
191 | `character` VARCHAR(16) NOT NULL,
192 | name VARCHAR(50) NOT NULL,
193 | level INT NOT NULL,
194 | castTimeEnd FLOAT NOT NULL,
195 | cooldownEnd FLOAT NOT NULL,
196 |
197 | PRIMARY KEY (`character`, name),
198 | FOREIGN KEY(`character`)
199 | REFERENCES characters(name)
200 | ON DELETE CASCADE ON UPDATE CASCADE
201 | ) CHARACTER SET=utf8mb4");
202 |
203 |
204 | ExecuteNonQueryMySql(@"
205 | CREATE TABLE IF NOT EXISTS character_buffs (
206 | `character` VARCHAR(16) NOT NULL,
207 | name VARCHAR(50) NOT NULL,
208 | level INT NOT NULL,
209 | buffTimeEnd FLOAT NOT NULL,
210 |
211 | PRIMARY KEY (`character`, name),
212 | FOREIGN KEY(`character`)
213 | REFERENCES characters(name)
214 | ON DELETE CASCADE ON UPDATE CASCADE
215 | ) CHARACTER SET=utf8mb4");
216 |
217 |
218 | ExecuteNonQueryMySql(@"
219 | CREATE TABLE IF NOT EXISTS character_quests(
220 | `character` VARCHAR(16) NOT NULL,
221 | name VARCHAR(50) NOT NULL,
222 | field0 INT NOT NULL,
223 | completed BOOLEAN NOT NULL,
224 |
225 | PRIMARY KEY(`character`, name),
226 | FOREIGN KEY(`character`)
227 | REFERENCES characters(name)
228 | ON DELETE CASCADE ON UPDATE CASCADE
229 | ) CHARACTER SET=utf8mb4");
230 |
231 |
232 | ExecuteNonQueryMySql(@"
233 | CREATE TABLE IF NOT EXISTS character_orders(
234 | orderid BIGINT NOT NULL AUTO_INCREMENT,
235 | `character` VARCHAR(16) NOT NULL,
236 | coins BIGINT NOT NULL,
237 | processed BIGINT NOT NULL,
238 |
239 | PRIMARY KEY(orderid),
240 | INDEX(`character`),
241 | FOREIGN KEY(`character`)
242 | REFERENCES characters(name)
243 | ON DELETE CASCADE ON UPDATE CASCADE
244 | ) CHARACTER SET=utf8mb4");
245 | }
246 |
247 | static Database()
248 | {
249 | Debug.Log("Initializing MySQL database");
250 |
251 | InitializeSchema();
252 |
253 | Utils.InvokeMany(typeof(Database), null, "Initialize_");
254 | }
255 |
256 | #region Helper Functions
257 |
258 | // run a query that doesn't return anything
259 | private static void ExecuteNonQueryMySql(string sql, params SqlParameter[] args)
260 | {
261 | try
262 | {
263 | MySqlHelper.ExecuteNonQuery(ConnectionString, sql, args);
264 | }
265 | catch (Exception ex)
266 | {
267 | Debug.LogErrorFormat("Failed to execute query {0}", sql);
268 | throw ex;
269 | }
270 |
271 | }
272 |
273 |
274 | private static void ExecuteNonQueryMySql(MySqlCommand command, string sql, params SqlParameter[] args)
275 | {
276 | try
277 | {
278 | command.CommandText = sql;
279 | command.Parameters.Clear();
280 |
281 | foreach (var arg in args)
282 | {
283 | command.Parameters.Add(arg);
284 | }
285 |
286 | command.ExecuteNonQuery();
287 | }
288 | catch (Exception ex)
289 | {
290 | Debug.LogErrorFormat("Failed to execute query {0}", sql);
291 | throw ex;
292 | }
293 |
294 | }
295 |
296 | // run a query that returns a single value
297 | private static object ExecuteScalarMySql(string sql, params SqlParameter[] args)
298 | {
299 | try
300 | {
301 | return MySqlHelper.ExecuteScalar(ConnectionString, sql, args);
302 | }
303 | catch (Exception ex)
304 | {
305 | Debug.LogErrorFormat("Failed to execute query {0}", sql);
306 | throw ex;
307 | }
308 | }
309 |
310 | private static DataRow ExecuteDataRowMySql(string sql, params SqlParameter[] args)
311 | {
312 | try
313 | {
314 | return MySqlHelper.ExecuteDataRow(ConnectionString, sql, args);
315 | }
316 | catch (Exception ex)
317 | {
318 | Debug.LogErrorFormat("Failed to execute query {0}", sql);
319 | throw ex;
320 | }
321 | }
322 |
323 | private static DataSet ExecuteDataSetMySql(string sql, params SqlParameter[] args)
324 | {
325 | try
326 | {
327 | return MySqlHelper.ExecuteDataset(ConnectionString, sql, args);
328 | }
329 | catch (Exception ex)
330 | {
331 | Debug.LogErrorFormat("Failed to execute query {0}", sql);
332 | throw ex;
333 | }
334 | }
335 |
336 | // run a query that returns several values
337 | private static List> ExecuteReaderMySql(string sql, params SqlParameter[] args)
338 | {
339 | try
340 | {
341 | var result = new List>();
342 |
343 | using (var reader = MySqlHelper.ExecuteReader(ConnectionString, sql, args))
344 | {
345 |
346 | while (reader.Read())
347 | {
348 | var buf = new object[reader.FieldCount];
349 | reader.GetValues(buf);
350 | result.Add(buf.ToList());
351 | }
352 | }
353 |
354 | return result;
355 | }
356 | catch (Exception ex)
357 | {
358 | Debug.LogErrorFormat("Failed to execute query {0}", sql);
359 | throw ex;
360 | }
361 |
362 | }
363 |
364 | // run a query that returns several values
365 | private static MySqlDataReader GetReader(string sql, params SqlParameter[] args)
366 | {
367 | try
368 | {
369 | return MySqlHelper.ExecuteReader(ConnectionString, sql, args);
370 | }
371 | catch (Exception ex)
372 | {
373 | Debug.LogErrorFormat("Failed to execute query {0}", sql);
374 | throw ex;
375 | }
376 | }
377 |
378 | #endregion
379 |
380 |
381 | // account data ////////////////////////////////////////////////////////////
382 | public static bool IsValidAccount(string account, string password)
383 | {
384 | // this function can be used to verify account credentials in a database
385 | // or a content management system.
386 | //
387 | // for example, we could setup a content management system with a forum,
388 | // news, shop etc. and then use a simple HTTP-GET to check the account
389 | // info, for example:
390 | //
391 | // var request = new WWW("example.com/verify.php?id="+id+"&pw="+pw);
392 | // while (!request.isDone)
393 | // print("loading...");
394 | // return request.error == null && request.text == "ok";
395 | //
396 | // where verify.php is a script like this one:
397 | //
407 | //
408 | // or we could check in a MYSQL database:
409 | // var dbConn = new MySql.Data.MySqlClient.MySqlConnection("Persist Security Info=False;server=localhost;database=notas;uid=root;password=" + dbpwd);
410 | // var cmd = dbConn.CreateCommand();
411 | // cmd.CommandText = "SELECT id FROM accounts WHERE id='" + account + "' AND pw='" + password + "'";
412 | // dbConn.Open();
413 | // var reader = cmd.ExecuteReader();
414 | // if (reader.Read())
415 | // return reader.ToString() == account;
416 | // return false;
417 | //
418 | // as usual, we will use the simplest solution possible:
419 | // create account if not exists, compare password otherwise.
420 | // no CMS communication necessary and good enough for an Indie MMORPG.
421 |
422 | // not empty?
423 | if (!Utils.IsNullOrWhiteSpace(account) && !Utils.IsNullOrWhiteSpace(password))
424 | {
425 |
426 | var row = ExecuteDataRowMySql("SELECT password, banned FROM accounts WHERE name=@name", new SqlParameter("@name", account));
427 | if (row != null)
428 | {
429 | return password == (string)row["password"] && !(bool)row["banned"];
430 | }
431 | else
432 | {
433 | // account doesn't exist. create it.
434 | ExecuteNonQueryMySql("INSERT INTO accounts VALUES (@name, @password, 0)", new SqlParameter("@name", account), new SqlParameter("@password", password));
435 | return true;
436 | }
437 | }
438 | return false;
439 | }
440 |
441 | // character data //////////////////////////////////////////////////////////
442 | public static bool CharacterExists(string characterName)
443 | {
444 | // checks deleted ones too so we don't end up with duplicates if we un-
445 | // delete one
446 | return ((long)ExecuteScalarMySql("SELECT Count(*) FROM characters WHERE name=@name", new SqlParameter("@name", characterName))) == 1;
447 | }
448 |
449 | public static void CharacterDelete(string characterName)
450 | {
451 | // soft delete the character so it can always be restored later
452 | ExecuteNonQueryMySql("UPDATE characters SET deleted=1 WHERE name=@character", new SqlParameter("@character", characterName));
453 | }
454 |
455 | // returns a dict of
456 | // we really need the prefab name too, so that client character selection
457 | // can read all kinds of properties like icons, stats, 3D models and not
458 | // just the character name
459 | public static List CharactersForAccount(string account)
460 | {
461 | var result = new List();
462 |
463 | var table = ExecuteReaderMySql("SELECT name FROM characters WHERE account=@account AND deleted=0", new SqlParameter("@account", account));
464 | foreach (var row in table)
465 | result.Add((string)row[0]);
466 | return result;
467 | }
468 |
469 | private static void LoadInventory(Player player)
470 | {
471 | // fill all slots first
472 | for (int i = 0; i < player.inventorySize; ++i)
473 | player.inventory.Add(new ItemSlot());
474 |
475 | // override with the inventory stored in database
476 | using (var reader = GetReader(@"SELECT * FROM character_inventory WHERE `character`=@character;",
477 | new SqlParameter("@character", player.name)))
478 | {
479 |
480 | while (reader.Read())
481 | {
482 | string itemName = (string)reader["name"];
483 | int slot = (int)reader["slot"];
484 |
485 | ScriptableItem itemData;
486 | if (slot < player.inventorySize && ScriptableItem.dict.TryGetValue(itemName.GetStableHashCode(), out itemData))
487 | {
488 | Item item = new Item(itemData);
489 | int amount = (int)reader["amount"];
490 | item.summonedHealth = (int)reader["summonedHealth"];
491 | item.summonedLevel = (int)reader["summonedLevel"];
492 | item.summonedExperience = (long)reader["summonedExperience"];
493 | player.inventory[slot] = new ItemSlot(item, amount); ;
494 | }
495 | }
496 | }
497 | }
498 |
499 | private static void LoadEquipment(Player player)
500 | {
501 | // fill all slots first
502 | for (int i = 0; i < player.equipmentInfo.Length; ++i)
503 | player.equipment.Add(new ItemSlot());
504 |
505 | using (var reader = GetReader(@"SELECT * FROM character_equipment WHERE `character`=@character;",
506 | new SqlParameter("@character", player.name)))
507 | {
508 |
509 | while (reader.Read())
510 | {
511 | string itemName = (string)reader["name"];
512 | int slot = (int)reader["slot"];
513 |
514 | ScriptableItem itemData;
515 | if (slot < player.equipmentInfo.Length && ScriptableItem.dict.TryGetValue(itemName.GetStableHashCode(), out itemData))
516 | {
517 | Item item = new Item(itemData);
518 | int amount = (int)reader["amount"];
519 | player.equipment[slot] = new ItemSlot(item, amount);
520 | }
521 | }
522 | }
523 | }
524 |
525 | private static void LoadSkills(Player player)
526 | {
527 | // load skills based on skill templates (the others don't matter)
528 | // -> this way any template changes in a prefab will be applied
529 | // to all existing players every time (unlike item templates
530 | // which are only for newly created characters)
531 |
532 | // fill all slots first
533 | foreach (ScriptableSkill skillData in player.skillTemplates)
534 | player.skills.Add(new Skill(skillData));
535 |
536 | using (var reader = GetReader(
537 | "SELECT name, level, castTimeEnd, cooldownEnd FROM character_skills WHERE `character`=@character ",
538 | new SqlParameter("@character", player.name)))
539 | {
540 |
541 | while (reader.Read())
542 | {
543 |
544 | var skillName = (string)reader["name"];
545 |
546 | int index = player.skills.FindIndex(skill => skill.name == skillName);
547 | if (index != -1)
548 | {
549 | Skill skill = player.skills[index];
550 | // make sure that 1 <= level <= maxlevel (in case we removed a skill
551 | // level etc)
552 | skill.level = Mathf.Clamp((int)reader["level"], 1, skill.maxLevel);
553 | // make sure that 1 <= level <= maxlevel (in case we removed a skill
554 | // level etc)
555 | // castTimeEnd and cooldownEnd are based on Time.time, which
556 | // will be different when restarting a server, hence why we
557 | // saved them as just the remaining times. so let's convert them
558 | // back again.
559 | skill.castTimeEnd = (float)reader["castTimeEnd"] + Time.time;
560 | skill.cooldownEnd = (float)reader["cooldownEnd"] + Time.time;
561 |
562 | player.skills[index] = skill;
563 | }
564 | }
565 | }
566 | }
567 |
568 | private static void LoadBuffs(Player player)
569 | {
570 |
571 | using (var reader = GetReader(
572 | "SELECT name, level, buffTimeEnd FROM character_buffs WHERE `character` = @character ",
573 | new SqlParameter("@character", player.name)))
574 | {
575 | while (reader.Read())
576 | {
577 | string buffName = (string)reader["name"];
578 | ScriptableSkill skillData;
579 | if (ScriptableSkill.dict.TryGetValue(buffName.GetStableHashCode(), out skillData))
580 | {
581 | // make sure that 1 <= level <= maxlevel (in case we removed a skill
582 | // level etc)
583 | int level = Mathf.Clamp((int)reader["level"], 1, skillData.maxLevel);
584 | Buff buff = new Buff((BuffSkill)skillData, level);
585 | // buffTimeEnd is based on Time.time, which will be
586 | // different when restarting a server, hence why we saved
587 | // them as just the remaining times. so let's convert them
588 | // back again.
589 | buff.buffTimeEnd = (float)reader["buffTimeEnd"] + Time.time;
590 | player.buffs.Add(buff);
591 | }
592 | }
593 |
594 | }
595 | }
596 |
597 | private static void LoadQuests(Player player)
598 | {
599 | // load quests
600 |
601 | using (var reader = GetReader("SELECT name, field0, completed FROM character_quests WHERE `character`=@character",
602 | new SqlParameter("@character", player.name)))
603 | {
604 |
605 | while (reader.Read())
606 | {
607 | string questName = (string)reader["name"];
608 | ScriptableQuest questData;
609 | if (ScriptableQuest.dict.TryGetValue(questName.GetStableHashCode(), out questData))
610 | {
611 | Quest quest = new Quest(questData);
612 | quest.field0 = (int)reader["field0"];
613 | quest.completed = (bool)reader["completed"];
614 | player.quests.Add(quest);
615 | }
616 | }
617 | }
618 | }
619 |
620 | private static void LoadGuild(Player player)
621 | {
622 | // in a guild?
623 | if (player.guildName != "")
624 | {
625 | // load guild info
626 | var row = ExecuteDataRowMySql("SELECT notice FROM guild_info WHERE name=@guild", new SqlParameter("@guild", player.guildName));
627 | if (row != null)
628 | {
629 | player.guild.notice = (string)row["notice"];
630 | }
631 |
632 | // load members list
633 | var members = new List();
634 |
635 | using (var reader = GetReader(
636 | "SELECT name, level, `rank` FROM characters WHERE guild=@guild AND deleted=0",
637 | new SqlParameter("@guild", player.guildName)))
638 | {
639 |
640 | while (reader.Read())
641 | {
642 | var member = new GuildMember();
643 | member.name = (string)reader["name"];
644 | member.rank = (GuildRank)((int)reader["rank"]);
645 | member.online = Player.onlinePlayers.ContainsKey(member.name);
646 | member.level = (int)reader["level"];
647 |
648 | members.Add(member);
649 | };
650 | }
651 | player.guild.members = members.ToArray(); // guild.AddMember each time is too slow because array resizing
652 | }
653 | }
654 |
655 | public static GameObject CharacterLoad(string characterName, List prefabs)
656 | {
657 | var row = ExecuteDataRowMySql("SELECT * FROM characters WHERE name=@name AND deleted=0", new SqlParameter("@name", characterName));
658 | if (row != null)
659 | {
660 | // instantiate based on the class name
661 | string className = (string)row["class"];
662 | var prefab = prefabs.Find(p => p.name == className);
663 | if (prefab != null)
664 | {
665 | var go = GameObject.Instantiate(prefab.gameObject);
666 | var player = go.GetComponent();
667 |
668 | player.name = (string)row["name"];
669 | player.account = (string)row["account"];
670 | player.className = (string)row["class"];
671 | float x = (float)row["x"];
672 | float y = (float)row["y"];
673 | float z = (float)row["z"];
674 | Vector3 position = new Vector3(x, y, z);
675 | player.level = (int)row["level"];
676 | int health = (int)row["health"];
677 | int mana = (int)row["mana"];
678 | player.strength = (int)row["strength"];
679 | player.intelligence = (int)row["intelligence"];
680 | player.experience = (long)row["experience"];
681 | player.skillExperience = (long)row["skillExperience"];
682 | player.gold = (long)row["gold"];
683 | player.coins = (long)row["coins"];
684 |
685 | if (row.IsNull("guild"))
686 | player.guildName = "";
687 | else
688 | player.guildName = (string)row["guild"];
689 |
690 | // try to warp to loaded position.
691 | // => agent.warp is recommended over transform.position and
692 | // avoids all kinds of weird bugs
693 | // => warping might fail if we changed the world since last save
694 | // so we reset to start position if not on navmesh
695 | player.agent.Warp(position);
696 | if (!player.agent.isOnNavMesh)
697 | {
698 | Transform start = NetworkManager.singleton.GetNearestStartPosition(position);
699 | player.agent.Warp(start.position);
700 | Debug.Log(player.name + " invalid position was reset");
701 | }
702 |
703 | LoadInventory(player);
704 | LoadEquipment(player);
705 | LoadSkills(player);
706 | LoadBuffs(player);
707 | LoadQuests(player);
708 | LoadGuild(player);
709 |
710 | // assign health / mana after max values were fully loaded
711 | // (they depend on equipment, buffs, etc.)
712 | player.health = health;
713 | player.mana = mana;
714 |
715 | // addon system hooks
716 | Utils.InvokeMany(typeof(Database), null, "CharacterLoad_", player);
717 |
718 | return go;
719 | }
720 | else Debug.LogError("no prefab found for class: " + className);
721 | }
722 | return null;
723 | }
724 |
725 | static void SaveInventory(Player player, MySqlCommand command)
726 | {
727 | // inventory: remove old entries first, then add all new ones
728 | // (we could use UPDATE where slot=... but deleting everything makes
729 | // sure that there are never any ghosts)
730 | ExecuteNonQueryMySql(command, "DELETE FROM character_inventory WHERE `character`=@character", new SqlParameter("@character", player.name));
731 | for (int i = 0; i < player.inventory.Count; ++i)
732 | {
733 | ItemSlot slot = player.inventory[i];
734 | if (slot.amount > 0) // only relevant items to save queries/storage/time
735 | ExecuteNonQueryMySql(command, "INSERT INTO character_inventory VALUES (@character, @slot, @name, @amount, @summonedHealth, @summonedLevel, @summonedExperience)",
736 | new SqlParameter("@character", player.name),
737 | new SqlParameter("@slot", i),
738 | new SqlParameter("@name", slot.item.name),
739 | new SqlParameter("@amount", slot.amount),
740 | new SqlParameter("@summonedHealth", slot.item.summonedHealth),
741 | new SqlParameter("@summonedLevel", slot.item.summonedLevel),
742 | new SqlParameter("@summonedExperience", slot.item.summonedExperience));
743 | }
744 | }
745 |
746 | static void SaveEquipment(Player player, MySqlCommand command)
747 | {
748 | // equipment: remove old entries first, then add all new ones
749 | // (we could use UPDATE where slot=... but deleting everything makes
750 | // sure that there are never any ghosts)
751 | ExecuteNonQueryMySql(command, "DELETE FROM character_equipment WHERE `character`=@character", new SqlParameter("@character", player.name));
752 | for (int i = 0; i < player.equipment.Count; ++i)
753 | {
754 | ItemSlot slot = player.equipment[i];
755 | if (slot.amount > 0) // only relevant equip to save queries/storage/time
756 | ExecuteNonQueryMySql(command, "INSERT INTO character_equipment VALUES (@character, @slot, @name, @amount)",
757 | new SqlParameter("@character", player.name),
758 | new SqlParameter("@slot", i),
759 | new SqlParameter("@name", slot.item.name),
760 | new SqlParameter("@amount", slot.amount));
761 | }
762 | }
763 |
764 | static void SaveSkills(Player player, MySqlCommand command)
765 | {
766 | // skills: remove old entries first, then add all new ones
767 | ExecuteNonQueryMySql(command, "DELETE FROM character_skills WHERE `character`=@character", new SqlParameter("@character", player.name));
768 | foreach (var skill in player.skills)
769 | {
770 | // only save relevant skills to save a lot of queries and storage
771 | // (considering thousands of players)
772 | // => interesting only if learned or if buff/status (murderer etc.)
773 | if (skill.level > 0) // only relevant skills to save queries/storage/time
774 | {
775 | // castTimeEnd and cooldownEnd are based on Time.time, which
776 | // will be different when restarting the server, so let's
777 | // convert them to the remaining time for easier save & load
778 | // note: this does NOT work when trying to save character data shortly
779 | // before closing the editor or game because Time.time is 0 then.
780 | ExecuteNonQueryMySql(command, @"
781 | INSERT INTO character_skills
782 | SET
783 | `character` = @character,
784 | name = @name,
785 | level = @level,
786 | castTimeEnd = @castTimeEnd,
787 | cooldownEnd = @cooldownEnd",
788 | new SqlParameter("@character", player.name),
789 | new SqlParameter("@name", skill.name),
790 | new SqlParameter("@level", skill.level),
791 | new SqlParameter("@castTimeEnd", skill.CastTimeRemaining()),
792 | new SqlParameter("@cooldownEnd", skill.CooldownRemaining()));
793 | }
794 | }
795 | }
796 |
797 | static void SaveBuffs(Player player, MySqlCommand command)
798 | {
799 | ExecuteNonQueryMySql(command, "DELETE FROM character_buffs WHERE `character`=@character", new SqlParameter("@character", player.name));
800 | foreach (var buff in player.buffs)
801 | {
802 | // buffTimeEnd is based on Time.time, which will be different when
803 | // restarting the server, so let's convert them to the remaining
804 | // time for easier save & load
805 | // note: this does NOT work when trying to save character data shortly
806 | // before closing the editor or game because Time.time is 0 then.
807 | ExecuteNonQueryMySql(command, "INSERT INTO character_buffs VALUES (@character, @name, @level, @buffTimeEnd)",
808 | new SqlParameter("@character", player.name),
809 | new SqlParameter("@name", buff.name),
810 | new SqlParameter("@level", buff.level),
811 | new SqlParameter("@buffTimeEnd", (float)buff.BuffTimeRemaining()));
812 | }
813 | }
814 |
815 | static void SaveQuests(Player player, MySqlCommand command)
816 | {
817 | // quests: remove old entries first, then add all new ones
818 | ExecuteNonQueryMySql(command, "DELETE FROM character_quests WHERE `character`=@character", new SqlParameter("@character", player.name));
819 | foreach (var quest in player.quests)
820 | {
821 | ExecuteNonQueryMySql(command, "INSERT INTO character_quests VALUES (@character, @name, @field0, @completed)",
822 | new SqlParameter("@character", player.name),
823 | new SqlParameter("@name", quest.name),
824 | new SqlParameter("@field0", quest.field0),
825 | new SqlParameter("@completed", quest.completed));
826 | }
827 | }
828 |
829 | // adds or overwrites character data in the database
830 | static void CharacterSave(Player player, bool online, MySqlCommand command)
831 | {
832 | // online status:
833 | // '' if offline (if just logging out etc.)
834 | // current time otherwise
835 | // -> this way it's fault tolerant because external applications can
836 | // check if online != '' and if time difference < saveinterval
837 | // -> online time is useful for network zones (server<->server online
838 | // checks), external websites which render dynamic maps, etc.
839 | // -> it uses the ISO 8601 standard format
840 | DateTime? onlineTimestamp = null;
841 |
842 | if (!online)
843 | onlineTimestamp = DateTime.Now;
844 |
845 | var query = @"
846 | INSERT INTO characters
847 | SET
848 | name=@name,
849 | account=@account,
850 | class = @class,
851 | x = @x,
852 | y = @y,
853 | z = @z,
854 | level = @level,
855 | health = @health,
856 | mana = @mana,
857 | strength = @strength,
858 | intelligence = @intelligence,
859 | experience = @experience,
860 | skillExperience = @skillExperience,
861 | gold = @gold,
862 | coins = @coins,
863 | online = @online,
864 | deleted = 0,
865 | guild = @guild
866 | ON DUPLICATE KEY UPDATE
867 | account=@account,
868 | class = @class,
869 | x = @x,
870 | y = @y,
871 | z = @z,
872 | level = @level,
873 | health = @health,
874 | mana = @mana,
875 | strength = @strength,
876 | intelligence = @intelligence,
877 | experience = @experience,
878 | skillExperience = @skillExperience,
879 | gold = @gold,
880 | coins = @coins,
881 | online = @online,
882 | deleted = 0,
883 | guild = @guild
884 | ";
885 |
886 | ExecuteNonQueryMySql(command, query,
887 | new SqlParameter("@name", player.name),
888 | new SqlParameter("@account", player.account),
889 | new SqlParameter("@class", player.className),
890 | new SqlParameter("@x", player.transform.position.x),
891 | new SqlParameter("@y", player.transform.position.y),
892 | new SqlParameter("@z", player.transform.position.z),
893 | new SqlParameter("@level", player.level),
894 | new SqlParameter("@health", player.health),
895 | new SqlParameter("@mana", player.mana),
896 | new SqlParameter("@strength", player.strength),
897 | new SqlParameter("@intelligence", player.intelligence),
898 | new SqlParameter("@experience", player.experience),
899 | new SqlParameter("@skillExperience", player.skillExperience),
900 | new SqlParameter("@gold", player.gold),
901 | new SqlParameter("@coins", player.coins),
902 | new SqlParameter("@online", onlineTimestamp),
903 | new SqlParameter("@guild", player.guildName == "" ? null : player.guildName)
904 | );
905 |
906 | SaveInventory(player, command);
907 | SaveEquipment(player, command);
908 | SaveSkills(player, command);
909 | SaveBuffs(player, command);
910 | SaveQuests(player, command);
911 |
912 | // addon system hooks
913 | Utils.InvokeMany(typeof(Database), null, "CharacterSave_", player);
914 | }
915 |
916 | // adds or overwrites character data in the database
917 | public static void CharacterSave(Player player, bool online, bool useTransaction = true)
918 | {
919 | // only use a transaction if not called within SaveMany transaction
920 | Transaction(command =>
921 | {
922 | CharacterSave(player, online, command);
923 | });
924 | }
925 |
926 |
927 |
928 | // save multiple characters at once (useful for ultra fast transactions)
929 | public static void CharacterSaveMany(List players, bool online = true)
930 | {
931 | Transaction(command =>
932 | {
933 | foreach (var player in players)
934 | CharacterSave(player, online, command);
935 | });
936 | }
937 |
938 | // guilds //////////////////////////////////////////////////////////////////
939 | public static void SaveGuild(string guild, string notice, List members)
940 | {
941 | Transaction(command =>
942 | {
943 | var query = @"
944 | INSERT INTO guild_info
945 | SET
946 | name = @guild,
947 | notice = @notice
948 | ON DUPLICATE KEY UPDATE
949 | notice = @notice";
950 |
951 | // guild info
952 | ExecuteNonQueryMySql(command, query,
953 | new SqlParameter("@guild", guild),
954 | new SqlParameter("@notice", notice));
955 |
956 | ExecuteNonQueryMySql(command, "UPDATE characters set guild = NULL where guild=@guild", new SqlParameter("@guild", guild));
957 |
958 |
959 | foreach (var member in members)
960 | {
961 |
962 | Debug.Log("Saving guild " + guild + " member " + member.name);
963 | ExecuteNonQueryMySql(command, "UPDATE characters set guild = @guild, `rank`=@rank where name=@character",
964 | new SqlParameter("@guild", guild),
965 | new SqlParameter("@character", member.name),
966 | new SqlParameter("@rank", member.rank));
967 | }
968 | });
969 | }
970 |
971 | public static bool GuildExists(string guild)
972 | {
973 | return ((long)ExecuteScalarMySql("SELECT Count(*) FROM guild_info WHERE name=@name", new SqlParameter("@name", guild))) == 1;
974 | }
975 |
976 | public static void RemoveGuild(string guild)
977 | {
978 | ExecuteNonQueryMySql("DELETE FROM guild_info WHERE name=@name", new SqlParameter("@name", guild));
979 | }
980 |
981 | // item mall ///////////////////////////////////////////////////////////////
982 | public static List GrabCharacterOrders(string characterName)
983 | {
984 | // grab new orders from the database and delete them immediately
985 | //
986 | // note: this requires an orderid if we want someone else to write to
987 | // the database too. otherwise deleting would delete all the new ones or
988 | // updating would update all the new ones. especially in sqlite.
989 | //
990 | // note: we could just delete processed orders, but keeping them in the
991 | // database is easier for debugging / support.
992 | var result = new List();
993 | var table = ExecuteReaderMySql("SELECT orderid, coins FROM character_orders WHERE `character`=@character AND processed=0", new SqlParameter("@character", characterName));
994 | foreach (var row in table)
995 | {
996 | result.Add((long)row[1]);
997 | ExecuteNonQueryMySql("UPDATE character_orders SET processed=1 WHERE orderid=@orderid", new SqlParameter("@orderid", (long)row[0]));
998 | }
999 | return result;
1000 | }
1001 | }
1002 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Paul Pacheco
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/MySql.Data.dll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/paulpach/ummorpg_mysql/513813f3dffbdcce0ec80ff1bc323f61c462ee91/MySql.Data.dll
--------------------------------------------------------------------------------
/Readme.md:
--------------------------------------------------------------------------------
1 | # UMMORPG Mysql Addon
2 |
3 | This is a Mysql Addon for uMMORPG 1.144 and [previous versions](https://github.com/paulpach/ummorpg_mysql/releases)
4 |
5 | This is pretty much a drop in replacement for the sqlite Database.cs that comes with uMMORPG.
6 |
7 | There are a few enhancement I made in this addon not present in the sqlite version:
8 |
9 | * Primary keys and indexes. This can greatly improve performance when you have lots of users
10 | * Native mysql types such as boolean and Datetime, no awkard conversions
11 | * Foreign keys, make it really easy to do maintenance on your database and ensure data integrity
12 | * utf8, character names in any language
13 | * Normalized the tables.
14 | * Optimize database access. Don't do so many round trips to the database for inventory, skills and equipment.
15 |
16 | # How to contribute
17 |
18 | Send me pull requests if you want to see some changes.
19 |
20 | Open issues if you find a bug
21 |
22 | Or buy me a beer in my [patreon page](https://www.patreon.com/user?u=13679599) if you want to provide brain fuel.
23 |
24 | # Installation instructions
25 |
26 | ## 1. Backup
27 | you have been warned
28 |
29 | ## 2. Install mysql
30 | I recommend mysql 8.0 or later [community edition](https://dev.mysql.com/downloads/).
31 |
32 | ## 3. Set character encoding (if MySQL version 7 or earlier)
33 | If using MySQL 7 or earlier, the default character set is `latin1`, which causes problems for the mysql driver.
34 | You need to change it to utf8mb4 or you will get exceptions
35 |
36 | edit my.cnf or my.ini and add these settings
37 | ```
38 | [mysqld]
39 | init_connect='SET collation_connection = utf8mb4_unicode_ci'
40 | init_connect='SET NAMES utf8mb4'
41 | character-set-server=utf8mb4
42 | collation-server=utf8mb4_unicode_ci
43 | ```
44 |
45 | ## 4. Set mysql native password (if MySQL 8 or later)
46 |
47 | By default, mysql 8 uses `caching_sha2_password` authentication method. We must use a .net 3.5 driver. It is too old and does not support this authentication method. You must change mysql to use `mysql_native_password` instead.
48 |
49 | Add this to your my.cnf or my.ini
50 | ```
51 | default-authentication-plugin=mysql_native_password
52 | ```
53 |
54 | ## 5. Restart mysql
55 |
56 | ## 6. Validate
57 |
58 | Log into mysql and type:
59 | ```
60 | show variables like "character_set_server";
61 | ```
62 |
63 | Make sure that the `character_set_server` is set to `utf8mb4`. If it didn't take the settings search for [all mysql configuration files](https://dev.mysql.com/doc/refman/8.0/en/option-files.html) in your system, one of them might be overriding your setting.
64 |
65 | ```
66 | show variables like "default_authentication_plugin";
67 | ```
68 |
69 | Make sure it says `mysql_native_password`
70 |
71 | ## 7. Create a database
72 | Create a user and database in mysql for your game. For example:
73 |
74 | ```
75 | create database ummorpg;
76 | create user 'ummorpg'@'%' identified by 'db_password';
77 | grant all on ummorpg.* to 'ummorpg'@'%';
78 | ```
79 |
80 | Make sure you can connect to your database from your server using the newly created account and database.
81 |
82 | ## 8. Set environment variables
83 |
84 | Now you must tell ummorpg how to get to that database. Out of the box you do that by setting environment variables before running unity or your server.
85 |
86 | For windows: [environment variables](https://www.youtube.com/watch?v=bEroNNzqlF4).
87 | For linux and mac, add them to your `~/.bash_profile`
88 |
89 | ~~~~
90 | MYSQL_HOST=localhost
91 | MYSQL_DATABASE=ummorpg
92 | MYSQL_USER=ummorpg
93 | MYSQL_PASSWORD=db_password
94 | MYSQL_PORT=3306
95 | ~~~~
96 |
97 | Adjust the settings according to your set up
98 |
99 | If you don’t want to use environment variables, change the method `ConnectionString` near the top in `Database_MySql.cs`. I use environment variables because I deploy my server in docker containers.
100 |
101 | ## 9. Run Unity and open your project
102 |
103 | ## 10. Delete Database.cs that comes with uMMORPG
104 |
105 | ## 11. Add the addon
106 |
107 | Download all files from this repository and add them to your project. Put them wherever you want.
108 |
109 | You don't need the [Addons](Addons) folder if you don't have NetworkZones.
110 |
111 | ## 12. Set up NetworkZones (optional)
112 |
113 | follow [these instructions](Addons/NetworkZones/Readme.md).
114 |
115 | ## 13. Hit play and enjoy
116 |
117 | # Docker instructions
118 |
119 | As an option, you can run Mysql in a docker container.
120 |
121 | ## 1. Download and Install Docker
122 |
123 | Depending on the operating system you want to use follow these directions: https://docs.docker.com/install/
124 |
125 | Note: According to the MySQL Docker help page you cannot set the value 'MYSQL_HOST=localhost' as it casues issue. So far I have not had an issue leaving it out of the configuration.
126 |
127 | ## 2. Create MySQL container (if MySQL version 7 or earlier)
128 |
129 | ```
130 | docker run --name mysql \
131 | -p 3306:3306 \
132 | --restart always \
133 | -v /docker/mysql/datadir:/var/lib/mysql \
134 | -e MYSQL_ROOT_PASSWORD=CHANGEMEPLEASE \
135 | -e MYSQL_DATABASE=ummorpg \
136 | -e MYSQL_USER=ummorpg \
137 | -e MYSQL_PASSWORD=db_password \
138 | -d mysql:5.7.24 \
139 | --character-set-server=utf8mb4 \
140 | --collation-server=utf8mb4_unicode_ci
141 | ```
142 |
143 | ## 3. Create MySQL container (if MySQL 8 or later) THIS IS UNTESTED AT THIS TIME.
144 |
145 | ```
146 | docker run --name mysql \
147 | -p 3306:3306 \
148 | --restart always \
149 | -v /docker/mysql/datadir:/var/lib/mysql \
150 | -e MYSQL_ROOT_PASSWORD=CHANGEMEPLEASE \
151 | -e MYSQL_DATABASE=ummorpg \
152 | -e MYSQL_USER=ummorpg \
153 | -e MYSQL_PASSWORD=db_password \
154 | -d mysql:latest \
155 | --character-set-server=utf8mb4 \
156 | --collation-server=utf8mb4_unicode_ci \
157 | --default-authentication-plugin=mysql_native_password
158 | ```
159 |
160 | ## 4. For more information about MySQL in Docker please see this page: https://hub.docker.com/_/mysql/
161 |
162 | # Troubleshooting
163 | Many addons add their own tables and columns.
164 | They will need to be modified to work with mysql.
165 | That is out of my control, it is entirely up to you to update the addons.
166 | If you do adapt the addons, consider sending me a pull request so that other people can benefit.
167 |
168 | If you get `KeyNotFoundException: The given key was not present in the dicionary` it is likely that you are using the wrong character set. Go back to step 6 and make sure it is correctly configured.
169 |
170 | This is provided as is, no warranty, I am not responsible if it offers your first born child in sacrifice to the devil.
171 | I am simply offering it for free for anyone who might want it.
172 |
173 |
174 |
--------------------------------------------------------------------------------