diff --git a/docs/config.md b/docs/config.md index 539baeb9..279d4eb3 100644 --- a/docs/config.md +++ b/docs/config.md @@ -14,7 +14,20 @@ Found under `[database]` `queueTimeoutMin` [Default: 5] is the maximum amount of time to wait for the queue to drain when the server stops in minutes -`queueCheckDelaySec` [Default: 10] is the amount of time between checking if the queue is empty when the server stops in seconds +`queueCheckDelaySec` [Default: 10] is the frequency in seconds to notify in console that the queue is not empty when the server stops + +`autoPurgeDays` [Default: -1] is the number of days to keep actions in the database. If set to -1, actions will never be purged automatically + +`batchSize` [Default: 1000] is the number of actions to insert into the database at once. +This can be increased to improve performance, but may cause issues with slow databases + +`batchDelay` [Default: 10] is the amount of time in ticks to wait between batches if the next batch isn't full. +This can be increased to improve performance, but may cause issues with slow databases + +`location` [Default: Nothing] is the location of the database file when using the default SQLite database or other file based databases like H2. +The path is relative to the server's root directory. If the path is left out, the database will default to the server's world directory. + +`logSQL` [Default: false] will log all SQL queries to the console. This is useful for debugging, but can be very spammy ### Search settings diff --git a/docs/install.md b/docs/install.md index 1034f412..91a40f72 100644 --- a/docs/install.md +++ b/docs/install.md @@ -40,7 +40,7 @@ properties = [] `url`: Must be URL of database with `/` appended. An example URL would be `localhost/ledger`. You can optionally add port information such as `localhost:3000/ledger` ### PostgreSQL -MySQL requires running a separate PostgreSQL database and more setup than just plug and play SQLite, but can support much larger databases at faster speeds. It is more experimental the MySQL but may yield faster performance. +PostgreSQL requires running a separate PostgreSQL database and more setup than just plug and play SQLite, but can support much larger databases at faster speeds. It is more experimental the MySQL but may yield faster performance. Add the following to the bottom of your Ledger config file: diff --git a/docs/parameters.md b/docs/parameters.md index 0e705414..00ec7282 100644 --- a/docs/parameters.md +++ b/docs/parameters.md @@ -83,7 +83,7 @@ An easy way to remember the difference between `before:1d` and `after:1d` is to If you go back in time 1 day, do you want everything that happened `before` then or `after` then. Usually you want `after`. -### Rollback Status +## Rollback Status Key - `rolledback:` Value - `true` or `false` Negative Allowed - `No` diff --git a/src/main/kotlin/com/github/quiltservertools/ledger/api/ExtensionManager.kt b/src/main/kotlin/com/github/quiltservertools/ledger/api/ExtensionManager.kt index ee59f709..e676e127 100644 --- a/src/main/kotlin/com/github/quiltservertools/ledger/api/ExtensionManager.kt +++ b/src/main/kotlin/com/github/quiltservertools/ledger/api/ExtensionManager.kt @@ -2,8 +2,8 @@ package com.github.quiltservertools.ledger.api import com.github.quiltservertools.ledger.Ledger import com.github.quiltservertools.ledger.config.config +import com.github.quiltservertools.ledger.config.getDatabasePath import net.minecraft.server.MinecraftServer -import net.minecraft.util.WorldSavePath import javax.sql.DataSource object ExtensionManager { @@ -30,7 +30,7 @@ object ExtensionManager { extensions.forEach { if (it is DatabaseExtension) { if (dataSource == null) { - dataSource = it.getDataSource(server.getSavePath(WorldSavePath.ROOT)) + dataSource = it.getDataSource(config.getDatabasePath()) } else { failExtensionRegistration(it) } diff --git a/src/main/kotlin/com/github/quiltservertools/ledger/commands/arguments/SearchParamArgument.kt b/src/main/kotlin/com/github/quiltservertools/ledger/commands/arguments/SearchParamArgument.kt index 50162aa0..829e6738 100644 --- a/src/main/kotlin/com/github/quiltservertools/ledger/commands/arguments/SearchParamArgument.kt +++ b/src/main/kotlin/com/github/quiltservertools/ledger/commands/arguments/SearchParamArgument.kt @@ -27,6 +27,7 @@ import net.minecraft.util.math.BlockBox import net.minecraft.util.math.BlockPos import net.minecraft.util.math.Vec3i import java.time.Instant +import java.util.* import java.util.concurrent.CompletableFuture object SearchParamArgument { @@ -140,7 +141,8 @@ object SearchParamArgument { } } else { val profile = source.server.userCache?.findByName(sourceInput.property) - val id = profile?.orElse(null)?.id + // If the player doesn't exist use a random UUID to make the query not match + val id = profile?.orElse(null)?.id ?: UUID.randomUUID() if (id != null) { val playerIdEntry = Negatable(id, sourceInput.allowed) diff --git a/src/main/kotlin/com/github/quiltservertools/ledger/commands/parameters/SourceParameter.kt b/src/main/kotlin/com/github/quiltservertools/ledger/commands/parameters/SourceParameter.kt index ee9459d4..3c7d510c 100644 --- a/src/main/kotlin/com/github/quiltservertools/ledger/commands/parameters/SourceParameter.kt +++ b/src/main/kotlin/com/github/quiltservertools/ledger/commands/parameters/SourceParameter.kt @@ -27,14 +27,12 @@ class SourceParameter : SimpleParameter() { val stringReader = StringReader(builder.input) stringReader.cursor = builder.start - val players = context.source.playerNames + val sources = context.source.playerNames DatabaseManager.getKnownSources().forEach { - players.add("@$it") + sources.add("@$it") } - // TODO suggest non-player sources - return CommandSource.suggestMatching( - players, + sources, builder ) } diff --git a/src/main/kotlin/com/github/quiltservertools/ledger/config/DatabaseSpec.kt b/src/main/kotlin/com/github/quiltservertools/ledger/config/DatabaseSpec.kt index 8314ab96..62e953f2 100644 --- a/src/main/kotlin/com/github/quiltservertools/ledger/config/DatabaseSpec.kt +++ b/src/main/kotlin/com/github/quiltservertools/ledger/config/DatabaseSpec.kt @@ -1,6 +1,10 @@ package com.github.quiltservertools.ledger.config +import com.github.quiltservertools.ledger.Ledger +import com.uchuhimo.konf.Config import com.uchuhimo.konf.ConfigSpec +import net.minecraft.util.WorldSavePath +import java.nio.file.Path @Suppress("MagicNumber") object DatabaseSpec : ConfigSpec() { @@ -10,4 +14,14 @@ object DatabaseSpec : ConfigSpec() { val batchSize by optional(1000) val batchDelay by optional(10) val logSQL by optional(false) + val location by optional(null) +} + +fun Config.getDatabasePath(): Path { + val location = config[DatabaseSpec.location] + return if (location != null) { + Path.of(location) + } else { + Ledger.server.getSavePath(WorldSavePath.ROOT) + } } diff --git a/src/main/kotlin/com/github/quiltservertools/ledger/database/DatabaseManager.kt b/src/main/kotlin/com/github/quiltservertools/ledger/database/DatabaseManager.kt index 658867db..106abca7 100644 --- a/src/main/kotlin/com/github/quiltservertools/ledger/database/DatabaseManager.kt +++ b/src/main/kotlin/com/github/quiltservertools/ledger/database/DatabaseManager.kt @@ -8,25 +8,28 @@ import com.github.quiltservertools.ledger.actionutils.SearchResults import com.github.quiltservertools.ledger.config.DatabaseSpec import com.github.quiltservertools.ledger.config.SearchSpec import com.github.quiltservertools.ledger.config.config +import com.github.quiltservertools.ledger.config.getDatabasePath import com.github.quiltservertools.ledger.logInfo import com.github.quiltservertools.ledger.logWarn import com.github.quiltservertools.ledger.registry.ActionRegistry import com.github.quiltservertools.ledger.utility.Negatable import com.github.quiltservertools.ledger.utility.PlayerResult +import com.google.common.cache.Cache import com.mojang.authlib.GameProfile import kotlinx.coroutines.delay -import kotlinx.coroutines.sync.Mutex import net.minecraft.util.Identifier -import net.minecraft.util.WorldSavePath import net.minecraft.util.math.BlockPos +import org.jetbrains.exposed.dao.Entity +import org.jetbrains.exposed.dao.EntityClass +import org.jetbrains.exposed.dao.IntEntityClass import org.jetbrains.exposed.dao.id.EntityID +import org.jetbrains.exposed.dao.id.IntIdTable import org.jetbrains.exposed.sql.Column import org.jetbrains.exposed.sql.Database import org.jetbrains.exposed.sql.Op import org.jetbrains.exposed.sql.Query import org.jetbrains.exposed.sql.SchemaUtils import org.jetbrains.exposed.sql.SortOrder -import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq import org.jetbrains.exposed.sql.SqlExpressionBuilder.inSubQuery import org.jetbrains.exposed.sql.SqlExpressionBuilder.lessEq import org.jetbrains.exposed.sql.SqlLogger @@ -40,7 +43,6 @@ import org.jetbrains.exposed.sql.deleteWhere import org.jetbrains.exposed.sql.innerJoin import org.jetbrains.exposed.sql.insertAndGetId import org.jetbrains.exposed.sql.insertIgnore -import org.jetbrains.exposed.sql.lowerCase import org.jetbrains.exposed.sql.or import org.jetbrains.exposed.sql.orWhere import org.jetbrains.exposed.sql.selectAll @@ -49,14 +51,20 @@ import org.jetbrains.exposed.sql.statements.expandArgs import org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransaction import org.jetbrains.exposed.sql.transactions.transaction import org.jetbrains.exposed.sql.update +import org.sqlite.SQLiteConfig import org.sqlite.SQLiteDataSource import java.time.Instant import java.time.temporal.ChronoUnit import java.util.* +import java.util.function.Function import javax.sql.DataSource import kotlin.io.path.pathString import kotlin.math.ceil +const val MAX_QUERY_RETRIES = 10 +const val MIN_RETRY_DELAY = 1000L +const val MAX_RETRY_DELAY = 300_000L + object DatabaseManager { // These values are initialised late to allow the database to be created at server start, @@ -66,8 +74,6 @@ object DatabaseManager { get() = database.dialect.name private val cache = DatabaseCacheService - private val dbMutex = Mutex() - private var enforceMutex = false fun setup(dataSource: DataSource?) { val source = dataSource ?: getDefaultDatasource() @@ -75,9 +81,10 @@ object DatabaseManager { } private fun getDefaultDatasource(): DataSource { - val dbFilepath = Ledger.server.getSavePath(WorldSavePath.ROOT).resolve("ledger.sqlite").pathString - enforceMutex = true - return SQLiteDataSource().apply { + val dbFilepath = config.getDatabasePath().resolve("ledger.sqlite").pathString + return SQLiteDataSource(SQLiteConfig().apply { + setJournalMode(SQLiteConfig.JournalMode.WAL) + }).apply { url = "jdbc:sqlite:$dbFilepath" } } @@ -169,8 +176,8 @@ object DatabaseManager { type.world = Identifier.tryParse(action[Tables.Worlds.identifier]) type.objectIdentifier = Identifier(action[Tables.ObjectIdentifiers.identifier]) type.oldObjectIdentifier = Identifier( - action[Tables.ObjectIdentifiers.alias("oldObjects")[Tables.ObjectIdentifiers.identifier]] - ) + action[Tables.ObjectIdentifiers.alias("oldObjects")[Tables.ObjectIdentifiers.identifier]] + ) type.objectState = action[Tables.Actions.blockState] type.oldObjectState = action[Tables.Actions.oldBlockState] type.sourceName = action[Tables.Sources.name] @@ -209,62 +216,65 @@ object DatabaseManager { op = op.and { Tables.Actions.rolledBack.eq(params.rolledBack) } } - val sourceNames = ArrayList>() - params.sourceNames?.forEach { - val sourceId = getSourceId(it.property) - if (sourceId != null) { - sourceNames.add(Negatable(sourceId, it.allowed)) - } else { - // Unknown source name - op = Op.FALSE - } - } - - val playerNames = ArrayList>() - params.sourcePlayerIds?.forEach { - val playerId = getPlayerId(it.property) - if (playerId != null) { - playerNames.add(Negatable(playerId, it.allowed)) - } else { - // Unknown player name - op = Op.FALSE - } - } - op = addParameters( op, - sourceNames, + params.sourceNames, + DatabaseManager::getSourceId, Tables.Actions.sourceName ) op = addParameters( op, - params.actions?.map { Negatable(getActionId(it.property), it.allowed) }, + params.actions, + DatabaseManager::getActionId, Tables.Actions.actionIdentifier ) op = addParameters( op, - params.worlds?.map { Negatable(getWorldId(it.property), it.allowed) }, + params.worlds, + DatabaseManager::getWorldId, Tables.Actions.world ) op = addParameters( op, - params.objects?.map { Negatable(getRegistryKeyId(it.property), it.allowed) }, + params.objects, + DatabaseManager::getRegistryKeyId, Tables.Actions.objectId, Tables.Actions.oldObjectId ) op = addParameters( op, - playerNames, + params.sourcePlayerIds, + DatabaseManager::getPlayerId, Tables.Actions.sourcePlayer ) return op } + private fun , C : EntityID?, T> addParameters( + op: Op, + paramSet: Collection>?, + objectToId: Function, + column: Column, + orColumn: Column? = null, + ): Op { + val idParamSet = mutableSetOf>() + paramSet?.forEach { + val paramId = objectToId.apply(it.property) + if (paramId != null) { + idParamSet.add(Negatable(paramId, it.allowed)) + } else { + // Unknown source name + return Op.FALSE + } + } + return addParameters(op, idParamSet, column, orColumn) + } + private fun , C : EntityID?> addParameters( op: Op, paramSet: Collection>?, @@ -353,12 +363,15 @@ object DatabaseManager { } private suspend fun execute(body: suspend Transaction.() -> T): T { - if (enforceMutex) dbMutex.lock() while (Ledger.server.overworld?.savingDisabled != false) { delay(timeMillis = 1000) } return newSuspendedTransaction(db = database) { + repetitionAttempts = MAX_QUERY_RETRIES + minRepetitionDelay = MIN_RETRY_DELAY + maxRepetitionDelay = MAX_RETRY_DELAY + if (Ledger.config[DatabaseSpec.logSQL]) { addLogger(object : SqlLogger { override fun log(context: StatementContext, transaction: Transaction) { @@ -367,7 +380,7 @@ object DatabaseManager { }) } body(this) - }.also { if (enforceMutex) dbMutex.unlock() } + } } suspend fun purgeActions(params: ActionSearchParams) { @@ -401,18 +414,18 @@ object DatabaseManager { private fun Transaction.insertActions(actions: List) { Tables.Actions.batchInsert(actions, shouldReturnGeneratedValues = false) { action -> - this[Tables.Actions.actionIdentifier] = getActionId(action.identifier) + this[Tables.Actions.actionIdentifier] = getOrCreateActionId(action.identifier) this[Tables.Actions.timestamp] = action.timestamp this[Tables.Actions.x] = action.pos.x this[Tables.Actions.y] = action.pos.y this[Tables.Actions.z] = action.pos.z - this[Tables.Actions.objectId] = getRegistryKeyId(action.objectIdentifier) - this[Tables.Actions.oldObjectId] = getRegistryKeyId(action.oldObjectIdentifier) - this[Tables.Actions.world] = getWorldId(action.world ?: Ledger.server.overworld.registryKey.value) + this[Tables.Actions.objectId] = getOrCreateRegistryKeyId(action.objectIdentifier) + this[Tables.Actions.oldObjectId] = getOrCreateRegistryKeyId(action.oldObjectIdentifier) + this[Tables.Actions.world] = getOrCreateWorldId(action.world ?: Ledger.server.overworld.registryKey.value) this[Tables.Actions.blockState] = action.objectState this[Tables.Actions.oldBlockState] = action.oldObjectState this[Tables.Actions.sourceName] = getOrCreateSourceId(action.sourceName) - this[Tables.Actions.sourcePlayer] = action.sourceProfile?.let { getPlayerId(it.id) } + this[Tables.Actions.sourcePlayer] = action.sourceProfile?.let { getOrCreatePlayerId(it.id) } this[Tables.Actions.extraData] = action.extraData } } @@ -439,9 +452,10 @@ object DatabaseManager { .innerJoin(Tables.ActionIdentifiers) .innerJoin(Tables.Worlds) .leftJoin(Tables.Players) - .innerJoin(Tables.oldObjectTable, { - Tables.Actions.oldObjectId - }, { Tables.oldObjectTable[Tables.ObjectIdentifiers.id] }) + .innerJoin( + Tables.oldObjectTable, + { Tables.Actions.oldObjectId }, + { Tables.oldObjectTable[Tables.ObjectIdentifiers.id] }) .innerJoin(Tables.ObjectIdentifiers, { Tables.Actions.objectId }, { Tables.ObjectIdentifiers.id }) .innerJoin(Tables.Sources) .selectAll() @@ -480,9 +494,10 @@ object DatabaseManager { .innerJoin(Tables.ActionIdentifiers) .innerJoin(Tables.Worlds) .leftJoin(Tables.Players) - .innerJoin(Tables.oldObjectTable, { - Tables.Actions.oldObjectId - }, { Tables.oldObjectTable[Tables.ObjectIdentifiers.id] }) + .innerJoin( + Tables.oldObjectTable, + { Tables.Actions.oldObjectId }, + { Tables.oldObjectTable[Tables.ObjectIdentifiers.id] }) .innerJoin(Tables.ObjectIdentifiers, { Tables.Actions.objectId }, { Tables.ObjectIdentifiers.id }) .innerJoin(Tables.Sources) .selectAll() @@ -500,17 +515,17 @@ object DatabaseManager { .innerJoin(Tables.ActionIdentifiers) .innerJoin(Tables.Worlds) .leftJoin(Tables.Players) - .innerJoin(Tables.oldObjectTable, { - Tables.Actions.oldObjectId - }, { Tables.oldObjectTable[Tables.ObjectIdentifiers.id] }) + .innerJoin( + Tables.oldObjectTable, + { Tables.Actions.oldObjectId }, + { Tables.oldObjectTable[Tables.ObjectIdentifiers.id] }) .innerJoin(Tables.ObjectIdentifiers, { Tables.Actions.objectId }, { Tables.ObjectIdentifiers.id }) .innerJoin(Tables.Sources) .selectAll() .andWhere { buildQueryParams(params) and (Tables.Actions.rolledBack eq false) } .orderBy(Tables.Actions.id, SortOrder.DESC) - val actionIds = selectQuery.map { - it[Tables.Actions.id] - }.toSet() // SQLite doesn't support update where so select by ID. Might not be as efficent + val actionIds = selectQuery.map { it[Tables.Actions.id] } + .toSet() // SQLite doesn't support update where so select by ID. Might not be as efficent actions.addAll(getActionsFromQuery(selectQuery)) Tables.Actions @@ -528,9 +543,10 @@ object DatabaseManager { .innerJoin(Tables.ActionIdentifiers) .innerJoin(Tables.Worlds) .leftJoin(Tables.Players) - .innerJoin(Tables.oldObjectTable, { - Tables.Actions.oldObjectId - }, { Tables.oldObjectTable[Tables.ObjectIdentifiers.id] }) + .innerJoin( + Tables.oldObjectTable, + { Tables.Actions.oldObjectId }, + { Tables.oldObjectTable[Tables.ObjectIdentifiers.id] }) .innerJoin(Tables.ObjectIdentifiers, { Tables.Actions.objectId }, { Tables.ObjectIdentifiers.id }) .innerJoin(Tables.Sources) .selectAll() @@ -547,62 +563,122 @@ object DatabaseManager { return actions } - private fun getPlayerId(playerId: UUID): Int? { - cache.playerKeys.getIfPresent(playerId)?.let { return it } + fun getKnownSources() = + cache.sourceKeys.asMap().keys - return Tables.Player.find { Tables.Players.playerId eq playerId }.firstOrNull()?.id?.value?.also { - cache.playerKeys.put(playerId, it) + private fun getObjectId( + obj: T, + cache: Cache, + table: EntityClass>, + column: Column + ): Int? = getObjectId(obj, Function.identity(), cache, table, column) + + private fun getObjectId( + obj: T, + mapper: Function, + cache: Cache, + table: EntityClass>, + column: Column + ): Int? { + cache.getIfPresent(obj)?.let { return it } + return table.find { column eq mapper.apply(obj) }.firstOrNull()?.id?.value?.also { + cache.put(obj, it) } } - private fun Transaction.selectPlayer(playerName: String) = - Tables.Player.find { Tables.Players.playerName.lowerCase() eq playerName }.firstOrNull() - - fun getKnownSources() = - cache.sourceKeys.asMap().keys + private fun getOrCreateObjectId( + obj: T, + cache: Cache, + entity: IntEntityClass<*>, + table: IntIdTable, + column: Column + ): Int = + getOrCreateObjectId(obj, Function.identity(), cache, entity, table, column) + + private fun getOrCreateObjectId( + obj: T, + mapper: Function, + cache: Cache, + entity: IntEntityClass<*>, + table: IntIdTable, + column: Column + ): Int { + getObjectId(obj, mapper, cache, entity, column)?.let { return it } + + return entity[ + table.insertAndGetId { + it[column] = mapper.apply(obj) + } + ].id.value.also { cache.put(obj, it) } + } - private fun getSourceId(source: String): Int? { - cache.sourceKeys.getIfPresent(source)?.let { return it } + private fun getOrCreatePlayerId(playerId: UUID): Int = + getOrCreateObjectId(playerId, cache.playerKeys, Tables.Player, Tables.Players, Tables.Players.playerId) - return Tables.Source.find { Tables.Sources.name eq source }.firstOrNull()?.id?.value.also { - it?.let { cache.sourceKeys.put(source, it) } - } - } + private fun getOrCreateSourceId(source: String): Int = + getOrCreateObjectId(source, cache.sourceKeys, Tables.Source, Tables.Sources, Tables.Sources.name) - private fun getOrCreateSourceId(source: String): Int { - cache.sourceKeys.getIfPresent(source)?.let { return it } - Tables.Source.find { Tables.Sources.name eq source }.firstOrNull()?.let { return it.id.value } + private fun getOrCreateActionId(actionTypeId: String): Int = + getOrCreateObjectId( + actionTypeId, + cache.actionIdentifierKeys, + Tables.ActionIdentifier, + Tables.ActionIdentifiers, + Tables.ActionIdentifiers.actionIdentifier + ) - return Tables.Source[ - Tables.Sources.insertAndGetId { - it[name] = source - } - ].id.value.also { cache.sourceKeys.put(source, it) } - } + private fun getOrCreateRegistryKeyId(identifier: Identifier): Int = + getOrCreateObjectId( + identifier, + Identifier::toString, + cache.objectIdentifierKeys, + Tables.ObjectIdentifier, + Tables.ObjectIdentifiers, + Tables.ObjectIdentifiers.identifier + ) - private fun getActionId(actionTypeId: String): Int { - cache.actionIdentifierKeys.getIfPresent(actionTypeId)?.let { return it } + private fun getOrCreateWorldId(identifier: Identifier): Int = + getOrCreateObjectId( + identifier, + Identifier::toString, + cache.worldIdentifierKeys, + Tables.World, + Tables.Worlds, + Tables.Worlds.identifier + ) - return Tables.ActionIdentifier.find { Tables.ActionIdentifiers.actionIdentifier eq actionTypeId } - .first().id.value - .also { cache.actionIdentifierKeys.put(actionTypeId, it) } - } + private fun getPlayerId(playerId: UUID): Int? = + getObjectId(playerId, cache.playerKeys, Tables.Player, Tables.Players.playerId) - private fun getRegistryKeyId(identifier: Identifier): Int { - cache.objectIdentifierKeys.getIfPresent(identifier)?.let { return it } + private fun getSourceId(source: String): Int? = + getObjectId(source, cache.sourceKeys, Tables.Source, Tables.Sources.name) - return Tables.ObjectIdentifier.find { Tables.ObjectIdentifiers.identifier eq identifier.toString() } - .limit(1).first().id.value - .also { cache.objectIdentifierKeys.put(identifier, it) } - } + private fun getActionId(actionTypeId: String): Int? = + getObjectId( + actionTypeId, + cache.actionIdentifierKeys, + Tables.ActionIdentifier, + Tables.ActionIdentifiers.actionIdentifier + ) - private fun getWorldId(identifier: Identifier): Int { - cache.worldIdentifierKeys.getIfPresent(identifier)?.let { return it } + private fun getRegistryKeyId(identifier: Identifier): Int? = + getObjectId( + identifier, + Identifier::toString, + cache.objectIdentifierKeys, + Tables.ObjectIdentifier, + Tables.ObjectIdentifiers.identifier + ) - return Tables.World.find { Tables.Worlds.identifier eq identifier.toString() }.limit(1).first().id.value - .also { cache.worldIdentifierKeys.put(identifier, it) } - } + private fun getWorldId(identifier: Identifier): Int? = + getObjectId( + identifier, + Identifier::toString, + cache.worldIdentifierKeys, + Tables.World, + Tables.Worlds.identifier + ) // Workaround because can't delete from a join in exposed https://kotlinlang.slack.com/archives/C0CG7E0A1/p1605866974117400 private fun Transaction.purgeActions(params: ActionSearchParams) = Tables.Actions diff --git a/src/main/resources/data/ledger/lang/es_es.json b/src/main/resources/data/ledger/lang/es_es.json new file mode 100644 index 00000000..07b45447 --- /dev/null +++ b/src/main/resources/data/ledger/lang/es_es.json @@ -0,0 +1,66 @@ +{ + "text.ledger.header.search.pos": "--- Buscando registros en %s ---", + "text.ledger.header.search": "------ Buscando registros ------", + "text.ledger.header.status": "------ Ledger ------", + + "text.ledger.footer.search": "------- %s [Página %s de %s] %s -------", + "text.ledger.footer.page_forward": ">>", + "text.ledger.footer.page_forward.hover": "Página siguiente", + "text.ledger.footer.page_backward": "<<", + "text.ledger.footer.page_backward.hover": "Página anterior", + + "text.ledger.status.queue": "Cola de Base de Datos: %s", + "text.ledger.status.queue.busy": "Ocupada", + "text.ledger.status.queue.empty": "Vacía", + "text.ledger.status.version": "Versión: %s", + "text.ledger.status.discord": "Discord: %s", + "text.ledger.status.discord.join": "Clic para unirse", + "text.ledger.status.db_type": "Tipo de Base de Datos: %s", + "text.ledger.status.wiki": "Wiki: %s", + "text.ledger.status.wiki.view": "Clic para ver", + + "text.ledger.preview.start": "Iniciando vista previa", + + "text.ledger.action_message": "%1$s %2$s %3$s %4$s %5$s", + "text.ledger.action_message.time_diff": "hace %s", + "text.ledger.action_message.location.hover": "Clic para teletransportarse", + "text.ledger.action_message.with": "with", + + "text.ledger.rollback.start": "Retrocediendo %s acciones", + "text.ledger.rollback.finish": "Retroceso completo", + "text.ledger.rollback.fail": "Fallo al retroceder %s x%s", + + "text.ledger.restore.start": "Restaurando %s acciones", + "text.ledger.restore.finish": "Restauración completa", + "text.ledger.restore.fail": "Fallo al restaurar %s x%s", + + "text.ledger.inspect.toggle": "Modo de inspección: %s", + "text.ledger.inspect.on": "Activado", + "text.ledger.inspect.off": "Desactivado", + + "text.ledger.database.busy": "La base de datos está actualmente ocupada, esto puede tomar un momento.", + + "error.ledger.no_cached_params": "Sin búsqueda en caché, realice una búsqueda antes de continuar.", + "error.ledger.command.no_results": "Sin resultados de búsqueda", + "error.ledger.no_more_pages": "No más páginas", + "error.ledger.unknown_param": "Parámetro desconocido %s", + "error.ledger.no_preview": "Sin vista previa", + + "text.ledger.action.block-place": "colocado", + "text.ledger.action.block-break": "roto", + "text.ledger.action.block-change": "cambiado", + "text.ledger.action.item-insert": "añadido", + "text.ledger.action.item-remove": "quitado", + "text.ledger.action.item-pick-up": "recogido", + "text.ledger.action.item-drop": "dropeado", + "text.ledger.action.entity-kill": "matado", + "text.ledger.action.entity-change": "cambiado", + + "text.ledger.network.protocols_mismatched": "Las versiones del protocolo no coinciden: la versión del Ledger es %d y la versión del cliente es %d", + "text.ledger.network.no_mod_info": "No se pudo identificar la información del mod de cliente.", + + "text.ledger.purge.starting": "------ Iniciando purga ------", + "text.ledger.purge.complete": "------ Purga completada ------", + + "text.ledger.player.result": "%1$s: Primera conexión: %2$s. Última conexión: %3$s" +} \ No newline at end of file diff --git a/src/main/resources/ledger.toml b/src/main/resources/ledger.toml index b04e48a8..eb32917d 100644 --- a/src/main/resources/ledger.toml +++ b/src/main/resources/ledger.toml @@ -9,6 +9,8 @@ autoPurgeDays = -1 batchSize = 1000 # The amount of time to wait between batches in ticks (20 ticks = 1 second) batchDelay = 10 +# The location of the database file. Defaults to the world folder if not specified +#location = "./custom-dir" [search] # Number of actions to show per page