├── 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 | --------------------------------------------------------------------------------