Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,5 @@ on:
jobs:
reusable:
uses: groundsgg/.github/.github/workflows/gradle-ci.yml@main
secrets:
PACKAGES_TOKEN: ${{ secrets.PACKAGES_TOKEN }}
2 changes: 2 additions & 0 deletions .github/workflows/docker-gradle-build-push.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,5 @@ permissions:
jobs:
reusable:
uses: groundsgg/.github/.github/workflows/docker-gradle-build-push.yml@main
secrets:
PACKAGES_TOKEN: ${{ secrets.PACKAGES_TOKEN }}
2 changes: 2 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,5 @@ permissions:
jobs:
reusable:
uses: groundsgg/.github/.github/workflows/gradle-publish.yml@main
secrets:
PACKAGES_TOKEN: ${{ secrets.PACKAGES_TOKEN }}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package gg.grounds.proxy.api

import java.util.UUID

/** Only a verified currently loaded HUD-capable pack may enable bitmap HUD glyphs. */
fun interface PlayerHudAppearanceQuery {
fun appearanceOf(playerId: UUID): PlayerHudAppearance
}

data class PlayerHudAppearance(val ready: Boolean = false)
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package gg.grounds.proxy.api

import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.Test

class PlayerHudAppearanceTest {
@Test
fun `unverified appearance is disabled by default`() {
assertFalse(PlayerHudAppearance().ready)
}
}
3 changes: 2 additions & 1 deletion velocity/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ plugins { id("gg.grounds.velocity") version "0.1.1" }

repositories {
maven {
url = uri("https://maven.pkg.github.com/groundsgg/*")
url = uri("https://maven.pkg.github.com/groundsgg/library-minigame-api")
credentials {
username =
providers.gradleProperty("github.user").orNull ?: System.getenv("GITHUB_ACTOR")
Expand All @@ -14,6 +14,7 @@ repositories {

dependencies {
implementation(project(":api"))
implementation("gg.grounds:library-minigame-api-hud:0.6.1-announcement-bullet-75f53d5")
// service-config answers REST; the MOTD store parses its JSON with gson, the
// way plugin-match's and plugin-social's clients do.
implementation("com.google.code.gson:gson:2.15.0")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,12 @@ import com.google.inject.Inject
import com.velocitypowered.api.event.PostOrder
import com.velocitypowered.api.event.ResultedEvent
import com.velocitypowered.api.event.Subscribe
import com.velocitypowered.api.event.connection.DisconnectEvent
import com.velocitypowered.api.event.connection.LoginEvent
import com.velocitypowered.api.event.connection.PostLoginEvent
import com.velocitypowered.api.event.player.KickedFromServerEvent
import com.velocitypowered.api.event.player.ServerPostConnectEvent
import com.velocitypowered.api.event.player.ServerPreConnectEvent
import com.velocitypowered.api.event.proxy.ProxyInitializeEvent
import com.velocitypowered.api.event.proxy.ProxyPingEvent
import com.velocitypowered.api.event.proxy.ProxyShutdownEvent
Expand All @@ -21,12 +24,15 @@ import gg.grounds.i18n.Translations
import gg.grounds.presentation.VelocitySystemMessages
import gg.grounds.proxy.api.NetworkPlayerCounts
import gg.grounds.proxy.api.PlayerDisplayQuery
import gg.grounds.proxy.api.PlayerHudAppearanceQuery
import gg.grounds.proxy.api.PlayerLocaleQuery
import gg.grounds.proxy.api.PlayerMessageRenderers
import gg.grounds.proxy.api.PlayerRoleQuery
import gg.grounds.proxy.api.PlayerSessionTransfer
import gg.grounds.proxy.api.ProxyService
import gg.grounds.proxy.api.ProxyServiceRegistry
import gg.grounds.proxy.api.ServerDisplayQuery
import gg.grounds.proxy.velocity.announcements.*
import gg.grounds.proxy.velocity.command.MotdCommand
import gg.grounds.proxy.velocity.command.OnlineCommand
import gg.grounds.proxy.velocity.command.ProxyCommand
Expand All @@ -52,8 +58,12 @@ import gg.grounds.proxy.velocity.tab.TabParties
import gg.grounds.proxy.velocity.tab.VoicePresences
import io.micrometer.core.instrument.MeterRegistry
import io.nats.client.Subscription
import java.net.URI
import java.nio.file.Files
import java.nio.file.Path
import java.time.Duration
import java.time.Instant
import java.util.Locale
import java.util.UUID
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicBoolean
Expand Down Expand Up @@ -172,6 +182,14 @@ internal fun resolveConfigServiceTarget(
class GroundsProxyPlugin
@Inject
constructor(private val proxy: ProxyServer, private val logger: Logger) {
@Volatile private var announcementSettings: AnnouncementSettings? = null
private var announcementController: AnnouncementController? = null
private var announcementSync: AnnouncementSynchronizer? = null
private var announcementStore: AnnouncementConfigStore? = null
private var announcementPoll: ScheduledTask? = null
private var announcementTick: ScheduledTask? = null
private val announcementQueued = AtomicBoolean(false)

private val messages =
Translations.forBundleWithSystemRenderer(
"gg.grounds.proxy.messages",
Expand Down Expand Up @@ -305,6 +323,7 @@ constructor(private val proxy: ProxyServer, private val logger: Logger) {

startMotd()
startMaintenance()
startAnnouncements()

subscribeForProxy()

Expand Down Expand Up @@ -604,11 +623,25 @@ constructor(private val proxy: ProxyServer, private val logger: Logger) {
*/
@Subscribe
fun onServerConnected(event: ServerPostConnectEvent) {
event.player.currentServer.orElse(null)?.let { backend ->
announcementController?.completedSwitch(
event.player.uniqueId,
event.player,
backend.serverInfo.name,
backend,
)
}
announcementController?.refreshAll()
tabList?.refresh(event.player)
}

@Subscribe
fun onShutdown(event: ProxyShutdownEvent) {
announcementSync?.close()
announcementPoll?.cancel()
announcementTick?.cancel()
announcementController?.close()
announcementStore?.close()
bedrockRoster?.close()
ProxyServiceRegistry.unregister(ProxyService::class.java)
ProxyServiceRegistry.unregister(PlayerDisplayQuery::class.java)
Expand All @@ -629,6 +662,154 @@ constructor(private val proxy: ProxyServer, private val logger: Logger) {
}
}

@Subscribe
fun onAnnouncementDisconnect(event: DisconnectEvent) {
announcementController?.forget(event.player.uniqueId, event.player)
}

@Subscribe(order = PostOrder.LAST)
fun onAnnouncementPreConnect(event: ServerPreConnectEvent) {
if (event.result.isAllowed) {
announcementController?.beginSwitch(
event.player.uniqueId,
event.player,
event,
event.player.currentServer.orElse(null),
) {
event.result.server.orElse(null)?.serverInfo?.name
}
}
}

@Subscribe(order = PostOrder.LAST)
fun onAnnouncementConnectionFailure(event: KickedFromServerEvent) {
if (event.kickedDuringServerConnect() && event.result is KickedFromServerEvent.Notify) {
val player = event.player
announcementController?.failedSwitch(
player.uniqueId,
player,
event.server.serverInfo.name,
player.currentServer.orElse(null),
player.isActive,
)
announcementController?.refreshAll()
}
}

private fun startAnnouncements() {
val ownership = AnnouncementOwnership.parse(System.getenv("GROUNDS_ANNOUNCEMENTS_OWNER"))
if (ownership == null)
logger.error("Invalid GROUNDS_ANNOUNCEMENTS_OWNER; announcements disabled")
if (ownership != AnnouncementOwnership.PROXY) return
val environment = System.getenv("GROUNDS_ENVIRONMENT")
if (environment !in setOf("stage", "prod")) {
logger.error("Announcements require an explicit stage/prod environment")
return
}
val target = resolveConfigServiceTarget() ?: return
val store =
AnnouncementConfigStore(
URI(if (target.contains("://")) target else "http://$target"),
environment!!,
token = {
val path =
Path.of(
System.getenv("GROUNDS_TOKEN_FILE") ?: "/var/run/secrets/grounds/token"
)
if (Files.exists(path)) Files.readString(path).trim() else null
},
)
announcementStore = store
val controller =
AnnouncementController(
viewers = {
proxy.allPlayers.map { player ->
object : AnnouncementViewer {
override val playerId
get() = player.uniqueId

override val session: Any
get() = player

override val connected
get() = player.isActive

override val serverType
get() =
player.currentServer.orElse(null)?.serverInfo?.name?.let {
ProxyServiceRegistry.get(ServerDisplayQuery::class.java)
?.displayOf(it)
?.kind
}

override val locale
get() =
ProxyServiceRegistry.get(PlayerLocaleQuery::class.java)
?.localeOf(player.uniqueId)
?: player.effectiveLocale
?: Locale.ENGLISH

override val hudReady
get() =
ProxyServiceRegistry.get(PlayerHudAppearanceQuery::class.java)
?.appearanceOf(player.uniqueId)
?.ready == true

override fun show(bar: net.kyori.adventure.bossbar.BossBar) {
player.showBossBar(bar)
}

override fun hide(bar: net.kyori.adventure.bossbar.BossBar) {
player.hideBossBar(bar)
}
}
}
},
settings = { announcementSettings },
)
announcementController = controller
val sync =
AnnouncementSynchronizer(
store::read,
{
announcementSettings = it
controller.refreshAll()
},
{
logger.warn(
"Announcement refresh failed; retaining last valid snapshot ({})",
it.javaClass.simpleName,
)
},
)
announcementSync = sync
announcementPoll =
proxy.scheduler
.buildTask(this, Runnable { sync.refresh() })
.repeat(Duration.ofSeconds(15))
.schedule()
announcementTick =
proxy.scheduler
.buildTask(this, Runnable { controller.refreshAll() })
.repeat(Duration.ofSeconds(1))
.schedule()
crossProxySubscriptions +=
natsHandler.subscribe("config.velocity.$environment.changed") {
if (announcementQueued.compareAndSet(false, true)) {
proxy.scheduler
.buildTask(
this,
Runnable {
announcementQueued.set(false)
sync.refresh()
},
)
.delay(Duration.ofMillis(250))
.schedule()
}
}
}

/**
* Bring up the Prometheus endpoint, if this proxy was asked for one.
*
Expand Down
Loading
Loading