From 00481cfba773cef86062ca2316831020dd9d67eb Mon Sep 17 00:00:00 2001 From: Jason <131086642+JasonP01@users.noreply.github.com> Date: Tue, 3 Mar 2026 15:58:01 +0200 Subject: [PATCH 1/2] perf(mindustry): Updated AFK system to be more precise and added manual command (#494) --------- Co-authored-by: Finley Experience <62483793+phinner@users.noreply.github.com> --- .../security/AdminRequestListener.kt | 25 +++++-- .../mindustry/security/AfkListener.kt | 68 +++++++++++++------ .../mindustry/translation/Messages.kt | 17 ++++- .../mindustry/bundles/bundle_en.properties | 8 ++- .../mindustry/bundles/bundle_fr.properties | 2 - .../mindustry/bundles/bundle_ru.properties | 2 - 6 files changed, 86 insertions(+), 36 deletions(-) diff --git a/imperium-mindustry/src/main/kotlin/com/xpdustry/imperium/mindustry/security/AdminRequestListener.kt b/imperium-mindustry/src/main/kotlin/com/xpdustry/imperium/mindustry/security/AdminRequestListener.kt index eb5a63c4a..c937eb78b 100644 --- a/imperium-mindustry/src/main/kotlin/com/xpdustry/imperium/mindustry/security/AdminRequestListener.kt +++ b/imperium-mindustry/src/main/kotlin/com/xpdustry/imperium/mindustry/security/AdminRequestListener.kt @@ -168,16 +168,23 @@ class AdminRequestListener(instances: InstanceManager) : ImperiumApplication.Lis } } - Vars.net.handleServer(AdminRequestCallPacket::class.java, ::interceptAdminRequest) + Vars.net.handleServer(AdminRequestCallPacket::class.java) { con, packet -> + ImperiumScope.MAIN.launch { + // TODO We REALLY need a proper local account cache... + val playerRank = runCatching { getUserRank(con.player) }.getOrNull() ?: Rank.EVERYONE + runMindustryThread { interceptAdminRequest(con, packet, playerRank) } + } + } } - private fun interceptAdminRequest(con: NetConnection, packet: AdminRequestCallPacket) { + private fun interceptAdminRequest(con: NetConnection, packet: AdminRequestCallPacket, playerRank: Rank) { if (con.player == null) { logger.warn("Received admin request from non-existent player (uuid: {}, ip: {})", con.uuid, con.address) return } - if (!con.player.admin()) { + // Allow undercover staff to use the admin menu + if (playerRank < Rank.OVERSEER || !con.player.admin()) { logger.warn( "{} ({}) attempted to perform an admin action without permission", con.player.plainName(), @@ -195,7 +202,10 @@ class AdminRequestListener(instances: InstanceManager) : ImperiumApplication.Lis return } - if (packet.other.admin() && (packet.action != AdminAction.switchTeam && packet.action != AdminAction.wave)) { + if ( + (packet.other.admin() && playerRank < Rank.ADMIN) && + (packet.action != AdminAction.switchTeam && packet.action != AdminAction.wave) + ) { logger.warn( "{} ({}) attempted to perform an admin action on the admin {} ({})", con.player.plainName(), @@ -270,7 +280,7 @@ class AdminRequestListener(instances: InstanceManager) : ImperiumApplication.Lis Call.infoMessage(requester.con, "Player not found.") return@launch } - val canSeeInfo = (accounts.selectBySession(requester.sessionKey)?.rank ?: Rank.EVERYONE) >= Rank.ADMIN + val canSeeInfo = getUserRank(requester) >= Rank.ADMIN val historic = users.findNamesAndAddressesById(user.id) Call.traceInfo( requester.con, @@ -318,6 +328,11 @@ class AdminRequestListener(instances: InstanceManager) : ImperiumApplication.Lis } } + private suspend fun getUserRank(requester: Player): Rank { + val account = accounts.selectBySession(requester.sessionKey) ?: return Rank.EVERYONE + return account.rank + } + companion object { private val logger by LoggerDelegate() } diff --git a/imperium-mindustry/src/main/kotlin/com/xpdustry/imperium/mindustry/security/AfkListener.kt b/imperium-mindustry/src/main/kotlin/com/xpdustry/imperium/mindustry/security/AfkListener.kt index 5daf76e86..61ac99a91 100644 --- a/imperium-mindustry/src/main/kotlin/com/xpdustry/imperium/mindustry/security/AfkListener.kt +++ b/imperium-mindustry/src/main/kotlin/com/xpdustry/imperium/mindustry/security/AfkListener.kt @@ -17,21 +17,26 @@ */ package com.xpdustry.imperium.mindustry.security -import arc.math.geom.Point2 +import com.xpdustry.distributor.api.Distributor import com.xpdustry.distributor.api.annotation.EventHandler import com.xpdustry.distributor.api.annotation.TaskHandler +import com.xpdustry.distributor.api.command.CommandSender import com.xpdustry.distributor.api.plugin.MindustryPlugin import com.xpdustry.distributor.api.scheduler.MindustryTimeUnit import com.xpdustry.imperium.common.application.ImperiumApplication +import com.xpdustry.imperium.common.command.ImperiumCommand import com.xpdustry.imperium.common.config.ImperiumConfig -import com.xpdustry.imperium.common.inject.get +import com.xpdustry.imperium.mindustry.command.annotation.ClientSide +import com.xpdustry.imperium.mindustry.command.annotation.ServerSide import com.xpdustry.imperium.mindustry.misc.Entities import com.xpdustry.imperium.mindustry.misc.PlayerMap import com.xpdustry.imperium.mindustry.misc.asAudience -import com.xpdustry.imperium.mindustry.translation.player_afk +import com.xpdustry.imperium.mindustry.translation.is_player_afk +import com.xpdustry.imperium.mindustry.translation.player_afk_announcement import com.xpdustry.imperium.mindustry.translation.player_afk_kick import java.time.Duration import java.time.Instant +import kotlin.time.toJavaDuration import kotlin.time.toKotlinDuration import mindustry.Vars import mindustry.game.EventType @@ -44,10 +49,11 @@ interface AfkManager { class AfkListener(private val config: ImperiumConfig, plugin: MindustryPlugin) : AfkManager, ImperiumApplication.Listener { private val lastActivity = PlayerMap(plugin) - private val lastPosition = PlayerMap(plugin) private val notified = PlayerMap(plugin) override fun onImperiumInit() { + // TODO: TapEvent is not covered here but it is a valid way to show a player is there however how to detect if + // its just a miss click? Do TapEvents occur if the window isnt focused? Vars.netServer.admins.addActionFilter { action -> onDisturbingThePeace(action.player) true @@ -66,22 +72,8 @@ class AfkListener(private val config: ImperiumConfig, plugin: MindustryPlugin) : @TaskHandler(interval = 15, unit = MindustryTimeUnit.SECONDS) fun onPlayerAfkUpdate() { - Entities.getPlayers().forEach { player -> - val new = Point2.pack(player.tileX(), player.tileY()) - val old = lastPosition.set(player, new) - if (old != new) { - onDisturbingThePeace(player) - } - - val duration = getAfkDuration(player).toKotlinDuration() - when { - duration >= config.mindustry.afkDelay -> { - if (notified.set(player, Unit) != Unit) player.asAudience.sendMessage(player_afk(enabled = true)) - } - duration >= config.mindustry.afkKickDelay -> { - player.asAudience.kick(player_afk_kick(), Duration.ZERO) - } - } + for (player in Entities.getPlayers()) { + checkAfkStatus(player) } } @@ -89,6 +81,24 @@ class AfkListener(private val config: ImperiumConfig, plugin: MindustryPlugin) : return getAfkDuration(player).toKotlinDuration() >= config.mindustry.afkDelay } + private fun checkAfkStatus(player: Player) { + val duration = getAfkDuration(player).toKotlinDuration() + + when { + duration >= config.mindustry.afkKickDelay -> { + player.asAudience.kick(player_afk_kick(), Duration.ZERO) + } + duration >= config.mindustry.afkDelay -> { + if (notified.set(player, Unit) != Unit) { + Distributor.get() + .audienceProvider + .players + .sendMessage(player_afk_announcement(true, player.plainName())) + } + } + } + } + private fun getAfkDuration(player: Player): Duration { return lastActivity[player]?.let { Duration.between(it, Instant.now()) } ?: Duration.ZERO } @@ -96,9 +106,25 @@ class AfkListener(private val config: ImperiumConfig, plugin: MindustryPlugin) : // PERSONA!! private fun onDisturbingThePeace(player: Player) { if (isPlayerAfk(player)) { - player.asAudience.sendMessage(player_afk(enabled = false)) + Distributor.get().audienceProvider.players.sendMessage(player_afk_announcement(false, player.plainName())) } notified.remove(player) lastActivity[player] = Instant.now() } + + @ImperiumCommand(["afk"]) + @ClientSide + @ServerSide + fun isAfkCommand(sender: CommandSender, target: Player? = null) { + if (target == null) { + if (sender.isPlayer) { + val afkInstant = Instant.now().minus((config.mindustry.afkDelay).toJavaDuration()) + lastActivity[sender.player] = afkInstant + checkAfkStatus(sender.player) + } else sender.error("Console must specify a player. You can't be afk as console.") + } else { + val isAfk = isPlayerAfk(target) + sender.reply(is_player_afk(isAfk, target.plainName())) + } + } } diff --git a/imperium-mindustry/src/main/kotlin/com/xpdustry/imperium/mindustry/translation/Messages.kt b/imperium-mindustry/src/main/kotlin/com/xpdustry/imperium/mindustry/translation/Messages.kt index fb5fd7de2..26cd42078 100644 --- a/imperium-mindustry/src/main/kotlin/com/xpdustry/imperium/mindustry/translation/Messages.kt +++ b/imperium-mindustry/src/main/kotlin/com/xpdustry/imperium/mindustry/translation/Messages.kt @@ -459,7 +459,18 @@ private val CYAN_PREFIX = text(">>> ", CYAN) fun cyan_prefix(component: Component): Component = components(CYAN_PREFIX, space(), component) -fun player_afk(enabled: Boolean): Component = - components(WHITE, text(">>> ", CYAN), translatable("imperium.player.afk.${if (enabled) "enabled" else "disabled"}")) - fun player_afk_kick(): Component = components(WHITE, translatable("imperium.player.afk.kicked", SCARLET)) + +fun is_player_afk(boolean: Boolean, player: String): Component = + components( + WHITE, + text(">>> ", CYAN), + translatable("imperium.player.afk.$boolean", TranslationArguments.array(text(player, ORANGE))), + ) + +fun player_afk_announcement(boolean: Boolean, player: String): Component = + components( + WHITE, + text(">>> ", CYAN), + translatable("imperium.player.afk.announcement.$boolean", TranslationArguments.array(text(player, ORANGE))), + ) diff --git a/imperium-mindustry/src/main/resources/com/xpdustry/imperium/mindustry/bundles/bundle_en.properties b/imperium-mindustry/src/main/resources/com/xpdustry/imperium/mindustry/bundles/bundle_en.properties index 1df32676e..5dfd5a696 100644 --- a/imperium-mindustry/src/main/resources/com/xpdustry/imperium/mindustry/bundles/bundle_en.properties +++ b/imperium-mindustry/src/main/resources/com/xpdustry/imperium/mindustry/bundles/bundle_en.properties @@ -291,6 +291,8 @@ imperium.notification.login=You are logged in as {0}. imperium.killall.force.success.base=You killed {0} unit(s) imperium.killall.force.success.type=of type {0} imperium.killall.force.success.team=from the {0} team. -imperium.player.afk.enabled=You have been marked as AFK -imperium.player.afk.disabled=You are no longer marked as AFK -imperium.player.afk.kicked=You have been kicked for being AFK for too long. \ No newline at end of file +imperium.player.afk.kicked=You have been kicked for being AFK for too long. +imperium.player.afk.true={0} is AFK. They are not counted in votes! +imperium.player.afk.false={0} is not AFK. They are counted in votes. +imperium.player.afk.announcement.true={0} has been marked as AFK! +imperium.player.afk.announcement.false={0} is no longer AFK! \ No newline at end of file diff --git a/imperium-mindustry/src/main/resources/com/xpdustry/imperium/mindustry/bundles/bundle_fr.properties b/imperium-mindustry/src/main/resources/com/xpdustry/imperium/mindustry/bundles/bundle_fr.properties index e8a539e27..fab0528b2 100644 --- a/imperium-mindustry/src/main/resources/com/xpdustry/imperium/mindustry/bundles/bundle_fr.properties +++ b/imperium-mindustry/src/main/resources/com/xpdustry/imperium/mindustry/bundles/bundle_fr.properties @@ -236,8 +236,6 @@ imperium.messages.warning=Attention imperium.messages.warning.bad_word=Votre message contenait des gros mots ({1}). Abstenez-vous d'utiliser ce genre de langage, ou vous risquez d'être {0}. imperium.no=Non imperium.notification.login=Vous êtes connecté en tant que {0}. -imperium.player.afk.disabled=Vous n'êtes plus marqué comme AFK -imperium.player.afk.enabled=Vous avez été marqué comme AFK imperium.player.afk.kicked=Vous avez été expulsé pour avoir été AFK trop longtemps. imperium.report.reason.cheating=Tricherie imperium.report.reason.griefing=Griefing diff --git a/imperium-mindustry/src/main/resources/com/xpdustry/imperium/mindustry/bundles/bundle_ru.properties b/imperium-mindustry/src/main/resources/com/xpdustry/imperium/mindustry/bundles/bundle_ru.properties index 6d7d23178..762622623 100644 --- a/imperium-mindustry/src/main/resources/com/xpdustry/imperium/mindustry/bundles/bundle_ru.properties +++ b/imperium-mindustry/src/main/resources/com/xpdustry/imperium/mindustry/bundles/bundle_ru.properties @@ -236,8 +236,6 @@ imperium.messages.warning=Внимание imperium.messages.warning.bad_word=Ваше сообщение содержит нехорошие слова ({1}). Воздержитесь от использования таких слов, иначе вы можете быть {0}. imperium.no=Нет imperium.notification.login=Вы вошли в систему под именем {0}. -imperium.player.afk.disabled=Вы более не помечены как "Отошёл" -imperium.player.afk.enabled=Вы получили пометку "Отошёл" imperium.player.afk.kicked=Вы были исключены т.к. слишком долго находились с пометкой "Отошёл". imperium.report.reason.cheating=Читерство imperium.report.reason.griefing=Гриффинг From 428cca3c23f031bec9201f8df7c4c6bfd132671b Mon Sep 17 00:00:00 2001 From: Finley Experience <62483793+phinner@users.noreply.github.com> Date: Tue, 3 Mar 2026 15:26:05 +0100 Subject: [PATCH 2/2] fix(discord): Closes https://github.com/xpdustry/imperium/issues/495 --- .../discord/commands/MapSubmitCommand.kt | 25 +++++++++++------- .../imperium/discord/map_preview_error.png | Bin 0 -> 5954 bytes 2 files changed, 16 insertions(+), 9 deletions(-) create mode 100644 imperium-discord/src/main/resources/com/xpdustry/imperium/discord/map_preview_error.png diff --git a/imperium-discord/src/main/kotlin/com/xpdustry/imperium/discord/commands/MapSubmitCommand.kt b/imperium-discord/src/main/kotlin/com/xpdustry/imperium/discord/commands/MapSubmitCommand.kt index ae38f93dc..aa0a5c0ef 100644 --- a/imperium-discord/src/main/kotlin/com/xpdustry/imperium/discord/commands/MapSubmitCommand.kt +++ b/imperium-discord/src/main/kotlin/com/xpdustry/imperium/discord/commands/MapSubmitCommand.kt @@ -42,7 +42,10 @@ import com.xpdustry.imperium.discord.misc.MessageCreate import com.xpdustry.imperium.discord.misc.await import com.xpdustry.imperium.discord.service.DiscordService import java.awt.Color +import java.awt.image.BufferedImage +import java.io.IOException import java.net.URI +import javax.imageio.ImageIO import kotlin.io.path.createTempFile import kotlin.io.path.deleteExisting import kotlinx.coroutines.future.await @@ -96,21 +99,13 @@ class MapSubmitCommand(instances: InstanceManager) : ImperiumApplication.Listene val tempFile = createTempFile() map.proxy.downloadToPath(tempFile).await() val metaResult = content.parseMap(tempFile.toFile()) - val previewResult = content.renderMap(tempFile.toFile()) if (metaResult.isFailure) { val ex = metaResult.exceptionOrNull()!! logger.error("Invalid map file", ex) reply.sendMessage("Invalid map file: " + ex.message).await() return } - if (previewResult.isFailure) { - val ex = previewResult.exceptionOrNull()!! - logger.error("Invalid map file", ex) - reply.sendMessage("Invalid map file: " + ex.message).await() - return - } val meta = metaResult.getOrThrow() - val preview = previewResult.getOrThrow() if (meta.width > MindustryMap.MAX_MAP_SIDE_SIZE || meta.height > MindustryMap.MAX_MAP_SIDE_SIZE) { reply @@ -121,6 +116,12 @@ class MapSubmitCommand(instances: InstanceManager) : ImperiumApplication.Listene return } + val previewResult = content.renderMap(tempFile.toFile()) + if (previewResult.isFailure) { + logger.error("Failed to render map preview", previewResult.exceptionOrNull()) + } + val preview = previewResult.getOrDefault(FALLBACK_PREVIEW) + val channel = discord.getMainServer().getTextChannelById(config.discord.channels.maps) ?: throw IllegalStateException("Map submission channel not found") @@ -138,7 +139,7 @@ class MapSubmitCommand(instances: InstanceManager) : ImperiumApplication.Listene field("Name", meta.name.stripMindustryColors(), false) field("Author", meta.author.stripMindustryColors(), false) field("Description", meta.description.stripMindustryColors(), false) - field("Size", "${preview.width} x ${preview.height}", false) + field("Size", "${meta.width} x ${meta.height}", false) if (notes != null) { field("Notes", notes, false) } @@ -292,5 +293,11 @@ class MapSubmitCommand(instances: InstanceManager) : ImperiumApplication.Listene private const val MAP_ACCEPT_BUTTON = "map-submission-accept:2" private const val MAP_REJECT_BUTTON = "map-submission-reject:2" private val logger by LoggerDelegate() + private val FALLBACK_PREVIEW: BufferedImage by lazy { + MapSubmitCommand::class + .java + .getResourceAsStream("/com/xpdustry/imperium/discord/map_preview_error.png") + ?.use { ImageIO.read(it) } ?: throw IOException("Failed to load fallback map preview image") + } } } diff --git a/imperium-discord/src/main/resources/com/xpdustry/imperium/discord/map_preview_error.png b/imperium-discord/src/main/resources/com/xpdustry/imperium/discord/map_preview_error.png new file mode 100644 index 0000000000000000000000000000000000000000..3c135aea6dac6526b90aab530897808c7d08b1c5 GIT binary patch literal 5954 zcmeAS@N?(olHy`uVBq!ia0y~yU;;9k7&zE~)R&4YzZe+AIXzt*Ln`9lUOU+J#!ATb z;`jgdcYS^NXD)am7d_zc zo~8fd)lV$TkI&k(JuLi$@w<1upJeCX;okdsZQaEGPt>I4k<>Y@Sy{}Qn@#eR5pYPlq|75fO zJFZH%#X!f-y8}!NpcEqoOaXW9ru|m`;TjjW_-@g>-M?M!em?<*l*uf;_m58k9iIVA z7mF|ES)RX}yRE({{>f9*QR8SBcKkCQ9A0Wt1z`%k35&)Ys{`9)y5C-xr(KNKVqkfY^QF7y+q<&Ul~TaOSy&D%GuDPZEnW-E#~UxEjT%O2m`Tr7 zNzXTlz309<_{kldAv>xHmS9IiVl*Vs%bn3QLH9Iq=O(aixEk2R==Gbl*&JAvFTSc- z^e$}mr=7cR>pssc?f5(S+0naqM-|a82oMd!jFL%n_JS()wSE(ew_`LjrBJFI^hRrs zoBGL{(!jd@;wur$a!?ebv?3Q@^ep=325i=Dv2s0k_Z51Zbhud^{*%8l3ltzIVFqjs z&AV0besAL+cl)iApS(fM1i&Vef9{U&_Z34h0C;Wf0ybKhJ$f2FJea`chyx;FhpQ!&5Z0&jO-V2a^ zU~@ZauF~_ot literal 0 HcmV?d00001