From e3a6ad2410e827f95aa20104646b9340b9941a8f Mon Sep 17 00:00:00 2001 From: Wueffi Date: Sat, 30 Aug 2025 02:08:06 +0200 Subject: [PATCH 01/13] Make a Test Command and do the backend implementation. --- .../org/openredstone/trialore/Storage.kt | 65 +++++++++++- .../org/openredstone/trialore/TestCommand.kt | 98 +++++++++++++++++++ .../org/openredstone/trialore/TrialOre.kt | 70 +++++++++++++ 3 files changed, 231 insertions(+), 2 deletions(-) create mode 100644 src/main/kotlin/org/openredstone/trialore/TestCommand.kt diff --git a/src/main/kotlin/org/openredstone/trialore/Storage.kt b/src/main/kotlin/org/openredstone/trialore/Storage.kt index 0cabcf5..04582af 100644 --- a/src/main/kotlin/org/openredstone/trialore/Storage.kt +++ b/src/main/kotlin/org/openredstone/trialore/Storage.kt @@ -24,6 +24,16 @@ object Trial : Table("trial") { override val primaryKey = PrimaryKey(id) } +object Test : Table("test") { + val id = integer("id").autoIncrement() + val testificate = varchar("testificate", 36).index() + val start = integer("start") + val end = integer("end").nullable() + val passed = bool("passed").nullable() + val wrong = integer("wrong").nullable() + override val primaryKey = PrimaryKey(id) +} + object UsernameCache : Table("username_cache") { val uuid = varchar("cache_user", 36).uniqueIndex() val username = varchar("cache_username", 16).index() @@ -41,6 +51,15 @@ data class TrialInfo( val attempt: Int = 0 ) +data class TestInfo( + val testificate: UUID, + val start: Int, + val end: Int, + val passed: Boolean, + val wrong: Int, + val attempt: Int = 0 +) + fun now() = System.currentTimeMillis().floorDiv(1000).toInt() class Storage( @@ -55,8 +74,8 @@ class Storage( } private fun initTables() = transaction(database) { - SchemaUtils.create( - Note, Trial, UsernameCache + SchemaUtils.createMissingTablesAndColumns( + Note, Trial, UsernameCache, Test ) } @@ -69,6 +88,13 @@ class Storage( }[Trial.id] } + fun insertTest(testificate: UUID): Int = transaction(database) { + Test.insert { + it[Test.testificate] = testificate.toString() + it[start] = now() + }[Test.id] + } + fun endTrial(trialId: Int, passed: Boolean) = transaction(database) { Trial.update({ Trial.id eq trialId}) { it[end] = now() @@ -76,6 +102,14 @@ class Storage( } } + fun endTest(testId: Int, passed: Boolean, wrong: Int) = transaction(database) { + Test.update( {Test.id eq testId}) { + it[end] = now() + it[Test.passed] = passed + it[Test.wrong] = wrong + } + } + fun getTrials(testificate: UUID): List = transaction(database) { Trial.selectAll().where { Trial.testificate eq testificate.toString() @@ -84,6 +118,14 @@ class Storage( } } + fun getTests(testificate: UUID): List = transaction(database) { + Test.selectAll().where { + Test.testificate eq testificate.toString() + }.map { + it[Test.id] + } + } + fun getTrialInfo(trialId: Int): TrialInfo = transaction(database) { val notes = Note.selectAll().where { Note.trial_id eq trialId @@ -102,12 +144,31 @@ class Storage( ) } + fun getTestInfo(testId: Int): TestInfo = transaction(database) { + val resultRow = Test.selectAll().where { + Test.id eq testId + }.firstOrNull() + TestInfo( + UUID.fromString(resultRow!![Test.testificate]), + resultRow[Test.start], + resultRow[Test.end] ?: 0, + resultRow[Test.passed] ?: false, + resultRow[Test.wrong] ?: 0 + ) + } + fun getTrialCount(testificate: UUID): Int = transaction(database) { Trial.selectAll().where { Trial.testificate eq testificate.toString() }.count().toInt() } + fun getTestCount(testificate: UUID): Int = transaction(database) { + Test.selectAll().where { + Test.testificate eq testificate.toString() + }.count().toInt() + } + fun insertNote(trialId: Int, note: String) = transaction(database) { Note.insert { it[trial_id] = trialId diff --git a/src/main/kotlin/org/openredstone/trialore/TestCommand.kt b/src/main/kotlin/org/openredstone/trialore/TestCommand.kt new file mode 100644 index 0000000..112e3f6 --- /dev/null +++ b/src/main/kotlin/org/openredstone/trialore/TestCommand.kt @@ -0,0 +1,98 @@ +package org.openredstone.trialore + +import co.aikar.commands.BaseCommand +import co.aikar.commands.annotation.* +import org.bukkit.entity.Player + +@CommandAlias("test") +@CommandPermission("trialore.test") +class TestCommand( + private val trialORE: TrialOre, +) : BaseCommand() { + + @Default() + @CommandAlias("starttest") + @Subcommand("start") + @Conditions("notTesting") + @Description("Take a test") + fun onStart(player: Player) { + val testificate = player + if (trialORE.testMapping.filter { (uuid) -> + uuid == testificate.uniqueId + }.isNotEmpty()) { + throw TrialOreException("You are already testing. This is 99.9% a Bug. Contact Nick :D") + } + testificate.renderMessage("Starting your test!") + trialORE.startTest(testificate.uniqueId) + } + + @Subcommand("list") + @CommandPermission("trialore.list") + @Description("Get the info of an individual from past tests") + @CommandCompletion("@usernameCache") + fun onList(player: Player, @Single target: String) { + var testificate = trialORE.server.onlinePlayers.firstOrNull { it.name == target }?.uniqueId + if (testificate == null) { + testificate = trialORE.database.usernameToUuidCache[target] + ?: throw TrialOreException("Invalid target $target. Please provide an online player or UUID") + } + val tests = trialORE.database.getTests(testificate) + player.renderMiniMessage("$target has taken ${tests.size} tests") + tests.forEachIndexed { index, test -> + val testInfo = trialORE.database.getTestInfo(test) + val state = if (testInfo.passed) { + "Passed" + } else { + "Failed" + } + val startTime = testInfo.start.toLong() + val timestamp = getRelativeTimestamp(startTime) + val wrong = testInfo.wrong + val correct = 25 - wrong + val percentage = (correct.toDouble() / 25.toDouble()) * 100 + player.renderMiniMessage("${getDate(startTime)}" + + " (State: ${state}), with ${wrong} wrong Answers (${percentage}) '>Test ${index+1}, $timestamp:") + } + } + + @Subcommand("history") + @CommandPermission("trialore.test") + @Description("Get your test history") + fun onHistory(player: Player) { + val testificate = player.uniqueId + val tests = trialORE.database.getTests(testificate) + player.renderMiniMessage("You have taken ${tests.size} tests") + tests.forEachIndexed { index, test -> + val testInfo = trialORE.database.getTestInfo(test) + val state = if (testInfo.passed) { + "Passed" + } else { + "Failed" + } + val startTime = testInfo.start.toLong() + val timestamp = getRelativeTimestamp(startTime) + val wrong = testInfo.wrong + val correct = 25 - wrong + val percentage = (correct.toDouble() / 25.toDouble()) * 100 + player.renderMiniMessage("${getDate(startTime)}" + + " (State: ${state}), with ${wrong} wrong Answers'>Test ${index+1}, $timestamp: $percentage%") + } + } + + @CommandAlias("stoptest") + @Subcommand("stop") + @Conditions("testing") + @Description("Stop a test") + fun onStop(player: Player, testMeta: TestMeta) { + player.renderMessage("You have exited your test") + trialORE.endTest(player.uniqueId, testMeta.testId, false, wrong = 25) + } + + @CommandAlias("testanswer") + @Subcommand("answer") + @Conditions("testing") + @Description("Answer a test question") + fun onAnswer(player: Player, testMeta: TestMeta, answer: String) { + player.renderMessage("Recieved Test-Answer: $answer") + } +} diff --git a/src/main/kotlin/org/openredstone/trialore/TrialOre.kt b/src/main/kotlin/org/openredstone/trialore/TrialOre.kt index ee01063..7e40c67 100644 --- a/src/main/kotlin/org/openredstone/trialore/TrialOre.kt +++ b/src/main/kotlin/org/openredstone/trialore/TrialOre.kt @@ -21,6 +21,7 @@ import org.bukkit.plugin.java.JavaPlugin import java.io.File import java.util.* import java.util.logging.Level +import kotlin.time.Duration.Companion.seconds val VERSION = "1.1" @@ -50,11 +51,17 @@ data class TrialMeta( val trialId: Int ) +data class TestMeta( + val testificate: UUID, + val testId: Int +) + class TrialOre : JavaPlugin(), Listener { lateinit var database: Storage lateinit var luckPerms: LuckPerms lateinit var config: TrialOreConfig val trialMapping: MutableMap> = mutableMapOf() + val testMapping: MutableMap = mutableMapOf() private val mapper = ObjectMapper(YAMLFactory()) override fun onEnable() { loadConfig() @@ -75,13 +82,31 @@ class TrialOre : JavaPlugin(), Listener { throw TrialOreException("You are not trialing anyone") } } + commandConditions.addCondition("notTesting") { + // Condition "notTesting" will fail if the person is taking a test + if (testMapping.containsKey(it.issuer.player.uniqueId)) { + throw TrialOreException("You are already taking a Test") + } + } + commandConditions.addCondition("testing") { + // Condition "notTesting" will fail if the person is taking a test + if (!testMapping.containsKey(it.issuer.player.uniqueId)) { + throw TrialOreException("You are not taking a Test") + } + } commandContexts.registerIssuerOnlyContext(TrialMeta::class.java) { context -> val meta = trialMapping[context.player.uniqueId] ?: throw TrialOreException("Invalid trial mapping. This is likely a bug") TrialMeta(meta.first, meta.second) } + commandContexts.registerIssuerOnlyContext(TestMeta::class.java) { context -> + val meta = testMapping[context.player.uniqueId] + ?: throw TrialOreException("Invalid test mapping. This is likely a bug") + TestMeta(context.player.uniqueId, meta) + } commandCompletions.registerCompletion("usernameCache") { database.usernameToUuidCache.keys } registerCommand(TrialCommand(this@TrialOre, VERSION)) + registerCommand(TestCommand(this@TrialOre)) setDefaultExceptionHandler(::handleCommandException, false) } } @@ -168,6 +193,11 @@ class TrialOre : JavaPlugin(), Listener { }, config.abandonForgiveness) } } + testMapping.forEach { (testtaker, testId) -> + if (uuid == testtaker) { + endTest(testtaker, testId, false, 25) + } + } } fun startTrial(trialer: UUID, testificate: UUID, app: String) { @@ -176,6 +206,11 @@ class TrialOre : JavaPlugin(), Listener { setLpParent(testificate, config.testificateGroup) } + fun startTest(testificate: UUID) { + val testId = this.database.insertTest(testificate) + this.testMapping[testificate] = testId + } + fun endTrial(trialer: UUID, trialId: Int, passed: Boolean, finalNote: String? = null) { if (finalNote != null) { this.database.insertNote(trialId, finalNote) @@ -192,6 +227,12 @@ class TrialOre : JavaPlugin(), Listener { sendReport(database.getTrialInfo(trialId), database.getTrialCount(testificate)) } + fun endTest(testificate: UUID, testId: Int, passed: Boolean, wrong: Int) { + this.database.endTest(testId, passed, wrong) + this.testMapping.remove(testificate) + sendTestReport(database.getTestInfo(testId), database.getTestCount(testificate)) + } + fun getParent(uuid: UUID): String? = luckPerms.userManager.getUser(uuid)?.primaryGroup private fun setLpParent(uuid: UUID, parent: String) { @@ -247,6 +288,35 @@ class TrialOre : JavaPlugin(), Listener { khttp.post(config.webhook, json = payload) } + private fun sendTestReport(testInfo: TestInfo, testCount: Int) { + val correct = 25 - testInfo.wrong + val percentage = (correct.toDouble() / 25.toDouble()) * 100 + val lines = mutableListOf( + "**Testificate**: ${database.uuidToUsernameCache[testInfo.testificate]}", + "**Attempt**: $testCount", + "**Start**: ", + "**End**: ", + "**Wrong**: ${testInfo.wrong}", + "**Percentage**: ${"%.2f".format(percentage)}%" + ) + val payload = mapOf( + "embeds" to listOf( + mapOf( + "title" to database.uuidToUsernameCache[testInfo.testificate], + "description" to lines.joinToString("\n"), + "color" to 0x5fff58, + "fields" to listOf( + mapOf( + "name" to "State", + "value" to "*Passed*" + ) + ) + ) + ) + ) + khttp.post(config.webhook, json = payload) + } + private fun handleCommandException( command: BaseCommand, registeredCommand: RegisteredCommand<*>, From d212f9ab1f396c52685b8090c0c9706208454e42 Mon Sep 17 00:00:00 2001 From: Wueffi Date: Sat, 13 Sep 2025 18:11:18 +0200 Subject: [PATCH 02/13] Actually do the test implementation. --- build.gradle.kts | 11 +- .../org/openredstone/trialore/Storage.kt | 6 + .../org/openredstone/trialore/TestCommand.kt | 53 ++++-- .../org/openredstone/trialore/TrialOre.kt | 173 +++++++++++++++++- src/main/resources/config.yml | 5 + 5 files changed, 221 insertions(+), 27 deletions(-) create mode 100644 src/main/resources/config.yml diff --git a/build.gradle.kts b/build.gradle.kts index fde8137..b61a806 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -47,15 +47,16 @@ java { tasks.shadowJar { relocate("co.aikar.commands", "trialore.acf") relocate("co.aikar.locales", "trialore.locales") + + relocate("com.fasterxml.jackson", "org.openredstone.lib.jackson") dependencies { - exclude( - dependency( - "net.luckperms:api:.*" - ) - ) + exclude(dependency("net.luckperms:api:.*")) + exclude(dependency("io.papermc.paper:paper-api:.*")) } + archiveClassifier.set("") } + tasks.build { dependsOn(tasks.shadowJar) } diff --git a/src/main/kotlin/org/openredstone/trialore/Storage.kt b/src/main/kotlin/org/openredstone/trialore/Storage.kt index 04582af..96e080e 100644 --- a/src/main/kotlin/org/openredstone/trialore/Storage.kt +++ b/src/main/kotlin/org/openredstone/trialore/Storage.kt @@ -169,6 +169,12 @@ class Storage( }.count().toInt() } + fun setTestWrong(testId: Int, wrong: Int) = transaction(database) { + Test.update({ Test.id eq testId }) { + it[Test.wrong] = wrong + } + } + fun insertNote(trialId: Int, note: String) = transaction(database) { Note.insert { it[trial_id] = trialId diff --git a/src/main/kotlin/org/openredstone/trialore/TestCommand.kt b/src/main/kotlin/org/openredstone/trialore/TestCommand.kt index 112e3f6..f5f247a 100644 --- a/src/main/kotlin/org/openredstone/trialore/TestCommand.kt +++ b/src/main/kotlin/org/openredstone/trialore/TestCommand.kt @@ -17,12 +17,11 @@ class TestCommand( @Description("Take a test") fun onStart(player: Player) { val testificate = player - if (trialORE.testMapping.filter { (uuid) -> - uuid == testificate.uniqueId - }.isNotEmpty()) { + if (trialORE.testMapping.containsKey(testificate.uniqueId)) { throw TrialOreException("You are already testing. This is 99.9% a Bug. Contact Nick :D") } testificate.renderMessage("Starting your test!") + testificate.renderMiniMessage("Note: When answering in binary, don't use a prefix like 0b.") trialORE.startTest(testificate.uniqueId) } @@ -40,18 +39,15 @@ class TestCommand( player.renderMiniMessage("$target has taken ${tests.size} tests") tests.forEachIndexed { index, test -> val testInfo = trialORE.database.getTestInfo(test) - val state = if (testInfo.passed) { - "Passed" - } else { - "Failed" + if (testInfo.passed) { + val startTime = testInfo.start.toLong() + val timestamp = getRelativeTimestamp(startTime) + val wrong = testInfo.wrong + val correct = 25 - wrong + val percentage = (correct.toDouble() / 25.toDouble()) * 100 + player.renderMiniMessage("${getDate(startTime)}" + + ", $wrong wrong Answers'>Test: ${index+1} (${percentage}), $timestamp") } - val startTime = testInfo.start.toLong() - val timestamp = getRelativeTimestamp(startTime) - val wrong = testInfo.wrong - val correct = 25 - wrong - val percentage = (correct.toDouble() / 25.toDouble()) * 100 - player.renderMiniMessage("${getDate(startTime)}" + - " (State: ${state}), with ${wrong} wrong Answers (${percentage}) '>Test ${index+1}, $timestamp:") } } @@ -93,6 +89,33 @@ class TestCommand( @Conditions("testing") @Description("Answer a test question") fun onAnswer(player: Player, testMeta: TestMeta, answer: String) { - player.renderMessage("Recieved Test-Answer: $answer") + val session = trialORE.testSessions[player.uniqueId] + ?: throw TrialOreException("No active test session found. This is likely a bug.") + + val expected = session.currentAnswer.trim() + val provided = answer.trim() + + val isCorrect = try { + val expNum = if (expected.matches(Regex("^[01]{1,}$"))) { + Integer.parseInt(expected, 2) + } else Integer.parseInt(expected) + val provNum = if (provided.matches(Regex("^[01]{1,}$"))) { + Integer.parseInt(provided, 2) + } else Integer.parseInt(provided) + expNum == provNum + } catch (e: NumberFormatException) { + expected.equals(provided, ignoreCase = true) + } + + if (isCorrect) { + player.renderMiniMessage("Correct!") + } else { + player.renderMiniMessage("Incorrect Answer. Expected: $expected") + session.wrong++ + trialORE.database.setTestWrong(session.testId, session.wrong) + } + + session.index++ + trialORE.sendNextQuestion(player, session) } } diff --git a/src/main/kotlin/org/openredstone/trialore/TrialOre.kt b/src/main/kotlin/org/openredstone/trialore/TrialOre.kt index 7e40c67..fa2ff95 100644 --- a/src/main/kotlin/org/openredstone/trialore/TrialOre.kt +++ b/src/main/kotlin/org/openredstone/trialore/TrialOre.kt @@ -21,7 +21,6 @@ import org.bukkit.plugin.java.JavaPlugin import java.io.File import java.util.* import java.util.logging.Level -import kotlin.time.Duration.Companion.seconds val VERSION = "1.1" @@ -206,11 +205,6 @@ class TrialOre : JavaPlugin(), Listener { setLpParent(testificate, config.testificateGroup) } - fun startTest(testificate: UUID) { - val testId = this.database.insertTest(testificate) - this.testMapping[testificate] = testId - } - fun endTrial(trialer: UUID, trialId: Int, passed: Boolean, finalNote: String? = null) { if (finalNote != null) { this.database.insertNote(trialId, finalNote) @@ -230,7 +224,7 @@ class TrialOre : JavaPlugin(), Listener { fun endTest(testificate: UUID, testId: Int, passed: Boolean, wrong: Int) { this.database.endTest(testId, passed, wrong) this.testMapping.remove(testificate) - sendTestReport(database.getTestInfo(testId), database.getTestCount(testificate)) + if (passed){ sendTestReport(database.getTestInfo(testId), database.getTestCount(testificate))} } fun getParent(uuid: UUID): String? = luckPerms.userManager.getUser(uuid)?.primaryGroup @@ -333,4 +327,169 @@ class TrialOre : JavaPlugin(), Listener { player.renderMiniMessage("$message") return true } + + data class TestSession( + val testId: Int, + val questions: List, + var index: Int = 0, + var currentAnswer: String = "", + var wrong: Int = 0, + val used: MutableMap> = mutableMapOf() + ) + + val testSessions: MutableMap = mutableMapOf() + private val rand = Random() + + fun startTest(testificate: UUID) { + val testId = this.database.insertTest(testificate) + this.testMapping[testificate] = testId + + val categories = mutableListOf().apply { + repeat(4) { add(1) } + repeat(4) { add(2) } + repeat(8) { add(3) } + repeat(3) { add(4) } + repeat(3) { add(5) } + repeat(3) { add(6) } + } + categories.shuffle(rand) + val session = TestSession(testId, categories) + testSessions[testificate] = session + + server.getPlayer(testificate)?.let { player -> sendNextQuestion(player, session) + } + + } + + fun sendNextQuestion(player: Player, session: TestSession) { + if (session.index >= session.questions.size) { + val passed = session.wrong <= 2 + endTest(player.uniqueId, session.testId, passed, session.wrong) + testSessions.remove(player.uniqueId) + if (passed) { + player.renderMiniMessage("Test finished. Wrong: ${session.wrong}") + player.renderMiniMessage("Use this ID in your Application: ${session.testId}") + } else { + player.renderMiniMessage(" You failed the test. Wrong: ${session.wrong}") + } + return + } + + if (session.wrong >= 3) { + player.renderMiniMessage("You gave ${session.wrong} wrong answers (Fail). You can stop the test by doing /test stop.") + } + val cat = session.questions[session.index] + val (qText, expected) = generateQuestion(cat, session.used) + session.currentAnswer = expected + player.renderMiniMessage("Question ${session.index + 1}/25: $qText") + } + + fun generateQuestion(category: Int, usedPerCategory: MutableMap>): Pair { + val used = usedPerCategory.getOrPut(category) { mutableSetOf() } + fun makeKey(vararg parts: Any) = parts.joinToString(":") + + fun randDecimal(): Int { + val options = (1..14).filterNot { it in listOf(0,1,2,4,8) } + return options[rand.nextInt(options.size)] + } + + fun randTwoSC(): Int { + val options = (-8..7).filter { it != 0 } + return options[rand.nextInt(options.size)] + } + + when(category) { + 1 -> { + var attempts = 0 + while (attempts++ < 200) { + val value = randDecimal() + val bin = value.toString(2).padStart(4,'0') + val key = makeKey("conv-bin->dec", bin) + if (key in used) continue + used.add(key) + return Pair("Convert binary $bin to decimal", value.toString()) + } + return Pair("Convert binary 0101 to decimal", "5") + } + + 2 -> { + var attempts = 0 + while (attempts++ < 200) { + val value = randDecimal() + val bin = value.toString(2).padStart(4,'0') + val key = makeKey("conv-dec->bin", value) + if (key in used) continue + used.add(key) + return Pair("Convert decimal $value to 4-bit binary", bin) + } + return Pair("Convert decimal 5 to 4-bit binary", "0101") + } + + 3 -> { + val gates = listOf("AND","NAND","OR","NOR","XOR","XNOR") + val gateIndex = used.size % gates.size + val op = if (gateIndex < 4) gates[gateIndex] else gates[rand.nextInt(gates.size)] + val a = rand.nextInt(1,15) + var b = rand.nextInt(1,15) + if (b == a) b = (b % 14) + 1 + val aBin = (a and 0xF).toString(2).padStart(4,'0') + val bBin = (b and 0xF).toString(2).padStart(4,'0') + val result = when(op){ + "AND" -> a and b + "NAND" -> (a and b) xor 0xF + "OR" -> a or b + "NOR" -> (a or b) xor 0xF + "XOR" -> a xor b + "XNOR" -> (a xor b) xor 0xF + else -> 0 + } and 0xF + val ansBin = result.toString(2).padStart(4,'0') + used.add(makeKey("gate",op,aBin,bBin)) + return Pair("Apply $op to $aBin and $bBin — give the 4-bit binary result", ansBin) + } + + 4 -> { + var attempts = 0 + while (attempts++ < 200) { + val value = randTwoSC() + val twos = (value and 0xF).toString(2).padStart(4,'0') + val key = makeKey("to-2sc",value) + if (key in used) continue + used.add(key) + return Pair("Write $value as 4-bit two's complement (2sc) binary", twos) + } + return Pair("Write -2 as 4-bit two's complement (2sc) binary", "1110") + } + + 5 -> { + var attempts = 0 + while (attempts++ < 200) { + val x = randTwoSC() and 0xF + val signed = if(x and 0x8 !=0) x-16 else x + val bin = x.toString(2).padStart(4,'0') + val key = makeKey("from-2sc",bin) + if(key in used) continue + used.add(key) + return Pair("What is 4-bit two's complement $bin equal to in decimal?", signed.toString()) + } + return Pair("What is 4-bit two's complement 1110 equal to in decimal?","-2") + } + + 6 -> { + var attempts = 0 + while(attempts++ < 200){ + val v = rand.nextInt(1,9) + val neg = (-v) and 0xF + val negBin = neg.toString(2).padStart(4,'0') + val key = makeKey("neg-2sc",v) + if(key in used) continue + used.add(key) + return Pair("What is the 2's complement (4-bit) representation of -$v ?",negBin) + } + return Pair("What is the 2's complement (4-bit) representation of -5 ?","1011") + } + + else -> return Pair("Invalid category","") + } + } } \ No newline at end of file diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml new file mode 100644 index 0000000..54ab3f8 --- /dev/null +++ b/src/main/resources/config.yml @@ -0,0 +1,5 @@ +studentGroup: "student" +testificateGroup: "testificate" +builderGroup: "builder" +webhook: "https://discord.com/api/webhooks/XXXX/XXXXXXXXXXXXXXXX" +abandonForgiveness: 6000 \ No newline at end of file From 19c217ddd2a6cce114f5e7acdfe9e20be7360c2e Mon Sep 17 00:00:00 2001 From: Wueffi Date: Sat, 13 Sep 2025 18:42:29 +0200 Subject: [PATCH 03/13] Add Warns for suspiciously fast done tests and set a Limit to 3 tests every 24 hours. --- .../org/openredstone/trialore/TestCommand.kt | 48 +++++++++++++++++-- .../org/openredstone/trialore/TrialOre.kt | 5 +- 2 files changed, 48 insertions(+), 5 deletions(-) diff --git a/src/main/kotlin/org/openredstone/trialore/TestCommand.kt b/src/main/kotlin/org/openredstone/trialore/TestCommand.kt index f5f247a..8c24be1 100644 --- a/src/main/kotlin/org/openredstone/trialore/TestCommand.kt +++ b/src/main/kotlin/org/openredstone/trialore/TestCommand.kt @@ -17,14 +17,28 @@ class TestCommand( @Description("Take a test") fun onStart(player: Player) { val testificate = player + if (trialORE.testMapping.containsKey(testificate.uniqueId)) { throw TrialOreException("You are already testing. This is 99.9% a Bug. Contact Nick :D") } + + val now = System.currentTimeMillis() + val tests = trialORE.database.getTests(testificate.uniqueId) + val lastThreeTests = tests.takeLast(3) + val lastThreeWithin24h = lastThreeTests.all { test -> + val startTime = trialORE.database.getTestInfo(test).start.toLong() + now - startTime <= 24 * 60 * 60 * 1000 + } + if (lastThreeWithin24h && lastThreeTests.size == 3) { + player.renderMiniMessage("Warning: Your last 3 tests were all taken within the last 24 hours! Do /test history to see them.") + return + } testificate.renderMessage("Starting your test!") testificate.renderMiniMessage("Note: When answering in binary, don't use a prefix like 0b.") trialORE.startTest(testificate.uniqueId) } + @Subcommand("list") @CommandPermission("trialore.list") @Description("Get the info of an individual from past tests") @@ -45,12 +59,38 @@ class TestCommand( val wrong = testInfo.wrong val correct = 25 - wrong val percentage = (correct.toDouble() / 25.toDouble()) * 100 + val duration = testInfo.end.toLong() - startTime + var rotatingLight = "" + if (duration <= 45 * 1000) { rotatingLight = ":rotating_light: :rotating_light: :rotating_light: Test done in ${duration}s"} player.renderMiniMessage("${getDate(startTime)}" + - ", $wrong wrong Answers'>Test: ${index+1} (${percentage}), $timestamp") + ", $wrong wrong Answers'>Test: ${index+1} (${percentage}), $timestamp $rotatingLight") } } } + @Subcommand("info") + @CommandPermission("trialore.list") + @Description("Get a test from an ID") + fun onInfo(player: Player, @Single id: Int) { + val testInfo = trialORE.database.getTestInfo(id) + val startTime = testInfo.start.toLong() + val duration = testInfo.end.toLong() - startTime + var rotatingLight = "" + if (duration <= 45 * 1000) { rotatingLight = ":rotating_light: :rotating_light: :rotating_light: Test done in ${duration}s"} + val timestamp = getRelativeTimestamp(startTime) + val wrong = testInfo.wrong + val testificate = testInfo.testificate + val correct = 25 - wrong + val percentage = (correct.toDouble() / 25.toDouble()) * 100 + if (testInfo.passed) { + player.renderMiniMessage("Passed! At ${getDate(startTime)} by $testificate" + + ", $wrong wrong Answers'>Test: $id (${percentage}), $timestamp $rotatingLight") + } else { + player.renderMiniMessage("Failed! At ${getDate(startTime)} by $testificate" + + ", $wrong wrong Answers'>Test: $id (${percentage}), $timestamp") + } + } + @Subcommand("history") @CommandPermission("trialore.test") @Description("Get your test history") @@ -58,8 +98,8 @@ class TestCommand( val testificate = player.uniqueId val tests = trialORE.database.getTests(testificate) player.renderMiniMessage("You have taken ${tests.size} tests") - tests.forEachIndexed { index, test -> - val testInfo = trialORE.database.getTestInfo(test) + tests.forEachIndexed { index, testid -> + val testInfo = trialORE.database.getTestInfo(testid) val state = if (testInfo.passed) { "Passed" } else { @@ -71,7 +111,7 @@ class TestCommand( val correct = 25 - wrong val percentage = (correct.toDouble() / 25.toDouble()) * 100 player.renderMiniMessage("${getDate(startTime)}" + - " (State: ${state}), with ${wrong} wrong Answers'>Test ${index+1}, $timestamp: $percentage%") + " (State: ${state}), with ${wrong} wrong Answers'>Test ${index+1} (ID: $testid), $timestamp: $percentage%") } } diff --git a/src/main/kotlin/org/openredstone/trialore/TrialOre.kt b/src/main/kotlin/org/openredstone/trialore/TrialOre.kt index fa2ff95..6216644 100644 --- a/src/main/kotlin/org/openredstone/trialore/TrialOre.kt +++ b/src/main/kotlin/org/openredstone/trialore/TrialOre.kt @@ -285,13 +285,16 @@ class TrialOre : JavaPlugin(), Listener { private fun sendTestReport(testInfo: TestInfo, testCount: Int) { val correct = 25 - testInfo.wrong val percentage = (correct.toDouble() / 25.toDouble()) * 100 + var rotatingLight = "" + if (testInfo.end - testInfo.start <= 45 * 1000 ) { rotatingLight = ":rotating_light: :rotating_light: :rotating_light: <45 Seconds!!" } val lines = mutableListOf( "**Testificate**: ${database.uuidToUsernameCache[testInfo.testificate]}", "**Attempt**: $testCount", "**Start**: ", "**End**: ", "**Wrong**: ${testInfo.wrong}", - "**Percentage**: ${"%.2f".format(percentage)}%" + "**Percentage**: ${"%.2f".format(percentage)}%", + rotatingLight ) val payload = mapOf( "embeds" to listOf( From 47724b14bf51ba7693775721f52eef9ffc5c09a6 Mon Sep 17 00:00:00 2001 From: Wueffi Date: Sat, 13 Sep 2025 18:50:49 +0200 Subject: [PATCH 04/13] Some documentation --- README.md | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 1e7a974..9f62593 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,23 @@ # TrialORE -ORE's trial management plugin +ORE's trial management and test plugin + +## Test Command Usage + +| Command | Alias | Description | +|------------------------------------|------------------------|-----------------------------------------| +| `/test start` | `/starttest` | Start a Test | +| `/test history` | | Shows your test history | +| `/test list [user]` | | Lists the passed tests of an individual | +| `/test stop ` | `/stoptest` | Stop your current test run | +| `/test answer [answer]` | `/testanswer [answer]` | Answer a question of the test. | + +## Doing a test +1. Start the test using `/test start`. +2. Once you get asked a question run `/test answer [answer]`. \ +The answer should be the answer you think is correct without any prefix. For example it should not be `0b1111` but `1111`. +3. After you finished the test and passed you get a test-id. Paste it in your App. \ +Staff are going to use this to verify your test. ## Full Command Usage From c9e6018940a730ff97142fc4ddbcefa580067507 Mon Sep 17 00:00:00 2001 From: Waffle <151867877+Wueffi@users.noreply.github.com> Date: Sat, 13 Sep 2025 18:53:12 +0200 Subject: [PATCH 05/13] Update README.md --- README.md | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 9f62593..2d43c7a 100644 --- a/README.md +++ b/README.md @@ -4,13 +4,14 @@ ORE's trial management and test plugin ## Test Command Usage -| Command | Alias | Description | -|------------------------------------|------------------------|-----------------------------------------| -| `/test start` | `/starttest` | Start a Test | -| `/test history` | | Shows your test history | -| `/test list [user]` | | Lists the passed tests of an individual | -| `/test stop ` | `/stoptest` | Stop your current test run | -| `/test answer [answer]` | `/testanswer [answer]` | Answer a question of the test. | +| Command | Alias | Description | +|------------------------------------|------------------------|----------------------------------------------| +| `/test start` | `/starttest` | Start a Test | +| `/test history` | | Shows your test history | +| `/test list [user]` | | Lists the passed tests of an individual | +| `/test stop ` | `/stoptest` | Stop your current test run | +| `/test info [id]` | | Check if the test with the given id is valid | +| `/test answer [answer]` | `/testanswer [answer]` | Answer a question of the test. | ## Doing a test 1. Start the test using `/test start`. @@ -50,4 +51,4 @@ This "abandonment" period defaults to 5 minutes. If a Testificate leaves, they will be demoted to Student and given 5 minutes to rejoin. If they do not rejoin within those 5 minutes, the trial ends. If a trialer leaves, the trial will automatically end in 5 minutes if they do not rejoin, demoting the Testificate to Student. -The only distinction of an abandoned trial is if the trial ends with an automated abandonment note. \ No newline at end of file +The only distinction of an abandoned trial is if the trial ends with an automated abandonment note. From ea7525682a3d65cb2d3d04468ebdcad0a4bd2c01 Mon Sep 17 00:00:00 2001 From: Wueffi Date: Sat, 13 Sep 2025 19:03:15 +0200 Subject: [PATCH 06/13] Add TestInfo null checks :D --- .../org/openredstone/trialore/Storage.kt | 9 ++++----- .../org/openredstone/trialore/TestCommand.kt | 19 ++++++++++++++++++- .../org/openredstone/trialore/TrialOre.kt | 3 ++- 3 files changed, 24 insertions(+), 7 deletions(-) diff --git a/src/main/kotlin/org/openredstone/trialore/Storage.kt b/src/main/kotlin/org/openredstone/trialore/Storage.kt index 96e080e..946778c 100644 --- a/src/main/kotlin/org/openredstone/trialore/Storage.kt +++ b/src/main/kotlin/org/openredstone/trialore/Storage.kt @@ -2,6 +2,7 @@ package org.openredstone.trialore import org.jetbrains.exposed.sql.* import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq +import org.jetbrains.exposed.sql.selectAll import org.jetbrains.exposed.sql.transactions.transaction import java.util.* @@ -144,12 +145,10 @@ class Storage( ) } - fun getTestInfo(testId: Int): TestInfo = transaction(database) { - val resultRow = Test.selectAll().where { - Test.id eq testId - }.firstOrNull() + fun getTestInfo(testId: Int): TestInfo? = transaction(database) { + val resultRow = Test.selectAll().where { Test.id eq testId }.firstOrNull() ?: return@transaction null TestInfo( - UUID.fromString(resultRow!![Test.testificate]), + UUID.fromString(resultRow[Test.testificate]), resultRow[Test.start], resultRow[Test.end] ?: 0, resultRow[Test.passed] ?: false, diff --git a/src/main/kotlin/org/openredstone/trialore/TestCommand.kt b/src/main/kotlin/org/openredstone/trialore/TestCommand.kt index 8c24be1..39c3db4 100644 --- a/src/main/kotlin/org/openredstone/trialore/TestCommand.kt +++ b/src/main/kotlin/org/openredstone/trialore/TestCommand.kt @@ -26,7 +26,12 @@ class TestCommand( val tests = trialORE.database.getTests(testificate.uniqueId) val lastThreeTests = tests.takeLast(3) val lastThreeWithin24h = lastThreeTests.all { test -> - val startTime = trialORE.database.getTestInfo(test).start.toLong() + val testInfo = trialORE.database.getTestInfo(test) + if (testInfo == null) { + player.sendMessage("Test id was null. Report this to Staff.") + return + } + val startTime = testInfo.start.toLong() now - startTime <= 24 * 60 * 60 * 1000 } if (lastThreeWithin24h && lastThreeTests.size == 3) { @@ -53,6 +58,10 @@ class TestCommand( player.renderMiniMessage("$target has taken ${tests.size} tests") tests.forEachIndexed { index, test -> val testInfo = trialORE.database.getTestInfo(test) + if (testInfo == null) { + player.sendMessage("Test id was null. If you are Nick, have fun. If not, report to Nick.") + return + } if (testInfo.passed) { val startTime = testInfo.start.toLong() val timestamp = getRelativeTimestamp(startTime) @@ -73,6 +82,10 @@ class TestCommand( @Description("Get a test from an ID") fun onInfo(player: Player, @Single id: Int) { val testInfo = trialORE.database.getTestInfo(id) + if (testInfo == null) { + player.sendMessage("Test id was null. If you are Nick, have fun. If not, report to Nick.") + return + } val startTime = testInfo.start.toLong() val duration = testInfo.end.toLong() - startTime var rotatingLight = "" @@ -100,6 +113,10 @@ class TestCommand( player.renderMiniMessage("You have taken ${tests.size} tests") tests.forEachIndexed { index, testid -> val testInfo = trialORE.database.getTestInfo(testid) + if (testInfo == null) { + player.sendMessage("Test id was null. Report this to Staff.") + return + } val state = if (testInfo.passed) { "Passed" } else { diff --git a/src/main/kotlin/org/openredstone/trialore/TrialOre.kt b/src/main/kotlin/org/openredstone/trialore/TrialOre.kt index 6216644..5432d05 100644 --- a/src/main/kotlin/org/openredstone/trialore/TrialOre.kt +++ b/src/main/kotlin/org/openredstone/trialore/TrialOre.kt @@ -224,7 +224,8 @@ class TrialOre : JavaPlugin(), Listener { fun endTest(testificate: UUID, testId: Int, passed: Boolean, wrong: Int) { this.database.endTest(testId, passed, wrong) this.testMapping.remove(testificate) - if (passed){ sendTestReport(database.getTestInfo(testId), database.getTestCount(testificate))} + val testInfo = database.getTestInfo(testId) + if (passed && testInfo != null){ sendTestReport(testInfo, database.getTestCount(testificate))} } fun getParent(uuid: UUID): String? = luckPerms.userManager.getUser(uuid)?.primaryGroup From 47edd7c8de05ccb1c55d9928425e1b9c58fd92ff Mon Sep 17 00:00:00 2001 From: Wueffi Date: Sat, 13 Sep 2025 21:19:28 +0200 Subject: [PATCH 07/13] Add permissions --- README.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 2d43c7a..11df0c5 100644 --- a/README.md +++ b/README.md @@ -4,14 +4,14 @@ ORE's trial management and test plugin ## Test Command Usage -| Command | Alias | Description | -|------------------------------------|------------------------|----------------------------------------------| -| `/test start` | `/starttest` | Start a Test | -| `/test history` | | Shows your test history | -| `/test list [user]` | | Lists the passed tests of an individual | -| `/test stop ` | `/stoptest` | Stop your current test run | -| `/test info [id]` | | Check if the test with the given id is valid | -| `/test answer [answer]` | `/testanswer [answer]` | Answer a question of the test. | +| Command | Alias | Permission | Description | +|------------------------------------|------------------------|---------------|----------------------------------------------| +| `/test start` | `/starttest` | trialore.test | Start a Test | +| `/test history` | | trialore.test | Shows your test history | +| `/test list [user]` | | trialore.list | Lists the passed tests of an individual | +| `/test stop ` | `/stoptest` | trialore.test | Stop your current test run | +| `/test info [id]` | | trialore.list | Check if the test with the given id is valid | +| `/test answer [answer]` | `/testanswer [answer]` | trialore.test | Answer a question of the test. | ## Doing a test 1. Start the test using `/test start`. From a6ecbf462979eb836911a6b8841cc87d85d8fb30 Mon Sep 17 00:00:00 2001 From: Waffle <151867877+Wueffi@users.noreply.github.com> Date: Sun, 14 Sep 2025 10:33:35 +0200 Subject: [PATCH 08/13] Minor ReadMe Corrections --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 11df0c5..2917f5d 100644 --- a/README.md +++ b/README.md @@ -6,12 +6,12 @@ ORE's trial management and test plugin | Command | Alias | Permission | Description | |------------------------------------|------------------------|---------------|----------------------------------------------| -| `/test start` | `/starttest` | trialore.test | Start a Test | +| `/test start` | `/starttest` | trialore.test | Start a test | | `/test history` | | trialore.test | Shows your test history | | `/test list [user]` | | trialore.list | Lists the passed tests of an individual | | `/test stop ` | `/stoptest` | trialore.test | Stop your current test run | -| `/test info [id]` | | trialore.list | Check if the test with the given id is valid | -| `/test answer [answer]` | `/testanswer [answer]` | trialore.test | Answer a question of the test. | +| `/test info [id]` | | trialore.list | Show all info of a test with the given ID | +| `/test answer [answer]` | `/testanswer [answer]` | trialore.test | Answer a question of the test | ## Doing a test 1. Start the test using `/test start`. From 9af879bfffaf258260c309295c590c5b272a1b7e Mon Sep 17 00:00:00 2001 From: Wueffi Date: Sun, 14 Sep 2025 14:32:34 +0200 Subject: [PATCH 09/13] Add /test check, add send failed tests and also stop taking the test twice --- README.md | 17 +++--- .../org/openredstone/trialore/TestCommand.kt | 60 +++++++++++++++---- .../org/openredstone/trialore/TrialOre.kt | 19 +++--- src/main/resources/config.yml | 3 +- 4 files changed, 70 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index 2917f5d..6479625 100644 --- a/README.md +++ b/README.md @@ -4,14 +4,15 @@ ORE's trial management and test plugin ## Test Command Usage -| Command | Alias | Permission | Description | -|------------------------------------|------------------------|---------------|----------------------------------------------| -| `/test start` | `/starttest` | trialore.test | Start a test | -| `/test history` | | trialore.test | Shows your test history | -| `/test list [user]` | | trialore.list | Lists the passed tests of an individual | -| `/test stop ` | `/stoptest` | trialore.test | Stop your current test run | -| `/test info [id]` | | trialore.list | Show all info of a test with the given ID | -| `/test answer [answer]` | `/testanswer [answer]` | trialore.test | Answer a question of the test | +| Command | Alias | Permission | Description | +|-------------------------|---------------|---------------|-------------------------------------------| +| `/test start` | `/starttest` | trialore.test | Start a test | +| `/test history` | | trialore.test | Shows your test history | +| `/test list [user]` | | trialore.list | Lists the passed tests of an individual | +| `/test stop ` | `/stoptest` | trialore.test | Stop your current test run | +| `/test info [id]` | | trialore.list | Show all info of a test with the given ID | +| `/test check [user]` | `/check` | trialore.list | Check if a user passed the test | +| `/test answer [answer]` | `/testanswer` | trialore.test | Answer a question of the test | ## Doing a test 1. Start the test using `/test start`. diff --git a/src/main/kotlin/org/openredstone/trialore/TestCommand.kt b/src/main/kotlin/org/openredstone/trialore/TestCommand.kt index 39c3db4..98e6dd5 100644 --- a/src/main/kotlin/org/openredstone/trialore/TestCommand.kt +++ b/src/main/kotlin/org/openredstone/trialore/TestCommand.kt @@ -2,8 +2,10 @@ package org.openredstone.trialore import co.aikar.commands.BaseCommand import co.aikar.commands.annotation.* +import org.bukkit.Bukkit import org.bukkit.entity.Player + @CommandAlias("test") @CommandPermission("trialore.test") class TestCommand( @@ -21,9 +23,16 @@ class TestCommand( if (trialORE.testMapping.containsKey(testificate.uniqueId)) { throw TrialOreException("You are already testing. This is 99.9% a Bug. Contact Nick :D") } + val tests = trialORE.database.getTests(testificate.uniqueId) + tests.forEachIndexed { index, testid -> + val testInfo = trialORE.database.getTestInfo(testid) + if (testInfo?.passed ?: false) { + player.renderMiniMessage("You already passed the test!") + return + } + } val now = System.currentTimeMillis() - val tests = trialORE.database.getTests(testificate.uniqueId) val lastThreeTests = tests.takeLast(3) val lastThreeWithin24h = lastThreeTests.all { test -> val testInfo = trialORE.database.getTestInfo(test) @@ -70,38 +79,59 @@ class TestCommand( val percentage = (correct.toDouble() / 25.toDouble()) * 100 val duration = testInfo.end.toLong() - startTime var rotatingLight = "" - if (duration <= 45 * 1000) { rotatingLight = ":rotating_light: :rotating_light: :rotating_light: Test done in ${duration}s"} + if (duration <= 45) { rotatingLight = ":rotating_light: :rotating_light: :rotating_light: Test done in ${duration}s"} player.renderMiniMessage("${getDate(startTime)}" + - ", $wrong wrong Answers'>Test: ${index+1} (${percentage}), $timestamp $rotatingLight") + ", $wrong wrong Answers'>Test: ${index+1} (${percentage}), $timestamp $rotatingLight") } } } @Subcommand("info") @CommandPermission("trialore.list") - @Description("Get a test from an ID") + @Description("Check a user's test") fun onInfo(player: Player, @Single id: Int) { val testInfo = trialORE.database.getTestInfo(id) if (testInfo == null) { - player.sendMessage("Test id was null. If you are Nick, have fun. If not, report to Nick.") + player.sendMessage("Test not existant. If you are Nick, have fun. If you believe this is an error, report to Nick.") return } val startTime = testInfo.start.toLong() val duration = testInfo.end.toLong() - startTime var rotatingLight = "" - if (duration <= 45 * 1000) { rotatingLight = ":rotating_light: :rotating_light: :rotating_light: Test done in ${duration}s"} + if (duration <= 45) { rotatingLight = ":rotating_light: :rotating_light: :rotating_light: Test done in ${duration}s"} val timestamp = getRelativeTimestamp(startTime) val wrong = testInfo.wrong val testificate = testInfo.testificate val correct = 25 - wrong val percentage = (correct.toDouble() / 25.toDouble()) * 100 if (testInfo.passed) { - player.renderMiniMessage("Passed! At ${getDate(startTime)} by $testificate" + - ", $wrong wrong Answers'>Test: $id (${percentage}), $timestamp $rotatingLight") + player.renderMiniMessage("${getDate(startTime)} by $testificate" + + ", $wrong wrong Answers'>Passed! Test: $id (${percentage}), $timestamp $rotatingLight") } else { player.renderMiniMessage("Failed! At ${getDate(startTime)} by $testificate" + - ", $wrong wrong Answers'>Test: $id (${percentage}), $timestamp") + ", $wrong wrong Answers'>Failed! Test: $id (${percentage}), $timestamp") + } + } + + @Subcommand("check") + @CommandPermission("trialore.list") + @Description("Check if a User passed the test") + @CommandAlias("check") + fun onCheck(player: Player, target: String) { + val testificate = Bukkit.getOfflinePlayer(target) + val tests = trialORE.database.getTests(testificate.uniqueId) + if (tests.isEmpty()) { + player.renderMiniMessage("Target has not been in any test.") + return + } + tests.forEachIndexed { index, testid -> + val testInfo = trialORE.database.getTestInfo(testid) + if (testInfo?.passed ?: false) { + player.renderMiniMessage("Target passed the test!") + return + } } + player.renderMiniMessage("Target has failed all their tests!") } @Subcommand("history") @@ -117,10 +147,14 @@ class TestCommand( player.sendMessage("Test id was null. Report this to Staff.") return } - val state = if (testInfo.passed) { - "Passed" + var state = "" + var color = "" + if (testInfo.passed) { + state = "Passed" + color = "" } else { - "Failed" + state = "Failed" + color = "" } val startTime = testInfo.start.toLong() val timestamp = getRelativeTimestamp(startTime) @@ -128,7 +162,7 @@ class TestCommand( val correct = 25 - wrong val percentage = (correct.toDouble() / 25.toDouble()) * 100 player.renderMiniMessage("${getDate(startTime)}" + - " (State: ${state}), with ${wrong} wrong Answers'>Test ${index+1} (ID: $testid), $timestamp: $percentage%") + " (State: $color${state}), with ${wrong} wrong Answers'>Test ${index+1} (ID: $testid), $timestamp: $color$percentage%") } } diff --git a/src/main/kotlin/org/openredstone/trialore/TrialOre.kt b/src/main/kotlin/org/openredstone/trialore/TrialOre.kt index 5432d05..535e32c 100644 --- a/src/main/kotlin/org/openredstone/trialore/TrialOre.kt +++ b/src/main/kotlin/org/openredstone/trialore/TrialOre.kt @@ -42,7 +42,8 @@ data class TrialOreConfig( val testificateGroup: String = "testificate", val builderGroup: String = "builder", val webhook: String = "webhook", - val abandonForgiveness: Long = 6000 + val abandonForgiveness: Long = 6000, + val sendFailedTests: Boolean = true ) data class TrialMeta( @@ -225,7 +226,9 @@ class TrialOre : JavaPlugin(), Listener { this.database.endTest(testId, passed, wrong) this.testMapping.remove(testificate) val testInfo = database.getTestInfo(testId) - if (passed && testInfo != null){ sendTestReport(testInfo, database.getTestCount(testificate))} + if (testInfo?.wrong == 25) { return } + if ((config.sendFailedTests || passed) && testInfo != null) + sendTestReport(testInfo, database.getTestCount(testificate)) } fun getParent(uuid: UUID): String? = luckPerms.userManager.getUser(uuid)?.primaryGroup @@ -297,16 +300,21 @@ class TrialOre : JavaPlugin(), Listener { "**Percentage**: ${"%.2f".format(percentage)}%", rotatingLight ) + val color = if(testInfo.passed) { + 0x5fff58 + } else { + 0xff5858 + } val payload = mapOf( "embeds" to listOf( mapOf( "title" to database.uuidToUsernameCache[testInfo.testificate], "description" to lines.joinToString("\n"), - "color" to 0x5fff58, + "color" to color, "fields" to listOf( mapOf( "name" to "State", - "value" to "*Passed*" + "value" to testInfo.passed ) ) ) @@ -379,9 +387,6 @@ class TrialOre : JavaPlugin(), Listener { return } - if (session.wrong >= 3) { - player.renderMiniMessage("You gave ${session.wrong} wrong answers (Fail). You can stop the test by doing /test stop.") - } val cat = session.questions[session.index] val (qText, expected) = generateQuestion(cat, session.used) session.currentAnswer = expected diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index 54ab3f8..91841b7 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -2,4 +2,5 @@ studentGroup: "student" testificateGroup: "testificate" builderGroup: "builder" webhook: "https://discord.com/api/webhooks/XXXX/XXXXXXXXXXXXXXXX" -abandonForgiveness: 6000 \ No newline at end of file +abandonForgiveness: 6000 +sendFailedTests: true \ No newline at end of file From 079f821a43fdd7e147166b2643ba6d1a11b2e58f Mon Sep 17 00:00:00 2001 From: Wueffi Date: Sun, 14 Sep 2025 18:39:45 +0200 Subject: [PATCH 10/13] Last commit before PR --- .../org/openredstone/trialore/TestCommand.kt | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/src/main/kotlin/org/openredstone/trialore/TestCommand.kt b/src/main/kotlin/org/openredstone/trialore/TestCommand.kt index 98e6dd5..b64ec01 100644 --- a/src/main/kotlin/org/openredstone/trialore/TestCommand.kt +++ b/src/main/kotlin/org/openredstone/trialore/TestCommand.kt @@ -34,18 +34,20 @@ class TestCommand( val now = System.currentTimeMillis() val lastThreeTests = tests.takeLast(3) - val lastThreeWithin24h = lastThreeTests.all { test -> - val testInfo = trialORE.database.getTestInfo(test) - if (testInfo == null) { - player.sendMessage("Test id was null. Report this to Staff.") + if (lastThreeTests.size == 3) { + val lastThreeWithin24h = lastThreeTests.all { test -> + val testInfo = trialORE.database.getTestInfo(test) + ?: run { + player.sendMessage("Test id was null. Report this to Staff.") + return@all false + } + val startTimeMs = testInfo.start.toLong() * 1000L + now - startTimeMs <= 24 * 60 * 60 * 1000L + } + if (lastThreeWithin24h) { + player.renderMiniMessage("Warning: Your last 3 tests were all taken within the last 24 hours! Do /test history to see them.") return } - val startTime = testInfo.start.toLong() - now - startTime <= 24 * 60 * 60 * 1000 - } - if (lastThreeWithin24h && lastThreeTests.size == 3) { - player.renderMiniMessage("Warning: Your last 3 tests were all taken within the last 24 hours! Do /test history to see them.") - return } testificate.renderMessage("Starting your test!") testificate.renderMiniMessage("Note: When answering in binary, don't use a prefix like 0b.") From 6a01c51b21c1d9ee9a997d09488df5885a71b2b5 Mon Sep 17 00:00:00 2001 From: Wueffi Date: Sun, 14 Sep 2025 18:39:45 +0200 Subject: [PATCH 11/13] Last commit before PR --- src/main/kotlin/org/openredstone/trialore/TrialOre.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/kotlin/org/openredstone/trialore/TrialOre.kt b/src/main/kotlin/org/openredstone/trialore/TrialOre.kt index 535e32c..808a18c 100644 --- a/src/main/kotlin/org/openredstone/trialore/TrialOre.kt +++ b/src/main/kotlin/org/openredstone/trialore/TrialOre.kt @@ -379,8 +379,7 @@ class TrialOre : JavaPlugin(), Listener { endTest(player.uniqueId, session.testId, passed, session.wrong) testSessions.remove(player.uniqueId) if (passed) { - player.renderMiniMessage("Test finished. Wrong: ${session.wrong}") - player.renderMiniMessage("Use this ID in your Application: ${session.testId}") + player.renderMiniMessage("Test finished(${session.testId}). Wrong: ${session.wrong}") } else { player.renderMiniMessage(" You failed the test. Wrong: ${session.wrong}") } From 6d430e5c6a657f5915c26256a37f1f96168b1ada Mon Sep 17 00:00:00 2001 From: Wueffi Date: Mon, 15 Sep 2025 14:14:54 +0200 Subject: [PATCH 12/13] Add a warning if you want to trial a person without a test. --- .../org/openredstone/trialore/Storage.kt | 10 +++-- .../org/openredstone/trialore/TrialCommand.kt | 39 +++++++++++-------- src/main/resources/plugin.yml | 2 +- 3 files changed, 30 insertions(+), 21 deletions(-) diff --git a/src/main/kotlin/org/openredstone/trialore/Storage.kt b/src/main/kotlin/org/openredstone/trialore/Storage.kt index 946778c..a752ef5 100644 --- a/src/main/kotlin/org/openredstone/trialore/Storage.kt +++ b/src/main/kotlin/org/openredstone/trialore/Storage.kt @@ -30,8 +30,8 @@ object Test : Table("test") { val testificate = varchar("testificate", 36).index() val start = integer("start") val end = integer("end").nullable() - val passed = bool("passed").nullable() - val wrong = integer("wrong").nullable() + val passed = bool("passed") + val wrong = integer("wrong") override val primaryKey = PrimaryKey(id) } @@ -93,6 +93,8 @@ class Storage( Test.insert { it[Test.testificate] = testificate.toString() it[start] = now() + it[Test.passed] = false + it[Test.wrong] = 25 }[Test.id] } @@ -151,8 +153,8 @@ class Storage( UUID.fromString(resultRow[Test.testificate]), resultRow[Test.start], resultRow[Test.end] ?: 0, - resultRow[Test.passed] ?: false, - resultRow[Test.wrong] ?: 0 + resultRow[Test.passed], + resultRow[Test.wrong] ) } diff --git a/src/main/kotlin/org/openredstone/trialore/TrialCommand.kt b/src/main/kotlin/org/openredstone/trialore/TrialCommand.kt index 995641c..010dcd6 100644 --- a/src/main/kotlin/org/openredstone/trialore/TrialCommand.kt +++ b/src/main/kotlin/org/openredstone/trialore/TrialCommand.kt @@ -82,23 +82,30 @@ class TrialCommand( fun onStart(player: Player, @Single target: String, @Single app: String) { val testificate = trialORE.server.onlinePlayers.firstOrNull { it.name == target } ?: throw TrialOreException("That individual is not online and cannot be trialed") - if (trialORE.trialMapping.filter { (_, meta) -> - meta.first == testificate.uniqueId - }.isNotEmpty()) { - throw TrialOreException("That individual is already trialing") - } - if (trialORE.getParent(testificate.uniqueId) != trialORE.config.studentGroup) { - throw TrialOreException("That individual is ineligible for trial due to rank") - } - if (player.uniqueId == testificate.uniqueId) { - throw TrialOreException("You cannot trial yourself") - } - if (!app.startsWith("https://discourse.openredstone.org/")) { - throw TrialOreException("Invalid app: $app") + val tests = trialORE.database.getTests(testificate.uniqueId) + tests.forEachIndexed { index, testid -> + val testInfo = trialORE.database.getTestInfo(testid) + if (testInfo?.passed ?: false) { + if (trialORE.trialMapping.filter { (_, meta) -> + meta.first == testificate.uniqueId + }.isNotEmpty()) { + throw TrialOreException("That individual is already trialing") + } + if (trialORE.getParent(testificate.uniqueId) != trialORE.config.studentGroup) { + throw TrialOreException("That individual is ineligible for trial due to rank") + } + if (player.uniqueId == testificate.uniqueId) { + throw TrialOreException("You cannot trial yourself") + } + if (!app.startsWith("https://discourse.openredstone.org/")) { + throw TrialOreException("Invalid app: $app") + } + player.renderMessage("Starting trial of ${testificate.name}") + testificate.renderMessage("Starting trial with ${player.name}") + trialORE.startTrial(player.uniqueId, testificate.uniqueId, app) + } } - player.renderMessage("Starting trial of ${testificate.name}") - testificate.renderMessage("Starting trial with ${player.name}") - trialORE.startTrial(player.uniqueId, testificate.uniqueId, app) + player.renderMiniMessage("Target has not passed the test yet.") } @Subcommand("note") diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index fd01a96..f547a4e 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -1,5 +1,5 @@ name: TrialORE -version: 1.0 +version: 1.1 main: org.openredstone.trialore.TrialOre api-version: 1.20 depend: [LuckPerms] From 9d0590dd9b4145f46b10a03edd710d57de794ffc Mon Sep 17 00:00:00 2001 From: Wueffi Date: Sun, 2 Nov 2025 17:54:18 +0100 Subject: [PATCH 13/13] Stop storing in-progress Tests in the database and removing TestID, make plugin loading not crash without a config file and let getTrials() and getTests() only query the database once --- .../org/openredstone/trialore/Storage.kt | 50 +++++++++---------- .../org/openredstone/trialore/TestCommand.kt | 17 +++---- .../org/openredstone/trialore/TrialOre.kt | 46 ++++++++++------- 3 files changed, 60 insertions(+), 53 deletions(-) diff --git a/src/main/kotlin/org/openredstone/trialore/Storage.kt b/src/main/kotlin/org/openredstone/trialore/Storage.kt index a752ef5..3a31f47 100644 --- a/src/main/kotlin/org/openredstone/trialore/Storage.kt +++ b/src/main/kotlin/org/openredstone/trialore/Storage.kt @@ -89,15 +89,6 @@ class Storage( }[Trial.id] } - fun insertTest(testificate: UUID): Int = transaction(database) { - Test.insert { - it[Test.testificate] = testificate.toString() - it[start] = now() - it[Test.passed] = false - it[Test.wrong] = 25 - }[Test.id] - } - fun endTrial(trialId: Int, passed: Boolean) = transaction(database) { Trial.update({ Trial.id eq trialId}) { it[end] = now() @@ -105,27 +96,29 @@ class Storage( } } - fun endTest(testId: Int, passed: Boolean, wrong: Int) = transaction(database) { - Test.update( {Test.id eq testId}) { - it[end] = now() + fun endTest(testificate: UUID, startingtime: Int, passed: Boolean, wrong: Int) = transaction(database) { + Test.insert { + it[Test.testificate] = testificate.toString() + it[start] = startingtime it[Test.passed] = passed it[Test.wrong] = wrong - } + it[end] = now() + }[Test.id] } fun getTrials(testificate: UUID): List = transaction(database) { - Trial.selectAll().where { - Trial.testificate eq testificate.toString() - }.map { + Query( + Trial, Trial.testificate eq testificate.toString() + ).map { it[Trial.id] } } fun getTests(testificate: UUID): List = transaction(database) { - Test.selectAll().where { - Test.testificate eq testificate.toString() - }.map { - it[Test.id] + Query( + Test, Test.testificate eq testificate.toString() + ).map { + it[Test.id] } } @@ -170,12 +163,6 @@ class Storage( }.count().toInt() } - fun setTestWrong(testId: Int, wrong: Int) = transaction(database) { - Test.update({ Test.id eq testId }) { - it[Test.wrong] = wrong - } - } - fun insertNote(trialId: Int, note: String) = transaction(database) { Note.insert { it[trial_id] = trialId @@ -201,6 +188,17 @@ class Storage( } } + fun didPass(testificate: UUID) : Boolean { + val tests = getTests(testificate) + tests.forEachIndexed { index, testid -> + val testInfo = getTestInfo(testid) + if (testInfo?.passed ?: false) { + return true + } + } + return false + } + fun ensureCachedUsername(user: UUID, username: String) = transaction(database) { UsernameCache.upsert { it[this.uuid] = user.toString() diff --git a/src/main/kotlin/org/openredstone/trialore/TestCommand.kt b/src/main/kotlin/org/openredstone/trialore/TestCommand.kt index b64ec01..f5748ea 100644 --- a/src/main/kotlin/org/openredstone/trialore/TestCommand.kt +++ b/src/main/kotlin/org/openredstone/trialore/TestCommand.kt @@ -23,14 +23,11 @@ class TestCommand( if (trialORE.testMapping.containsKey(testificate.uniqueId)) { throw TrialOreException("You are already testing. This is 99.9% a Bug. Contact Nick :D") } - val tests = trialORE.database.getTests(testificate.uniqueId) - tests.forEachIndexed { index, testid -> - val testInfo = trialORE.database.getTestInfo(testid) - if (testInfo?.passed ?: false) { - player.renderMiniMessage("You already passed the test!") - return - } + if (trialORE.database.didPass(testificate.uniqueId)) { + player.renderMiniMessage("You already passed the test!") + return } + val tests = trialORE.database.getTests(testificate.uniqueId) val now = System.currentTimeMillis() val lastThreeTests = tests.takeLast(3) @@ -38,7 +35,7 @@ class TestCommand( val lastThreeWithin24h = lastThreeTests.all { test -> val testInfo = trialORE.database.getTestInfo(test) ?: run { - player.sendMessage("Test id was null. Report this to Staff.") + player.sendMessage("Could not get last 3 tests... Report this to Staff.") return@all false } val startTimeMs = testInfo.start.toLong() * 1000L @@ -174,7 +171,7 @@ class TestCommand( @Description("Stop a test") fun onStop(player: Player, testMeta: TestMeta) { player.renderMessage("You have exited your test") - trialORE.endTest(player.uniqueId, testMeta.testId, false, wrong = 25) + trialORE.endTest(player.uniqueId, testMeta.session.startingtime, false, wrong = 25) } @CommandAlias("testanswer") @@ -205,7 +202,7 @@ class TestCommand( } else { player.renderMiniMessage("Incorrect Answer. Expected: $expected") session.wrong++ - trialORE.database.setTestWrong(session.testId, session.wrong) + // trialORE.database.setTestWrong(session.testId, session.wrong) } session.index++ diff --git a/src/main/kotlin/org/openredstone/trialore/TrialOre.kt b/src/main/kotlin/org/openredstone/trialore/TrialOre.kt index 808a18c..3a1d3a0 100644 --- a/src/main/kotlin/org/openredstone/trialore/TrialOre.kt +++ b/src/main/kotlin/org/openredstone/trialore/TrialOre.kt @@ -53,7 +53,7 @@ data class TrialMeta( data class TestMeta( val testificate: UUID, - val testId: Int + val session: TrialOre.TestSession ) class TrialOre : JavaPlugin(), Listener { @@ -61,7 +61,7 @@ class TrialOre : JavaPlugin(), Listener { lateinit var luckPerms: LuckPerms lateinit var config: TrialOreConfig val trialMapping: MutableMap> = mutableMapOf() - val testMapping: MutableMap = mutableMapOf() + val testMapping: MutableMap = mutableMapOf() private val mapper = ObjectMapper(YAMLFactory()) override fun onEnable() { loadConfig() @@ -126,10 +126,20 @@ class TrialOre : JavaPlugin(), Listener { dataFolder.mkdir() } val configFile = File(dataFolder, "config.yml") - // does not overwrite or throw - configFile.createNewFile() - val config = mapper.readTree(configFile) - val loadedConfig = mapper.treeToValue(config, TrialOreConfig::class.java) + if (!configFile.exists() || configFile.length() == 0L) { + configFile.createNewFile() + val defaultConfig = TrialOreConfig() + mapper.writeValue(configFile, defaultConfig) + } + + val loadedConfig: TrialOreConfig = try { + val config = mapper.readTree(configFile) + mapper.treeToValue(config, TrialOreConfig::class.java) + } catch (e: Exception) { + logger.warning("Failed to load config.yml, using defaults. $e") + TrialOreConfig() + } + logger.info("Loaded config.yml") return loadedConfig } @@ -193,9 +203,9 @@ class TrialOre : JavaPlugin(), Listener { }, config.abandonForgiveness) } } - testMapping.forEach { (testtaker, testId) -> + testMapping.forEach { (testtaker, session) -> if (uuid == testtaker) { - endTest(testtaker, testId, false, 25) + endTest(testtaker, session.startingtime, false, 25) } } } @@ -222,13 +232,14 @@ class TrialOre : JavaPlugin(), Listener { sendReport(database.getTrialInfo(trialId), database.getTrialCount(testificate)) } - fun endTest(testificate: UUID, testId: Int, passed: Boolean, wrong: Int) { - this.database.endTest(testId, passed, wrong) + fun endTest(testificate: UUID, startingtime: Int, passed: Boolean, wrong: Int): Int { + val testId = this.database.endTest(testificate, startingtime, passed, wrong) this.testMapping.remove(testificate) val testInfo = database.getTestInfo(testId) - if (testInfo?.wrong == 25) { return } + if (testInfo?.wrong == 25) { return 0 } if ((config.sendFailedTests || passed) && testInfo != null) sendTestReport(testInfo, database.getTestCount(testificate)) + return testId } fun getParent(uuid: UUID): String? = luckPerms.userManager.getUser(uuid)?.primaryGroup @@ -341,7 +352,7 @@ class TrialOre : JavaPlugin(), Listener { } data class TestSession( - val testId: Int, + val startingtime: Int, val questions: List, var index: Int = 0, var currentAnswer: String = "", @@ -353,8 +364,8 @@ class TrialOre : JavaPlugin(), Listener { private val rand = Random() fun startTest(testificate: UUID) { - val testId = this.database.insertTest(testificate) - this.testMapping[testificate] = testId + // val testId = this.database.insertTest(testificate) + val startingtime = now() val categories = mutableListOf().apply { repeat(4) { add(1) } @@ -365,7 +376,8 @@ class TrialOre : JavaPlugin(), Listener { repeat(3) { add(6) } } categories.shuffle(rand) - val session = TestSession(testId, categories) + val session = TestSession(startingtime, categories) + this.testMapping[testificate] = session testSessions[testificate] = session server.getPlayer(testificate)?.let { player -> sendNextQuestion(player, session) @@ -376,10 +388,10 @@ class TrialOre : JavaPlugin(), Listener { fun sendNextQuestion(player: Player, session: TestSession) { if (session.index >= session.questions.size) { val passed = session.wrong <= 2 - endTest(player.uniqueId, session.testId, passed, session.wrong) + val testId = endTest(player.uniqueId, session.startingtime, passed, session.wrong) testSessions.remove(player.uniqueId) if (passed) { - player.renderMiniMessage("Test finished(${session.testId}). Wrong: ${session.wrong}") + player.renderMiniMessage("Test (${testId}) finished. Wrong: ${session.wrong}") } else { player.renderMiniMessage(" You failed the test. Wrong: ${session.wrong}") }