diff --git a/build.gradle b/build.gradle index dc662fc..3bd59d0 100644 --- a/build.gradle +++ b/build.gradle @@ -88,6 +88,9 @@ compile 'org.yaml:snakeyaml:1.13' compile 'com.lmax:disruptor:3.2.1' + compile 'mysql:mysql-connector-java:5.1.31' + compile 'commons-pool:commons-pool:1.6' + compile 'commons-dbcp:commons-dbcp:1.4' testCompile "org.codehaus.groovy:groovy-all:2.3.0" testCompile "org.spockframework:spock-core:1.0-groovy-2.0-SNAPSHOT" diff --git a/src/main/java/org/ultramine/server/UltramineServerConfig.java b/src/main/java/org/ultramine/server/UltramineServerConfig.java index ad25eca..bc63786 100644 --- a/src/main/java/org/ultramine/server/UltramineServerConfig.java +++ b/src/main/java/org/ultramine/server/UltramineServerConfig.java @@ -9,6 +9,8 @@ public Teleportation teleportation = new Teleportation(); public SpawnLocations spawnLocations = new SpawnLocations(); public VanillaConfig vanilla = new VanillaConfig(); + public Map databases; + public SQLServerStorage inSQLServerStorage = new SQLServerStorage(); public static class WatchdogThreadConfig { @@ -29,6 +31,21 @@ public boolean respawnOnBed = true; } + public static class Database + { + public String url; //jdbc:mysql://localhost:3306/databasename + public String username; + public String password; + public int maxConnections; + } + + public static class SQLServerStorage + { + public boolean enabled = false; + public String database = "global"; + public String tablePrefix = "mc_"; + } + public static class VanillaConfig diff --git a/src/main/java/org/ultramine/server/UltramineServerModContainer.java b/src/main/java/org/ultramine/server/UltramineServerModContainer.java index c98eeaf..78640d9 100644 --- a/src/main/java/org/ultramine/server/UltramineServerModContainer.java +++ b/src/main/java/org/ultramine/server/UltramineServerModContainer.java @@ -36,6 +36,7 @@ import org.ultramine.commands.syntax.DefaultCompleters; import org.ultramine.permission.commands.BasicPermissionCommands; import org.ultramine.permission.internal.OpPermissionProxySet; +import org.ultramine.server.data.Databases; import org.ultramine.server.data.ServerDataLoader; import org.ultramine.server.data.player.PlayerCoreData; @@ -67,7 +68,10 @@ public void preInit(FMLPreInitializationEvent e) { if(e.getSide().isServer()) + { ConfigurationHandler.load(); + Databases.init(); + } } @Subscribe diff --git a/src/main/java/org/ultramine/server/data/Databases.java b/src/main/java/org/ultramine/server/data/Databases.java new file mode 100644 index 0000000..0cc9271 --- /dev/null +++ b/src/main/java/org/ultramine/server/data/Databases.java @@ -0,0 +1,46 @@ +package org.ultramine.server.data; + +import java.util.HashMap; +import java.util.Map; + +import javax.sql.DataSource; + +import org.apache.commons.dbcp.BasicDataSource; +import org.ultramine.server.ConfigurationHandler; +import org.ultramine.server.UltramineServerConfig; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; + +@SideOnly(Side.SERVER) +public class Databases +{ + private static Map databases = new HashMap(); + + public static void init() + { + for(Map.Entry ent : ConfigurationHandler.getServerConfig().databases.entrySet()) + { + UltramineServerConfig.Database info = ent.getValue(); + + BasicDataSource ds = new BasicDataSource(); + if(info.url.startsWith("jdbc:mysql:")) + ds.setDriverClassName("com.mysql.jdbc.Driver"); + ds.setUrl(info.url); + ds.setUsername(info.username); + ds.setPassword(info.password); + + ds.setMaxActive(info.maxConnections); + + databases.put(ent.getKey(), ds); + } + } + + public static DataSource getDataSource(String name) + { + DataSource ds = databases.get(name); + if(ds == null) + throw new RuntimeException("DataSource for name: " + name + " not found! Check your server.yml"); + return ds; + } +} diff --git a/src/main/java/org/ultramine/server/data/IDataProvider.java b/src/main/java/org/ultramine/server/data/IDataProvider.java index 751c55e..6681d55 100644 --- a/src/main/java/org/ultramine/server/data/IDataProvider.java +++ b/src/main/java/org/ultramine/server/data/IDataProvider.java @@ -16,6 +16,8 @@ */ public interface IDataProvider { + void init(); + NBTTagCompound loadPlayer(GameProfile player); void savePlayer(GameProfile player, NBTTagCompound nbt); diff --git a/src/main/java/org/ultramine/server/data/JDBCDataProvider.java b/src/main/java/org/ultramine/server/data/JDBCDataProvider.java new file mode 100644 index 0000000..3d511f8 --- /dev/null +++ b/src/main/java/org/ultramine/server/data/JDBCDataProvider.java @@ -0,0 +1,588 @@ +package org.ultramine.server.data; + +import gnu.trove.TCollections; +import gnu.trove.map.TObjectIntMap; +import gnu.trove.map.hash.TObjectIntHashMap; + +import java.io.ByteArrayInputStream; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import javax.sql.DataSource; + +import net.minecraft.nbt.CompressedStreamTools; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.server.management.ServerConfigurationManager; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.ultramine.server.ConfigurationHandler; +import org.ultramine.server.data.player.PlayerData; +import org.ultramine.server.data.player.PlayerDataExtension; +import org.ultramine.server.data.player.PlayerDataExtensionInfo; +import org.ultramine.server.util.GlobalExecutors; +import org.ultramine.server.util.WarpLocation; + +import com.mojang.authlib.GameProfile; + +public class JDBCDataProvider implements IDataProvider +{ + private static final Logger log = LogManager.getLogger(); + + private final ServerConfigurationManager mgr; + + private final DataSource ds; + + private final String tab_player_ids; + private final String tab_player_gamedata; + private final String tab_player_data; + private final String tab_warps; + private final String tab_fastwarps; + + private final TObjectIntMap playerIDs = TCollections.synchronizedMap(new TObjectIntHashMap(128, 0.75F, -1)); + + public JDBCDataProvider(ServerConfigurationManager mgr) + { + this.mgr = mgr; + + String tablePrefix = ConfigurationHandler.getServerConfig().inSQLServerStorage.tablePrefix; + tab_player_ids = tablePrefix + "player_ids"; + tab_player_gamedata = tablePrefix + "player_gamedata"; + tab_player_data = tablePrefix + "player_data"; + tab_warps = tablePrefix + "warps"; + tab_fastwarps = tablePrefix + "warps_fast"; + + ds = Databases.getDataSource(ConfigurationHandler.getServerConfig().inSQLServerStorage.database); + } + + @Override + public void init() + { + Connection conn = null; + Statement s = null; + try + { + conn = ds.getConnection(); + s = conn.createStatement(); + + s.execute("CREATE TABLE IF NOT EXISTS `"+tab_player_ids+"` (" + + "`pid` int(11) unsigned NOT NULL AUTO_INCREMENT," + + "`uuid` binary(16) NOT NULL," + + "PRIMARY KEY (`pid`)," + + "UNIQUE KEY `uuid` (`uuid`)" + + ") ENGINE=MyISAM ROW_FORMAT=FIXED"); + + s.execute("CREATE TABLE IF NOT EXISTS `"+tab_player_gamedata+"` (" + + "`pid` int(11) unsigned NOT NULL AUTO_INCREMENT," + + "`data` blob NOT NULL," + + "PRIMARY KEY (`pid`)" + + ") ENGINE=InnoDB"); + + s.execute("CREATE TABLE IF NOT EXISTS `"+tab_player_data+"` (" + + "`pid` int(11) unsigned NOT NULL AUTO_INCREMENT," + + "`data` blob NOT NULL," + + "PRIMARY KEY (`pid`)" + + ") ENGINE=InnoDB"); + + s.execute("CREATE TABLE IF NOT EXISTS `"+tab_warps+"` (" + + "`id` int(10) unsigned NOT NULL AUTO_INCREMENT," + + "`name` varchar(32) NOT NULL," + + "`dimension` int(11) NOT NULL," + + "`x` double NOT NULL," + + "`y` double NOT NULL," + + "`z` double NOT NULL," + + "`yaw` float NOT NULL," + + "`pitch` float NOT NULL," + + "`random` double NOT NULL," + + "PRIMARY KEY (`id`)," + + "UNIQUE KEY `name` (`name`)" + + ") ENGINE=InnoDB"); + + s.execute("CREATE TABLE IF NOT EXISTS `"+tab_fastwarps+"` (" + + "`id` int(10) unsigned NOT NULL AUTO_INCREMENT," + + "`name` varchar(32) NOT NULL," + + "PRIMARY KEY (`id`)," + + "UNIQUE KEY `name` (`name`)" + + ") ENGINE=InnoDB"); + } + catch(Exception e) + { + throw new RuntimeException("Failed to create SQL tables", e); + } + finally + { + close(null, s, null); + } + + PreparedStatement ps = null; + ResultSet rs = null; + try + { + ps = conn.prepareStatement("SELECT * FROM `"+tab_player_ids+"`"); + rs = ps.executeQuery(); + while(rs.next()) + playerIDs.put(toUUID(rs.getBytes(2)), rs.getInt(1)); + } + catch(Exception e) + { + throw new RuntimeException("Failed to load initial player database IDs", e); + } + finally + { + close(conn, ps, rs); + } + } + + @Override + public NBTTagCompound loadPlayer(GameProfile player) + { + int id = playerIDs.get(player.getId()); + if(id == -1) + return null; + + Connection conn = null; + PreparedStatement ps = null; + ResultSet rs = null; + try + { + conn = ds.getConnection(); + ps = conn.prepareStatement("SELECT `data` FROM `"+tab_player_gamedata+"` WHERE `pid`=?"); + ps.setInt(1, id); + rs = ps.executeQuery(); + if(!rs.next()) + return null; + return CompressedStreamTools.readCompressed(new ByteArrayInputStream(rs.getBytes("data"))); + } + catch(Exception e) + { + log.warn("Failed to load player gamedata for " + player.getName(), e); + } + finally + { + close(conn, ps, rs); + } + + return null; + } + + @Override + public void savePlayer(final GameProfile player, final NBTTagCompound nbt) + { + GlobalExecutors.writingIOExecutor().execute(new Runnable() + { + @Override + public void run() + { + int id = playerIDs.get(player.getId()); + + Connection conn = null; + PreparedStatement ps = null; + try + { + conn = ds.getConnection(); + if(id == -1) + id = createPlayerID(conn, player); + ps = conn.prepareStatement("INSERT INTO `"+tab_player_gamedata+"` (`pid`, `data`) VALUES (?, ?) ON DUPLICATE KEY UPDATE `data`=?"); + ps.setInt(1, id); + byte[] data = CompressedStreamTools.compress(nbt); + ps.setBytes(2, data); + ps.setBytes(3, data); + ps.executeUpdate(); + } + catch(Exception e) + { + log.warn("Failed to save player gamedata " + player.getName(), e); + } + finally + { + close(conn, ps, null); + } + } + }); + } + + @Override + public PlayerData loadPlayerData(GameProfile player) + { + int id = playerIDs.get(player.getId()); + if(id == -1) + return readPlayerData(null); + + Connection conn = null; + PreparedStatement ps = null; + ResultSet rs = null; + try + { + conn = ds.getConnection(); + ps = conn.prepareStatement("SELECT `data` FROM `"+tab_player_data+"` WHERE `pid`=?"); + ps.setInt(1, id); + rs = ps.executeQuery(); + if(!rs.next()) + return readPlayerData(null); + return readPlayerData(CompressedStreamTools.readCompressed(new ByteArrayInputStream(rs.getBytes("data")))); + } + catch(Exception e) + { + log.warn("Failed to load player data for " + player.getName(), e); + } + finally + { + close(conn, ps, rs); + } + + return null; + } + + @Override + public List loadAllPlayerData() + { + List list = new LinkedList(); + + Connection conn = null; + PreparedStatement ps = null; + ResultSet rs = null; + try + { + conn = ds.getConnection(); + ps = conn.prepareStatement("SELECT `data` FROM `"+tab_player_data+"`"); + rs = ps.executeQuery(); + while(rs.next()) + list.add(readPlayerData(CompressedStreamTools.readCompressed(new ByteArrayInputStream(rs.getBytes("data"))))); + } + catch(Exception e) + { + log.warn("Failed to load all player data", e); + } + finally + { + close(conn, ps, rs); + } + + return list; + } + + @Override + public void savePlayerData(PlayerData data) + { + final NBTTagCompound nbt = new NBTTagCompound(); + nbt.setString("id", data.getProfile().getId().toString()); + nbt.setString("name", data.getProfile().getName()); + for(PlayerDataExtensionInfo info : mgr.getDataLoader().getDataExtProviders()) + { + NBTTagCompound extnbt = new NBTTagCompound(); + data.get(info.getExtClass()).writeToNBT(extnbt); + nbt.setTag(info.getTagName(), extnbt); + } + + final GameProfile player = data.getProfile(); + + GlobalExecutors.writingIOExecutor().execute(new Runnable() + { + @Override + public void run() + { + int id = playerIDs.get(player.getId()); + + Connection conn = null; + PreparedStatement ps = null; + try + { + conn = ds.getConnection(); + if(id == -1) + id = createPlayerID(conn, player); + ps = conn.prepareStatement("INSERT INTO `"+tab_player_data+"` (`pid`, `data`) VALUES (?, ?) ON DUPLICATE KEY UPDATE `data`=values(data)"); + ps.setInt(1, id); + ps.setBytes(2, CompressedStreamTools.compress(nbt)); + ps.executeUpdate(); + } + catch(Exception e) + { + log.warn("Failed to save player data " + player.getName(), e); + } + finally + { + close(conn, ps, null); + } + } + }); + } + + @Override + public Map loadWarps() + { + Map map = new HashMap(); + + Connection conn = null; + PreparedStatement ps = null; + ResultSet rs = null; + try + { + conn = ds.getConnection(); + ps = conn.prepareStatement("SELECT * FROM `"+tab_warps+"`"); + rs = ps.executeQuery(); + while(rs.next()) + { + String name = rs.getString(2); + int dim = rs.getInt(3); + double x = rs.getDouble(4); + double y = rs.getDouble(5); + double z = rs.getDouble(6); + float yaw = rs.getFloat(7); + float pitch = rs.getFloat(8); + double random = rs.getDouble(9); + + map.put(name, new WarpLocation(dim, x, y, z, yaw, pitch, random)); + } + } + catch(Exception e) + { + log.warn("Failed to load warps", e); + } + finally + { + close(conn, ps, rs); + } + + return map; + } + + @Override + public void saveWarp(final String name, final WarpLocation warp) + { + GlobalExecutors.writingIOExecutor().execute(new Runnable() + { + @Override + public void run() + { + Connection conn = null; + PreparedStatement ps = null; + try + { + conn = ds.getConnection(); + ps = conn.prepareStatement("INSERT INTO `"+tab_warps+"` (`name`, `dimension`, `x`, `y`, `z`, `yaw`, `pitch`, `random`) VALUES " + + "(?, ?, ?, ?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE " + + "`dimension`=values(dimension), `x`=values(x), `y`=values(y), `z`=values(z), `yaw`=values(yaw), `pitch`=values(pitch), `random`=values(random)"); + + ps.setString(1, name); + + ps.setInt(2, warp.dimension); + ps.setDouble(3, warp.x); + ps.setDouble(4, warp.y); + ps.setDouble(5, warp.z); + ps.setFloat(6, warp.yaw); + ps.setFloat(7, warp.pitch); + ps.setDouble(8, warp.randomRadius); + + ps.executeUpdate(); + } + catch(Exception e) + { + log.warn("Failed to save warp: " + name, e); + } + finally + { + close(conn, ps, null); + } + } + }); + } + + @Override + public void removeWarp(final String name) + { + GlobalExecutors.writingIOExecutor().execute(new Runnable() + { + @Override + public void run() + { + Connection conn = null; + PreparedStatement ps = null; + try + { + conn = ds.getConnection(); + ps = conn.prepareStatement("DELETE FROM `"+tab_warps+"` WHERE `name`=?"); + ps.setString(1, name); + ps.executeUpdate(); + } + catch(Exception e) + { + log.warn("Failed to remove warp: " + name, e); + } + finally + { + close(conn, ps, null); + } + } + }); + } + + @Override + public List loadFastWarps() + { + List list = new LinkedList(); + + Connection conn = null; + PreparedStatement ps = null; + ResultSet rs = null; + try + { + conn = ds.getConnection(); + ps = conn.prepareStatement("SELECT * FROM `"+tab_fastwarps+"`"); + rs = ps.executeQuery(); + while(rs.next()) + list.add(rs.getString(2)); + } + catch(Exception e) + { + log.warn("Failed to load fastwarps", e); + } + finally + { + close(conn, ps, rs); + } + + return list; + } + + @Override + public void saveFastWarp(final String name) + { + GlobalExecutors.writingIOExecutor().execute(new Runnable() + { + @Override + public void run() + { + Connection conn = null; + PreparedStatement ps = null; + try + { + conn = ds.getConnection(); + ps = conn.prepareStatement("INSERT INTO `"+tab_fastwarps+"` (`name`) VALUES (?)"); + ps.setString(1, name); + ps.executeUpdate(); + } + catch(Exception e) + { + log.warn("Failed to save fastwarp: " + name, e); + } + finally + { + close(conn, ps, null); + } + } + }); + } + + @Override + public void removeFastWarp(final String name) + { + GlobalExecutors.writingIOExecutor().execute(new Runnable() + { + @Override + public void run() + { + Connection conn = null; + PreparedStatement ps = null; + try + { + conn = ds.getConnection(); + ps = conn.prepareStatement("DELETE FROM `"+tab_fastwarps+"` WHERE `name`=?"); + ps.setString(1, name); + ps.executeUpdate(); + } + catch(Exception e) + { + log.warn("Failed to remove fastwarp: " + name, e); + } + finally + { + close(conn, ps, null); + } + } + }); + } + + private int createPlayerID(Connection conn, GameProfile player) throws SQLException + { + PreparedStatement ps = null; + ResultSet rs = null; + try + { + conn = ds.getConnection(); + ps = conn.prepareStatement("INSERT INTO `"+tab_player_ids+"` (`uuid`) VALUES (?)", Statement.RETURN_GENERATED_KEYS); + ps.setBytes(1, toBytes(player.getId())); + ps.executeUpdate(); + + rs = ps.getGeneratedKeys(); + if(!rs.next()) + throw new RuntimeException("!keys.next()"); //impossible?? + int id = rs.getInt(1); + playerIDs.put(player.getId(), id); + return id; + } + catch(SQLException e) + { + log.warn("Failed to create player data id " + player.getName()); + throw e; + } + finally + { + close(null, ps, rs); + } + } + + private void close(Connection conn, Statement ps, ResultSet rs) + { + if(rs != null) try{rs.close();} catch(SQLException e){} + if(ps != null) try{ps.close();} catch(SQLException e){} + if(conn != null) try{conn.close();} catch(SQLException e){} + } + + private static UUID toUUID(byte[] data) + { + long msb = 0; + long lsb = 0; + for (int i = 0; i < 8; i++) + msb = (msb << 8) | (data[i] & 0xff); + for (int i = 8; i < 16; i++) + lsb = (lsb << 8) | (data[i] & 0xff); + return new UUID(msb, lsb); + } + + private static byte[] toBytes(UUID uuid) + { + long msb = uuid.getMostSignificantBits(); + long lsb = uuid.getLeastSignificantBits(); + byte[] data = new byte[16]; + for (int i = 0; i < 8; i++) + data[7-i] = (byte)((msb >> 8*i) & 0xff); + for (int i = 0; i < 8; i++) + data[15-i] = (byte)((lsb >> 8*i) & 0xff); + return data; + } + + private PlayerData readPlayerData(NBTTagCompound nbt) + { + List infos = mgr.getDataLoader().getDataExtProviders(); + List data = new ArrayList(infos.size()); + + for(PlayerDataExtensionInfo info : infos) + { + data.add(info.createFromNBT(nbt)); + } + + PlayerData pdata = new PlayerData(data); + if(nbt != null && nbt.hasKey("id") && nbt.hasKey("name")) + pdata.setProfile(new GameProfile(UUID.fromString(nbt.getString("id")), nbt.getString("name"))); + return pdata; + } +} diff --git a/src/main/java/org/ultramine/server/data/NBTFileDataProvider.java b/src/main/java/org/ultramine/server/data/NBTFileDataProvider.java index 7ee84dc..c7d6428 100644 --- a/src/main/java/org/ultramine/server/data/NBTFileDataProvider.java +++ b/src/main/java/org/ultramine/server/data/NBTFileDataProvider.java @@ -38,6 +38,16 @@ { this.mgr = mgr; } + + @Override + public void init() + { + if(umPlayerDir == null) + { + umPlayerDir = new File(((SaveHandler)mgr.getPlayerNBTLoader()).getPlayerSaveDir(), "ultramine"); + umPlayerDir.mkdir(); + } + } @Override public NBTTagCompound loadPlayer(GameProfile player) @@ -68,15 +78,11 @@ @Override public PlayerData loadPlayerData(GameProfile player) { - checkPlayerDir(); - return readPlayerData(getPlayerDataNBT(player.getId().toString())); } public List loadAllPlayerData() { - checkPlayerDir(); - List list = new ArrayList(); for(File file : umPlayerDir.listFiles()) { @@ -155,15 +161,6 @@ { writeWarpList(); } - - private void checkPlayerDir() - { - if(umPlayerDir == null) - { - umPlayerDir = new File(((SaveHandler)mgr.getPlayerNBTLoader()).getPlayerSaveDir(), "ultramine"); - umPlayerDir.mkdir(); - } - } private NBTTagCompound getPlayerDataNBT(String username) { diff --git a/src/main/java/org/ultramine/server/data/ServerDataLoader.java b/src/main/java/org/ultramine/server/data/ServerDataLoader.java index d6e7ec5..b718d09 100644 --- a/src/main/java/org/ultramine/server/data/ServerDataLoader.java +++ b/src/main/java/org/ultramine/server/data/ServerDataLoader.java @@ -31,7 +31,7 @@ { private static final boolean isClient = FMLCommonHandler.instance().getSide().isClient(); private final ServerConfigurationManager mgr; - private final IDataProvider dataProvider; + private IDataProvider dataProvider; private final List dataExtinfos = new ArrayList(); private final Map playerDataCache = new HashMap(); private final Map namedPlayerDataCache = new HashMap(); @@ -41,7 +41,6 @@ public ServerDataLoader(ServerConfigurationManager mgr) { this.mgr = mgr; - dataProvider = new NBTFileDataProvider(mgr); } public IDataProvider getDataProvider() @@ -117,6 +116,9 @@ public void loadCache() { + dataProvider = isClient || !ConfigurationHandler.getServerConfig().inSQLServerStorage.enabled ? new NBTFileDataProvider(mgr) : new JDBCDataProvider(mgr); + dataProvider.init(); + for(PlayerData data : dataProvider.loadAllPlayerData()) { playerDataCache.put(data.getProfile().getId(), data);