package emu.grasscutter.database; import static emu.grasscutter.config.Configuration.*; import com.mongodb.MongoCommandException; import com.mongodb.client.MongoClient; import com.mongodb.client.MongoClients; import com.mongodb.client.MongoDatabase; import com.mongodb.client.MongoIterable; import dev.morphia.Datastore; import dev.morphia.Morphia; import dev.morphia.annotations.Entity; import dev.morphia.mapping.Mapper; import dev.morphia.mapping.MapperOptions; import dev.morphia.query.experimental.filters.Filters; import emu.grasscutter.Grasscutter; import emu.grasscutter.Grasscutter.ServerRunMode; import emu.grasscutter.game.Account; import org.reflections.Reflections; public final class DatabaseManager { private static Datastore gameDatastore; private static Datastore dispatchDatastore; public static Datastore getGameDatastore() { return gameDatastore; } public static MongoDatabase getGameDatabase() { return getGameDatastore().getDatabase(); } // Yes. I very dislike this method. However, this will be good for now. // TODO: Add dispatch routes for player account management public static Datastore getAccountDatastore() { if (SERVER.runMode == ServerRunMode.GAME_ONLY) { return dispatchDatastore; } else { return gameDatastore; } } public static void initialize() { // Initialize MongoClient gameMongoClient = MongoClients.create(DATABASE.game.connectionUri); // Set mapper options. MapperOptions mapperOptions = MapperOptions.builder() .storeEmpties(true).storeNulls(false).build(); // Create data store. gameDatastore = Morphia.createDatastore(gameMongoClient, DATABASE.game.collection, mapperOptions); // Map classes. Class[] entities = new Reflections(Grasscutter.class.getPackageName()) .getTypesAnnotatedWith(Entity.class) .stream() .filter(cls -> { Entity e = cls.getAnnotation(Entity.class); return e != null && !e.value().equals(Mapper.IGNORED_FIELDNAME); }) .toArray(Class[]::new); gameDatastore.getMapper().map(entities); // Ensure indexes for the game datastore ensureIndexes(gameDatastore); if (SERVER.runMode == ServerRunMode.GAME_ONLY) { MongoClient dispatchMongoClient = MongoClients.create(DATABASE.server.connectionUri); dispatchDatastore = Morphia.createDatastore(dispatchMongoClient, DATABASE.server.collection, mapperOptions); dispatchDatastore.getMapper().map(new Class[] {DatabaseCounter.class, Account.class}); // Ensure indexes for dispatch datastore ensureIndexes(dispatchDatastore); } } /** * Ensures the database indexes exist and rebuilds them if there is an error with them * @param datastore The datastore to ensure indexes on */ private static void ensureIndexes(Datastore datastore) { try { datastore.ensureIndexes(); } catch (MongoCommandException e) { Grasscutter.getLogger().info("Mongo index error: ", e); // Duplicate index error if (e.getCode() == 85) { // Drop all indexes and re add them MongoIterable collections = datastore.getDatabase().listCollectionNames(); for (String name : collections) { datastore.getDatabase().getCollection(name).dropIndexes(); } // Add back indexes datastore.ensureIndexes(); } } } public static synchronized int getNextId(Class c) { DatabaseCounter counter = getGameDatastore().find(DatabaseCounter.class).filter(Filters.eq("_id", c.getSimpleName())).first(); if (counter == null) { counter = new DatabaseCounter(c.getSimpleName()); } try { return counter.getNextId(); } finally { getGameDatastore().save(counter); } } public static synchronized int getNextId(Object o) { return getNextId(o.getClass()); } }