Initial Commit

This commit is contained in:
Melledy
2025-10-27 02:02:26 -07:00
commit f58951fe2a
378 changed files with 315914 additions and 0 deletions

View File

@@ -0,0 +1,9 @@
package emu.nebula.database;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@Retention(RetentionPolicy.RUNTIME)
public @interface AccountDatabaseOnly {
}

View File

@@ -0,0 +1,23 @@
package emu.nebula.database;
import dev.morphia.annotations.Entity;
import dev.morphia.annotations.Id;
@Entity(value = "counters", useDiscriminator = false)
public class DatabaseCounter {
@Id
private String id;
private int count;
public DatabaseCounter() {}
public DatabaseCounter(String id) {
this.id = id;
this.count = 10000;
}
public int getNextId() {
int id = ++count;
return id;
}
}

View File

@@ -0,0 +1,251 @@
package emu.nebula.database;
import java.util.stream.Stream;
import emu.nebula.Config.DatabaseInfo;
import emu.nebula.Config.InternalMongoInfo;
import emu.nebula.Nebula;
import emu.nebula.Nebula.ServerType;
import emu.nebula.database.codecs.*;
import emu.nebula.util.Utils;
import org.bson.codecs.configuration.CodecRegistries;
import org.reflections.Reflections;
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 com.mongodb.client.result.DeleteResult;
import de.bwaldvogel.mongo.MongoBackend;
import de.bwaldvogel.mongo.MongoServer;
import de.bwaldvogel.mongo.backend.h2.H2Backend;
import de.bwaldvogel.mongo.backend.memory.MemoryBackend;
import dev.morphia.*;
import dev.morphia.annotations.Entity;
import dev.morphia.mapping.Mapper;
import dev.morphia.mapping.MapperOptions;
import dev.morphia.query.filters.Filters;
import dev.morphia.query.updates.UpdateOperators;
import lombok.Getter;
@Getter
public final class DatabaseManager {
@Getter
private static MongoServer server;
private Datastore datastore;
private static final InsertOneOptions INSERT_OPTIONS = new InsertOneOptions();
private static final DeleteOptions DELETE_OPTIONS = new DeleteOptions();
private static final DeleteOptions DELETE_MANY = new DeleteOptions().multi(true);
public DatabaseManager(DatabaseInfo info, ServerType type) {
// Variables
var internalConfig = Nebula.getConfig().getInternalMongoServer();
String connectionString = info.getUri();
// Start local mongo server
if (info.isUseInternal()) {
if (Utils.isPortOpen(internalConfig.getAddress(), internalConfig.getPort())) {
connectionString = startInternalMongoServer(internalConfig);
Nebula.getLogger().info("Started local MongoDB server at " + server.getConnectionString());
} else {
Nebula.getLogger().warn("Local MongoDB server could not be created because the port is in use.");
}
}
// Initialize
MongoClient mongoClient = MongoClients.create(connectionString);
// Add our custom fastutil codecs
var codecProvider = CodecRegistries.fromCodecs(
new IntSetCodec(), new IntListCodec(), new Int2IntMapCodec(), new ItemParamMapCodec()
);
// Set mapper options
MapperOptions mapperOptions = MapperOptions.builder()
.storeEmpties(true)
.storeNulls(false)
.codecProvider(codecProvider)
.build();
// Create data store.
datastore = Morphia.createDatastore(mongoClient, info.getCollection(), mapperOptions);
// Map classes
var entities = new Reflections(Nebula.class.getPackageName())
.getTypesAnnotatedWith(Entity.class)
.stream()
.filter(cls -> {
Entity e = cls.getAnnotation(Entity.class);
return e != null && !e.value().equals(Mapper.IGNORED_FIELDNAME);
})
.toList();
if (type.runLogin()) {
// Only map account related entities
var map = entities.stream().filter(cls -> {
return cls.getAnnotation(AccountDatabaseOnly.class) != null;
}).toArray(Class<?>[]::new);
datastore.getMapper().map(map);
}
if (type.runGame()) {
// Only map game related entities
var map = entities.stream().filter(cls -> {
return cls.getAnnotation(AccountDatabaseOnly.class) == null;
}).toArray(Class<?>[]::new);
datastore.getMapper().map(map);
}
// Ensure indexes
ensureIndexes();
// Done
Nebula.getLogger().info("Connected to the MongoDB database at " + connectionString);
}
public MongoDatabase getDatabase() {
return getDatastore().getDatabase();
}
private void ensureIndexes() {
try {
datastore.ensureIndexes();
} catch (MongoCommandException exception) {
Nebula.getLogger().warn("Mongo index error: ", exception);
// Duplicate index error
if (exception.getCode() == 85) {
// Drop all indexes and re add them
MongoIterable<String> collections = datastore.getDatabase().listCollectionNames();
for (String name : collections) {
datastore.getDatabase().getCollection(name).dropIndexes();
}
// Add back indexes
datastore.ensureIndexes();
}
}
}
// Database Functions
public boolean checkIfObjectExists(Class<?> cls, String filter, String value) {
return getDatastore().find(cls).filter(Filters.eq(filter, value)).count() > 0;
}
public <T> T getObjectByUid(Class<T> cls, long uid) {
return getDatastore().find(cls).filter(Filters.eq("_id", uid)).first();
}
public <T> T getObjectByField(Class<T> cls, String filter, Object value) {
return getDatastore().find(cls).filter(Filters.eq(filter, value)).first();
}
public <T> T getObjectByField(Class<T> cls, String filter, long value) {
return getDatastore().find(cls).filter(Filters.eq(filter, value)).first();
}
public <T> Stream<T> getObjects(Class<T> cls, String filter, Object value) {
return getDatastore().find(cls).filter(Filters.eq(filter, value)).stream();
}
public <T> Stream<T> getObjects(Class<T> cls, String filter, long value) {
return getDatastore().find(cls).filter(Filters.eq(filter, value)).stream();
}
public <T> Stream<T> getObjects(Class<T> cls) {
return getDatastore().find(cls).stream();
}
public <T> void save(T obj) {
getDatastore().save(obj, INSERT_OPTIONS);
}
public <T> boolean delete(T obj) {
DeleteResult result = getDatastore().delete(obj, DELETE_OPTIONS);
return result.getDeletedCount() > 0;
}
public boolean delete(Class<?> cls, String filter, long uid) {
DeleteResult result = getDatastore().find(cls).filter(Filters.eq(filter, uid)).delete(DELETE_MANY);
return result.getDeletedCount() > 0;
}
public void update(Object obj, int uid, String field, Object item) {
update(obj, uid, field, item, false);
}
public void update(Object obj, int uid, String field, Object value, boolean upsert) {
var opt = new UpdateOptions().upsert(upsert);
getDatastore().find(obj.getClass())
.filter(Filters.eq("_id", uid))
.update(opt, UpdateOperators.set(field, value));
}
@SuppressWarnings("removal")
public void update(Object obj, int uid, String field, Object value, String field2, Object value2) {
getDatastore().find(obj.getClass())
.filter(Filters.eq("_id", uid))
.update(UpdateOperators.set(field, value), UpdateOperators.set(field2, value2));
}
public void updateNested(Object obj, int uid, String filter, int filterId, String field, Object item) {
var opt = new UpdateOptions().upsert(false);
getDatastore().find(obj.getClass())
.filter(Filters.eq("_id", uid))
.filter(Filters.eq(filter, filterId))
.update(opt, UpdateOperators.set(field, item));
}
public void addToList(Object obj, int uid, String field, Object item) {
var opt = new UpdateOptions().upsert(false);
getDatastore().find(obj.getClass())
.filter(Filters.eq("_id", uid))
.update(opt, UpdateOperators.addToSet(field, item));
}
// Database counter
public synchronized int getNextObjectId(Class<?> c) {
DatabaseCounter counter = getDatastore().find(DatabaseCounter.class).filter(Filters.eq("_id", c.getSimpleName())).first();
if (counter == null) {
counter = new DatabaseCounter(c.getSimpleName());
}
try {
return counter.getNextId();
} finally {
getDatastore().save(counter);
}
}
// Internal MongoDB server
public static String startInternalMongoServer(InternalMongoInfo internalMongo) {
// Get backend
MongoBackend backend = null;
if (internalMongo.filePath != null && internalMongo.filePath.length() > 0) {
backend = new H2Backend(internalMongo.filePath);
} else {
backend = new MemoryBackend();
}
// Create the local mongo server and replace the connection string
server = new MongoServer(backend);
// Bind to address of it exists
if (internalMongo.getAddress() != null && internalMongo.getPort() != 0) {
server.bind(internalMongo.getAddress(), internalMongo.getPort());
} else {
server.bind(); // Binds to random port
}
return server.getConnectionString();
}
}

View File

@@ -0,0 +1,11 @@
package emu.nebula.database;
import emu.nebula.Nebula;
public interface GameDatabaseObject {
public default void save() {
Nebula.getGameDatabase().save(this);
}
}

View File

@@ -0,0 +1,43 @@
package emu.nebula.database.codecs;
import org.bson.BsonReader;
import org.bson.BsonType;
import org.bson.BsonWriter;
import org.bson.codecs.Codec;
import org.bson.codecs.DecoderContext;
import org.bson.codecs.EncoderContext;
import it.unimi.dsi.fastutil.ints.Int2IntMap;
import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap;
/**
* Custom mongodb codec for encoding/decoding fastutil int2int maps.
*/
public class Int2IntMapCodec implements Codec<Int2IntMap> {
@Override
public Class<Int2IntMap> getEncoderClass() {
return Int2IntMap.class;
}
@Override
public void encode(BsonWriter writer, Int2IntMap collection, EncoderContext encoderContext) {
writer.writeStartDocument();
for (var entry : collection.int2IntEntrySet()) {
writer.writeName(Integer.toString(entry.getIntKey()));
writer.writeInt32(entry.getIntValue());
}
writer.writeEndDocument();
}
@Override
public Int2IntMap decode(BsonReader reader, DecoderContext decoderContext) {
Int2IntMap collection = new Int2IntOpenHashMap();
reader.readStartDocument();
while (reader.readBsonType() != BsonType.END_OF_DOCUMENT) {
collection.put(Integer.parseInt(reader.readName()), reader.readInt32());
}
reader.readEndDocument();
return collection;
}
}

View File

@@ -0,0 +1,42 @@
package emu.nebula.database.codecs;
import org.bson.BsonReader;
import org.bson.BsonType;
import org.bson.BsonWriter;
import org.bson.codecs.Codec;
import org.bson.codecs.DecoderContext;
import org.bson.codecs.EncoderContext;
import it.unimi.dsi.fastutil.ints.IntArrayList;
import it.unimi.dsi.fastutil.ints.IntList;
/**
* Custom mongodb codec for encoding/decoding fastutil int sets.
*/
public class IntListCodec implements Codec<IntList> {
@Override
public Class<IntList> getEncoderClass() {
return IntList.class;
}
@Override
public void encode(BsonWriter writer, IntList collection, EncoderContext encoderContext) {
writer.writeStartArray();
for (int value : collection) {
writer.writeInt32(value);
}
writer.writeEndArray();
}
@Override
public IntList decode(BsonReader reader, DecoderContext decoderContext) {
IntList collection = new IntArrayList();
reader.readStartArray();
while (reader.readBsonType() != BsonType.END_OF_DOCUMENT) {
collection.add(reader.readInt32());
}
reader.readEndArray();
return collection;
}
}

View File

@@ -0,0 +1,42 @@
package emu.nebula.database.codecs;
import org.bson.BsonReader;
import org.bson.BsonType;
import org.bson.BsonWriter;
import org.bson.codecs.Codec;
import org.bson.codecs.DecoderContext;
import org.bson.codecs.EncoderContext;
import it.unimi.dsi.fastutil.ints.IntOpenHashSet;
import it.unimi.dsi.fastutil.ints.IntSet;
/**
* Custom mongodb codec for encoding/decoding fastutil int sets.
*/
public class IntSetCodec implements Codec<IntSet> {
@Override
public Class<IntSet> getEncoderClass() {
return IntSet.class;
}
@Override
public void encode(BsonWriter writer, IntSet collection, EncoderContext encoderContext) {
writer.writeStartArray();
for (int value : collection) {
writer.writeInt32(value);
}
writer.writeEndArray();
}
@Override
public IntSet decode(BsonReader reader, DecoderContext decoderContext) {
IntSet collection = new IntOpenHashSet();
reader.readStartArray();
while (reader.readBsonType() != BsonType.END_OF_DOCUMENT) {
collection.add(reader.readInt32());
}
reader.readEndArray();
return collection;
}
}

View File

@@ -0,0 +1,42 @@
package emu.nebula.database.codecs;
import org.bson.BsonReader;
import org.bson.BsonType;
import org.bson.BsonWriter;
import org.bson.codecs.Codec;
import org.bson.codecs.DecoderContext;
import org.bson.codecs.EncoderContext;
import emu.nebula.game.inventory.ItemParamMap;
/**
* Copy of Int2IntMapCodec.java
*/
public class ItemParamMapCodec implements Codec<ItemParamMap> {
@Override
public Class<ItemParamMap> getEncoderClass() {
return ItemParamMap.class;
}
@Override
public void encode(BsonWriter writer, ItemParamMap collection, EncoderContext encoderContext) {
writer.writeStartDocument();
for (var entry : collection.int2IntEntrySet()) {
writer.writeName(Integer.toString(entry.getIntKey()));
writer.writeInt32(entry.getIntValue());
}
writer.writeEndDocument();
}
@Override
public ItemParamMap decode(BsonReader reader, DecoderContext decoderContext) {
ItemParamMap collection = new ItemParamMap();
reader.readStartDocument();
while (reader.readBsonType() != BsonType.END_OF_DOCUMENT) {
collection.put(Integer.parseInt(reader.readName()), reader.readInt32());
}
reader.readEndDocument();
return collection;
}
}