Multi world support
This commit is contained in:
1 parent
91940a6f0a
commit
1df4776b68
12 files changed
+501
-52
No files matched your search
@@ -1,5 +1,17 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
NotAlexNoyle 08/05/26:
|
||||||
|
|
||||||
|
- Add multi world game chat. Worlds named `<prefix><number>-hub` or `<prefix><number>-<map>` get their
|
||||||
|
own isolated chat and link to the Discord channel configured under `discord.games` for that prefix.
|
||||||
|
- Tell a game's worlds apart in Discord with the configurable `gameLobbyLabel` and `gameWorldLabel`.
|
||||||
|
- Route joins, quits, kicks, deaths and advancements to the world's channel. The main world and its
|
||||||
|
nether and end keep using the general channel; any other world is not reported.
|
||||||
|
- Post a joined and left pair to a game channel when a player is teleported in or out of its worlds,
|
||||||
|
so its player count stays balanced.
|
||||||
|
- Add `ChatOGAPI`, letting a plugin register a `WorldChatFormatter` for the worlds it owns.
|
||||||
|
- Stop relaying a message to Discord when the blocklist has already suppressed it in game.
|
||||||
|
|
||||||
NotAlexNoyle 05/06/26:
|
NotAlexNoyle 05/06/26:
|
||||||
|
|
||||||
- Port all uses of PlaceholderAPI to MiniPlaceholders / Utilities-OG APIs.
|
- Port all uses of PlaceholderAPI to MiniPlaceholders / Utilities-OG APIs.
|
||||||
|
|||||||
@@ -7,14 +7,119 @@ Chat plugin for [TrueOG](https://github.com/true-og/true-og)
|
|||||||
- Translates chat messages using any OpenAI(-compatible) API. Click any chat or Discord message to translate it on-demand, and run command `/translatesettings <language>` to pick your preferred language (preferences persisted in KeyDB).
|
- Translates chat messages using any OpenAI(-compatible) API. Click any chat or Discord message to translate it on-demand, and run command `/translatesettings <language>` to pick your preferred language (preferences persisted in KeyDB).
|
||||||
- Discord bridge with custom emoji (Minecraft → Discord), animated NQN emoji support, clickable attachment links, and Unicode emoji-to-shortcode conversion (Emoji 17.0).
|
- Discord bridge with custom emoji (Minecraft → Discord), animated NQN emoji support, clickable attachment links, and Unicode emoji-to-shortcode conversion (Emoji 17.0).
|
||||||
- Multi-channel chat: general (`/g` / `/gc`), staff (`/s` / `/sc`), premium (`/p` / `/pc`) and developer (`/d` / `/dc`), each with its own Discord webhook.
|
- Multi-channel chat: general (`/g` / `/gc`), staff (`/s` / `/sc`), premium (`/p` / `/pc`) and developer (`/d` / `/dc`), each with its own Discord webhook.
|
||||||
- Private messaging (`/msg`, `/whisper`, `/pm`) with `/r` and `/reply` shortcuts.
|
- Multi world game chat: every world of a game gets its own chat, and all of a game's worlds share one Discord channel.
|
||||||
|
- Private messaging (`/msg`, `/whisper`, `/pm`) with `/r` and `/reply` shortcuts, working across every world.
|
||||||
- Forwards joins, quits, kicks, advancements, deaths and broadcasts to Discord, with full Vanish-OG awareness so vanished players never leak into Discord.
|
- Forwards joins, quits, kicks, advancements, deaths and broadcasts to Discord, with full Vanish-OG awareness so vanished players never leak into Discord.
|
||||||
- LuckPerms-driven Discord role color mapping so that player rank based formatting permissions carry over to Discord.
|
- LuckPerms-driven Discord role color mapping so that player rank based formatting permissions carry over to Discord.
|
||||||
- Pings in chat alert the mentioned player. Words both ways from Minecraft <-> Discord.
|
- Pings in chat alert the mentioned player. Works both ways from Minecraft <-> Discord.
|
||||||
- Censor `@everyone`, `@here` and role mentions on the Discord side.
|
- Censor `@everyone`, `@here` and role mentions on the Discord side.
|
||||||
- API for other plugins to send messages through the Discord bridge.
|
- API for other plugins to send messages through the Discord bridge and to style multi world game chat.
|
||||||
- Run command `/chatconfigreload` to fully reload the config and the Discord bridge at runtime.
|
- Run command `/chatconfigreload` to fully reload the config and the Discord bridge at runtime.
|
||||||
|
|
||||||
|
## Multi world games
|
||||||
|
|
||||||
|
A game is a set of worlds sharing one Discord channel. Worlds are matched by name: `HB1-hub` is a
|
||||||
|
lobby, `HB1-Ancient_Plateau` is a live game world, and both belong to the `HB` channel.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
discord:
|
||||||
|
games:
|
||||||
|
HB:
|
||||||
|
enabled: true
|
||||||
|
channelId: "..."
|
||||||
|
webhook: "https://discord.com/api/webhooks/..."
|
||||||
|
|
||||||
|
# %id% is the lobby id, for example HB1.
|
||||||
|
gameLobbyLabel: "[%id% - Lobby]"
|
||||||
|
gameWorldLabel: "[%id% - In Game]"
|
||||||
|
```
|
||||||
|
|
||||||
|
- Chat in a game world reaches only that world, plus the game's Discord channel.
|
||||||
|
- Joins, quits, kicks, deaths and advancements follow the same routing. The main world and its nether
|
||||||
|
and end keep using the general channel; any other world is not reported.
|
||||||
|
- Staff, premium and developer chat, private messages and `/broadcast` stay global in every world.
|
||||||
|
- `games` ships empty, so nothing changes until it is filled in.
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
Add Chat-OG as a submodule:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git submodule add https://github.com/true-og/Chat-OG libs/Chat-OG
|
||||||
|
```
|
||||||
|
|
||||||
|
`build.gradle.kts`:
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
extra["kotlinAttribute"] = Attribute.of("kotlin-tag", Boolean::class.javaObjectType)
|
||||||
|
val kotlinAttribute: Attribute<Boolean> by rootProject.extra
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
compileOnlyApi(project(":libs:Chat-OG")) { attributes { attribute(kotlinAttribute, true) } }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`plugin.yml`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
softdepend:
|
||||||
|
- Chat-OG
|
||||||
|
```
|
||||||
|
|
||||||
|
### Styling a game's chat
|
||||||
|
|
||||||
|
Chat-OG decides who receives a message; your plugin decides how it looks. Register one formatter per
|
||||||
|
game key, the same key used under `discord.games`.
|
||||||
|
|
||||||
|
Kotlin:
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
ChatOGAPI.setFormatter("HB") { sender, message, worldName, lobbyId ->
|
||||||
|
Component.join(
|
||||||
|
JoinConfiguration.noSeparators(),
|
||||||
|
UtilitiesOG.trueogColorize("&9${sender.name}&8 » &r"),
|
||||||
|
message,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Java:
|
||||||
|
|
||||||
|
```java
|
||||||
|
ChatOGAPI.setFormatter("HB", (sender, message, worldName, lobbyId) ->
|
||||||
|
Component.join(JoinConfiguration.noSeparators(),
|
||||||
|
UtilitiesOG.trueogColorize("&9" + sender.getName() + "&8 » &r"),
|
||||||
|
message));
|
||||||
|
```
|
||||||
|
|
||||||
|
`message` is already blocklist checked, colorized and permission gated, so build around it instead of
|
||||||
|
turning it back into a string. `trueogColorize` accepts both modern and legacy color codes.
|
||||||
|
|
||||||
|
Return `null` to use Chat-OG's default format, or `Component.empty()` to drop the message. A
|
||||||
|
formatter that throws is logged and falls back to the default, so it cannot break chat.
|
||||||
|
|
||||||
|
### `ChatOGAPI`
|
||||||
|
|
||||||
|
| Method | Purpose |
|
||||||
|
| --- | --- |
|
||||||
|
| `isAvailable()` | Whether Chat-OG is enabled and ready. |
|
||||||
|
| `setFormatter(key, formatter)` | Sets a game's chat formatter. Pass `null` to clear it. |
|
||||||
|
| `getLobbyId(worldName)` | The lobby id for a world, e.g. `HB1`, or `null` if it is not a game world. |
|
||||||
|
| `broadcast(worldName, message)` | Sends a message to everyone in a world, in game only. |
|
||||||
|
|
||||||
|
All of them are safe to call before Chat-OG loads.
|
||||||
|
|
||||||
|
### Sending to Discord
|
||||||
|
|
||||||
|
`ChatAPI` posts to the built-in channels and is registered with Bukkit's `ServicesManager`:
|
||||||
|
|
||||||
|
```java
|
||||||
|
ChatAPI api = Bukkit.getServicesManager().getRegistration(ChatAPI.class).getProvider();
|
||||||
|
api.sendMessage("Hello", "SomeName", uuid);
|
||||||
|
```
|
||||||
|
|
||||||
|
It also offers `sendStaffMessage`, `sendPremiumMessage`, `sendMessageWithBot` and `sendEmbed`.
|
||||||
|
|
||||||
## Building
|
## Building
|
||||||
```./gradlew clean build eclipse --warning-mode all```
|
```./gradlew clean build eclipse --warning-mode all```
|
||||||
|
|
||||||
|
|||||||
@@ -52,6 +52,11 @@ internal class ChatOG : JavaPlugin() {
|
|||||||
val discordBridgeLock = ReentrantReadWriteLock()
|
val discordBridgeLock = ReentrantReadWriteLock()
|
||||||
var lastMessagedMap: MutableMap<UUID, UUID> = HashMap()
|
var lastMessagedMap: MutableMap<UUID, UUID> = HashMap()
|
||||||
|
|
||||||
|
// Only true once onEnable has fully succeeded, so the API never runs against a half started plugin.
|
||||||
|
@Volatile private var ready = false
|
||||||
|
|
||||||
|
fun isReady() = ready
|
||||||
|
|
||||||
fun isConfigInitialized() = ::config.isInitialized
|
fun isConfigInitialized() = ::config.isInitialized
|
||||||
|
|
||||||
fun isLanguageDatabaseInitialized() = ::languageDatabase.isInitialized
|
fun isLanguageDatabaseInitialized() = ::languageDatabase.isInitialized
|
||||||
@@ -130,9 +135,13 @@ internal class ChatOG : JavaPlugin() {
|
|||||||
|
|
||||||
val chatAPI = ChatAPI()
|
val chatAPI = ChatAPI()
|
||||||
this.server.servicesManager.register(ChatAPI::class.java, chatAPI, this, ServicePriority.Normal)
|
this.server.servicesManager.register(ChatAPI::class.java, chatAPI, this, ServicePriority.Normal)
|
||||||
|
|
||||||
|
ready = true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDisable() {
|
override fun onDisable() {
|
||||||
|
ready = false
|
||||||
|
|
||||||
if (isConfigInitialized()) {
|
if (isConfigInitialized()) {
|
||||||
if (discordBridge != null) {
|
if (discordBridge != null) {
|
||||||
discordBridgeLock.read {
|
discordBridgeLock.read {
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ import nl.skbotnl.chatog.ChatOG.Companion.config
|
|||||||
import nl.skbotnl.chatog.ChatOG.Companion.discordBridge
|
import nl.skbotnl.chatog.ChatOG.Companion.discordBridge
|
||||||
import nl.skbotnl.chatog.ChatOG.Companion.discordBridgeLock
|
import nl.skbotnl.chatog.ChatOG.Companion.discordBridgeLock
|
||||||
import nl.skbotnl.chatog.ChatOG.Companion.scope
|
import nl.skbotnl.chatog.ChatOG.Companion.scope
|
||||||
|
import nl.skbotnl.chatog.chatsystem.GeneralChatSystem
|
||||||
|
import nl.skbotnl.chatog.chatsystem.WorldChatSystem
|
||||||
import nl.skbotnl.chatog.util.ChatUtil
|
import nl.skbotnl.chatog.util.ChatUtil
|
||||||
import nl.skbotnl.chatog.util.ChatUtil.legacyToMm
|
import nl.skbotnl.chatog.util.ChatUtil.legacyToMm
|
||||||
import nl.skbotnl.chatog.util.PlayerExtensions.chatSystem
|
import nl.skbotnl.chatog.util.PlayerExtensions.chatSystem
|
||||||
@@ -25,6 +27,7 @@ import org.bukkit.event.EventHandler
|
|||||||
import org.bukkit.event.Listener
|
import org.bukkit.event.Listener
|
||||||
import org.bukkit.event.entity.PlayerDeathEvent
|
import org.bukkit.event.entity.PlayerDeathEvent
|
||||||
import org.bukkit.event.player.PlayerAdvancementDoneEvent
|
import org.bukkit.event.player.PlayerAdvancementDoneEvent
|
||||||
|
import org.bukkit.event.player.PlayerChangedWorldEvent
|
||||||
import org.bukkit.event.player.PlayerJoinEvent
|
import org.bukkit.event.player.PlayerJoinEvent
|
||||||
import org.bukkit.event.player.PlayerKickEvent
|
import org.bukkit.event.player.PlayerKickEvent
|
||||||
import org.bukkit.event.player.PlayerQuitEvent
|
import org.bukkit.event.player.PlayerQuitEvent
|
||||||
@@ -32,6 +35,21 @@ import org.bukkit.event.server.BroadcastMessageEvent
|
|||||||
import xyz.jpenilla.announcerplus.listener.JoinQuitListener
|
import xyz.jpenilla.announcerplus.listener.JoinQuitListener
|
||||||
|
|
||||||
internal class Events : Listener {
|
internal class Events : Listener {
|
||||||
|
// The main world and its nether and end, taken from level-name rather than assumed to be "world".
|
||||||
|
private fun isMainWorld(worldName: String): Boolean {
|
||||||
|
val main = Bukkit.getWorlds().firstOrNull()?.name ?: return false
|
||||||
|
return worldName.equals(main, true) ||
|
||||||
|
worldName.equals("${main}_nether", true) ||
|
||||||
|
worldName.equals("${main}_the_end", true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// A multi world game world reports to its own channel, the main world to general, anything else nowhere.
|
||||||
|
private fun reports(worldName: String, key: String?) = key != null || isMainWorld(worldName)
|
||||||
|
|
||||||
|
// A game channel counts only its own players, since the server total would mislead there.
|
||||||
|
private fun onlineCount(key: String?) =
|
||||||
|
if (key == null) Bukkit.getOnlinePlayers().count() else WorldChatSystem.playersForKey(key).size
|
||||||
|
|
||||||
@EventHandler
|
@EventHandler
|
||||||
fun onJoin(event: PlayerJoinEvent) {
|
fun onJoin(event: PlayerJoinEvent) {
|
||||||
if (!config.discord.enabled) {
|
if (!config.discord.enabled) {
|
||||||
@@ -41,18 +59,17 @@ internal class Events : Listener {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val worldName = event.player.world.name
|
||||||
|
val key = WorldChatSystem.keyForWorld(worldName)
|
||||||
|
if (!reports(worldName, key)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
val playerPartString = ChatUtil.getPlayerPartString(event.player)
|
val playerPartString = ChatUtil.getPlayerPartString(event.player)
|
||||||
|
val message = "$playerPartString has joined the game. ${onlineCount(key)} player(s) online."
|
||||||
|
|
||||||
scope.launch {
|
scope.launch {
|
||||||
discordBridgeLock.read {
|
discordBridgeLock.read { discordBridge?.sendEmbed(message, event.player.uniqueId, 0x00FF00, key) }
|
||||||
discordBridge?.sendEmbed(
|
|
||||||
"$playerPartString has joined the game. ${
|
|
||||||
Bukkit.getOnlinePlayers().count()
|
|
||||||
} player(s) online.",
|
|
||||||
event.player.uniqueId,
|
|
||||||
0x00FF00,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,17 +82,47 @@ internal class Events : Listener {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val worldName = event.player.world.name
|
||||||
|
val key = WorldChatSystem.keyForWorld(worldName)
|
||||||
|
if (!reports(worldName, key)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
val playerPartString = ChatUtil.getPlayerPartString(event.player)
|
val playerPartString = ChatUtil.getPlayerPartString(event.player)
|
||||||
|
val message = "$playerPartString has left the game. ${onlineCount(key) - 1} player(s) online."
|
||||||
|
|
||||||
|
scope.launch {
|
||||||
|
discordBridgeLock.read { discordBridge?.sendEmbed(message, event.player.uniqueId, 0xFF0000, key) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Game players are teleported in rather than connecting, so without this a game channel only sees leaves.
|
||||||
|
@EventHandler
|
||||||
|
fun onWorldChange(event: PlayerChangedWorldEvent) {
|
||||||
|
if (!config.discord.enabled) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (VanishAPI.isVanished(event.player)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only the game channels track a per world population; general counts the whole server, which
|
||||||
|
// a world change does not alter.
|
||||||
|
val fromKey = WorldChatSystem.keyForWorld(event.from.name)
|
||||||
|
val toKey = WorldChatSystem.keyForWorld(event.player.world.name)
|
||||||
|
if (fromKey == toKey) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val playerPartString = ChatUtil.getPlayerPartString(event.player)
|
||||||
|
val leftMessage = fromKey?.let { "$playerPartString has left the game. ${onlineCount(it)} player(s) online." }
|
||||||
|
val joinedMessage = toKey?.let { "$playerPartString has joined the game. ${onlineCount(it)} player(s) online." }
|
||||||
|
|
||||||
scope.launch {
|
scope.launch {
|
||||||
discordBridgeLock.read {
|
discordBridgeLock.read {
|
||||||
discordBridge?.sendEmbed(
|
if (leftMessage != null) discordBridge?.sendEmbed(leftMessage, event.player.uniqueId, 0xFF0000, fromKey)
|
||||||
"$playerPartString has left the game. ${
|
if (joinedMessage != null)
|
||||||
Bukkit.getOnlinePlayers().count() - 1
|
discordBridge?.sendEmbed(joinedMessage, event.player.uniqueId, 0x00FF00, toKey)
|
||||||
} player(s) online.",
|
|
||||||
event.player.uniqueId,
|
|
||||||
0xFF0000,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -89,20 +136,20 @@ internal class Events : Listener {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val worldName = event.player.world.name
|
||||||
|
val key = WorldChatSystem.keyForWorld(worldName)
|
||||||
|
if (!reports(worldName, key)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
val playerPartString = ChatUtil.getPlayerPartString(event.player)
|
val playerPartString = ChatUtil.getPlayerPartString(event.player)
|
||||||
|
|
||||||
val reason = PlainTextComponentSerializer.plainText().serialize(event.reason())
|
val reason = PlainTextComponentSerializer.plainText().serialize(event.reason())
|
||||||
|
val message =
|
||||||
|
"$playerPartString was kicked with reason: \"${reason}\". ${onlineCount(key) - 1} player(s) online."
|
||||||
|
|
||||||
scope.launch {
|
scope.launch {
|
||||||
discordBridgeLock.read {
|
discordBridgeLock.read { discordBridge?.sendEmbed(message, event.player.uniqueId, 0xFF0000, key) }
|
||||||
discordBridge?.sendEmbed(
|
|
||||||
"$playerPartString was kicked with reason: \"${reason}\". ${
|
|
||||||
Bukkit.getOnlinePlayers().count() - 1
|
|
||||||
} player(s) online.",
|
|
||||||
event.player.uniqueId,
|
|
||||||
0xFF0000,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,6 +162,12 @@ internal class Events : Listener {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val worldName = event.player.world.name
|
||||||
|
val key = WorldChatSystem.keyForWorld(worldName)
|
||||||
|
if (!reports(worldName, key)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
val playerPartString = ChatUtil.getPlayerPartString(event.player)
|
val playerPartString = ChatUtil.getPlayerPartString(event.player)
|
||||||
|
|
||||||
val advancementTitleKey = event.advancement.display?.title() ?: return
|
val advancementTitleKey = event.advancement.display?.title() ?: return
|
||||||
@@ -132,7 +185,7 @@ internal class Events : Listener {
|
|||||||
|
|
||||||
scope.launch {
|
scope.launch {
|
||||||
discordBridgeLock.read {
|
discordBridgeLock.read {
|
||||||
discordBridge?.sendEmbed("$playerPartString $advancementMessage.", event.player.uniqueId, 0xFFFF00)
|
discordBridge?.sendEmbed("$playerPartString $advancementMessage.", event.player.uniqueId, 0xFFFF00, key)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -161,9 +214,15 @@ internal class Events : Listener {
|
|||||||
if (event.isCancelled) return
|
if (event.isCancelled) return
|
||||||
event.isCancelled = true
|
event.isCancelled = true
|
||||||
|
|
||||||
|
val worldName = event.player.world.name
|
||||||
|
|
||||||
scope.launch {
|
scope.launch {
|
||||||
val eventMessage = event.message() as TextComponent
|
val eventMessage = event.message() as TextComponent
|
||||||
event.player.chatSystem.sendMessage(eventMessage.content(), event.player)
|
val chatSystem = event.player.chatSystem
|
||||||
|
// A multi world game world replaces general chat only, so staff, premium and developer stay global.
|
||||||
|
val effective =
|
||||||
|
if (chatSystem === GeneralChatSystem) WorldChatSystem.forWorld(worldName) ?: chatSystem else chatSystem
|
||||||
|
effective.sendMessage(eventMessage.content(), event.player)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -176,11 +235,18 @@ internal class Events : Listener {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The in game death message is still rewritten below even when the world reports nowhere.
|
||||||
|
val worldName = event.player.world.name
|
||||||
|
val key = WorldChatSystem.keyForWorld(worldName)
|
||||||
|
val report = reports(worldName, key)
|
||||||
|
|
||||||
val deathMessage = event.deathMessage()
|
val deathMessage = event.deathMessage()
|
||||||
if (deathMessage is TextComponent) {
|
if (deathMessage is TextComponent) {
|
||||||
scope.launch {
|
if (report) {
|
||||||
discordBridgeLock.read {
|
scope.launch {
|
||||||
discordBridge?.sendEmbed(deathMessage.content(), event.player.uniqueId, 0xFF0000)
|
discordBridgeLock.read {
|
||||||
|
discordBridge?.sendEmbed(deathMessage.content(), event.player.uniqueId, 0xFF0000, key)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
@@ -195,12 +261,16 @@ internal class Events : Listener {
|
|||||||
val newDeathMessage = builder.build()
|
val newDeathMessage = builder.build()
|
||||||
|
|
||||||
event.deathMessage(newDeathMessage)
|
event.deathMessage(newDeathMessage)
|
||||||
|
if (!report) {
|
||||||
|
return
|
||||||
|
}
|
||||||
scope.launch {
|
scope.launch {
|
||||||
discordBridgeLock.read {
|
discordBridgeLock.read {
|
||||||
discordBridge?.sendEmbed(
|
discordBridge?.sendEmbed(
|
||||||
PlainTextComponentSerializer.plainText().serialize(newDeathMessage),
|
PlainTextComponentSerializer.plainText().serialize(newDeathMessage),
|
||||||
event.player.uniqueId,
|
event.player.uniqueId,
|
||||||
0xFF0000,
|
0xFF0000,
|
||||||
|
key,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package nl.skbotnl.chatog.api
|
||||||
|
|
||||||
|
import net.kyori.adventure.text.Component
|
||||||
|
import nl.skbotnl.chatog.ChatOG
|
||||||
|
import nl.skbotnl.chatog.chatsystem.WorldChatSystem
|
||||||
|
|
||||||
|
// Multi world game chat API. Worlds are bound to Discord channels in Chat-OG's config, so a plugin
|
||||||
|
// only ever supplies formatting. Callers must declare softdepend: [Chat-OG] in their plugin.yml.
|
||||||
|
object ChatOGAPI {
|
||||||
|
// False until Chat-OG has fully enabled, and again once it disables.
|
||||||
|
@JvmStatic fun isAvailable(): Boolean = ChatOG.isReady()
|
||||||
|
|
||||||
|
// Sets the chat formatter for a game key, for example "HB". False when Chat-OG is not ready.
|
||||||
|
@JvmStatic
|
||||||
|
fun setFormatter(key: String, formatter: WorldChatFormatter?): Boolean {
|
||||||
|
if (!isAvailable()) return false
|
||||||
|
WorldChatSystem.setFormatter(key, formatter)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lobby id for a world, for example "HB1", or null if it is not part of a multi world game.
|
||||||
|
@JvmStatic
|
||||||
|
fun getLobbyId(worldName: String): String? {
|
||||||
|
if (!isAvailable()) return null
|
||||||
|
return WorldChatSystem.forWorld(worldName)?.id
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sends a message to everyone in a world and returns how many players received it.
|
||||||
|
@JvmStatic
|
||||||
|
fun broadcast(worldName: String, message: Component): Int {
|
||||||
|
if (!isAvailable()) return 0
|
||||||
|
return WorldChatSystem.broadcast(worldName, message)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package nl.skbotnl.chatog.api
|
||||||
|
|
||||||
|
import net.kyori.adventure.text.Component
|
||||||
|
import org.bukkit.entity.Player
|
||||||
|
|
||||||
|
// Lets a plugin render its own chat line for the multi world game it owns.
|
||||||
|
fun interface WorldChatFormatter {
|
||||||
|
// The message is already blocklist checked, colorized and permission gated by Chat-OG.
|
||||||
|
// Never serialize it back to a string and re-parse it, or players without chat-og.color regain formatting.
|
||||||
|
// Compose instead, for example Component.join(noSeparators(), UtilitiesOG.trueogColorize(prefix), message).
|
||||||
|
// Return the finished line, null to use Chat-OG's default format, or Component.empty() to drop the message.
|
||||||
|
fun format(sender: Player, message: Component, worldName: String, lobbyId: String): Component?
|
||||||
|
}
|
||||||
@@ -39,24 +39,35 @@ internal abstract class ChatSystem {
|
|||||||
|
|
||||||
abstract fun sendDiscordMessage(text: String, playerPartString: String, uuid: UUID)
|
abstract fun sendDiscordMessage(text: String, playerPartString: String, uuid: UUID)
|
||||||
|
|
||||||
|
// Lets a subclass render its own chat line. Null keeps the default format, empty drops the message.
|
||||||
|
protected open fun formatLine(player: Player, message: Component): Component? = null
|
||||||
|
|
||||||
fun sendMessage(text: String, player: Player) {
|
fun sendMessage(text: String, player: Player) {
|
||||||
|
val messageComponent =
|
||||||
|
ChatUtil.processText(text, player)?.color(PlayerUtils.getMessageColor(player.uniqueId)) ?: return
|
||||||
|
|
||||||
|
// Runs before the relay so a dropped message never reaches Discord either.
|
||||||
|
val formattedLine = formatLine(player, messageComponent)
|
||||||
|
if (formattedLine == Component.empty()) return
|
||||||
|
|
||||||
var playerPartString = ChatUtil.getPlayerPartString(player)
|
var playerPartString = ChatUtil.getPlayerPartString(player)
|
||||||
playerPartString = listOfNotNull(prefix, playerPartString).joinToString(" | ")
|
playerPartString = listOfNotNull(prefix, playerPartString).joinToString(" | ")
|
||||||
|
|
||||||
val discordPlayerPartString =
|
val discordPlayerPartString =
|
||||||
listOfNotNull(prefix, ChatUtil.getPlayerPartString(player, includeSuffix = true)).joinToString(" | ")
|
listOfNotNull(prefix, ChatUtil.getPlayerPartString(player, includeSuffix = true)).joinToString(" | ")
|
||||||
|
|
||||||
|
// Relayed only after the blocklist check so a suppressed message never reaches Discord.
|
||||||
sendDiscordMessage(resolveChatItems(text), discordPlayerPartString, player.uniqueId)
|
sendDiscordMessage(resolveChatItems(text), discordPlayerPartString, player.uniqueId)
|
||||||
|
|
||||||
val messageComponent =
|
var textComponent =
|
||||||
ChatUtil.processText(text, player)?.color(PlayerUtils.getMessageColor(player.uniqueId)) ?: return
|
formattedLine
|
||||||
|
?: Component.join(
|
||||||
val chatComponent =
|
JoinConfiguration.noSeparators(),
|
||||||
UtilitiesOG.trueogColorize(
|
UtilitiesOG.trueogColorize(
|
||||||
ChatUtil.legacyToMm("$playerPartString<reset>${PlayerUtils.getSuffix(player.uniqueId)} &7> ")
|
ChatUtil.legacyToMm("$playerPartString<reset>${PlayerUtils.getSuffix(player.uniqueId)} &7> ")
|
||||||
)
|
),
|
||||||
|
messageComponent,
|
||||||
var textComponent = Component.join(JoinConfiguration.noSeparators(), chatComponent, messageComponent)
|
)
|
||||||
textComponent =
|
textComponent =
|
||||||
textComponent.hoverEvent(
|
textComponent.hoverEvent(
|
||||||
HoverEvent.hoverEvent(
|
HoverEvent.hoverEvent(
|
||||||
|
|||||||
@@ -13,8 +13,11 @@ import org.bukkit.Bukkit
|
|||||||
|
|
||||||
internal object GeneralChatSystem : ChatSystem() {
|
internal object GeneralChatSystem : ChatSystem() {
|
||||||
override val prefix = null
|
override val prefix = null
|
||||||
|
|
||||||
|
// Multi world game worlds have their own chat, so global chat must not reach into them either.
|
||||||
override val audience: Audience
|
override val audience: Audience
|
||||||
get() = Audience.audience(Bukkit.getOnlinePlayers())
|
get() =
|
||||||
|
Audience.audience(Bukkit.getOnlinePlayers().filter { WorldChatSystem.keyForWorld(it.world.name) == null })
|
||||||
|
|
||||||
override val name = "general"
|
override val name = "general"
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,106 @@
|
|||||||
|
package nl.skbotnl.chatog.chatsystem
|
||||||
|
|
||||||
|
import java.util.*
|
||||||
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
import kotlin.concurrent.read
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import net.kyori.adventure.audience.Audience
|
||||||
|
import net.kyori.adventure.text.Component
|
||||||
|
import nl.skbotnl.chatog.ChatOG.Companion.config
|
||||||
|
import nl.skbotnl.chatog.ChatOG.Companion.discordBridge
|
||||||
|
import nl.skbotnl.chatog.ChatOG.Companion.discordBridgeLock
|
||||||
|
import nl.skbotnl.chatog.ChatOG.Companion.plugin
|
||||||
|
import nl.skbotnl.chatog.ChatOG.Companion.scope
|
||||||
|
import nl.skbotnl.chatog.api.WorldChatFormatter
|
||||||
|
import nl.skbotnl.chatog.util.ChatUtil
|
||||||
|
import org.bukkit.Bukkit
|
||||||
|
import org.bukkit.entity.Player
|
||||||
|
|
||||||
|
// Chat for one world of a multi world game, isolated in game but sharing the game's Discord channel.
|
||||||
|
internal class WorldChatSystem(private val worldName: String, val key: String, val id: String, lobby: Boolean) :
|
||||||
|
ChatSystem() {
|
||||||
|
// No label in game, since everyone who can read this chat is already in the same world.
|
||||||
|
override val prefix = null
|
||||||
|
|
||||||
|
// The label only tags the webhook username, which is what tells a game's worlds apart in Discord.
|
||||||
|
private val label =
|
||||||
|
(if (lobby) config.discord.gameLobbyLabel ?: DEFAULT_LOBBY_LABEL
|
||||||
|
else config.discord.gameWorldLabel ?: DEFAULT_WORLD_LABEL)
|
||||||
|
.replace("%id%", id)
|
||||||
|
|
||||||
|
// Filtering the online players avoids touching World.getPlayers() from the async chat coroutine.
|
||||||
|
override val audience: Audience
|
||||||
|
get() = Audience.audience(Bukkit.getOnlinePlayers().filter { it.world.name == worldName })
|
||||||
|
|
||||||
|
override val name = "world:$worldName"
|
||||||
|
|
||||||
|
override fun sendDiscordMessage(text: String, playerPartString: String, uuid: UUID) {
|
||||||
|
if (!config.discord.enabled || config.discord.games[key]?.enabled != true) return
|
||||||
|
val discordMessageString = ChatUtil.convertEmojis(text)
|
||||||
|
|
||||||
|
scope.launch {
|
||||||
|
discordBridgeLock.read {
|
||||||
|
discordBridge?.sendGameMessage(key, discordMessageString, "$label $playerPartString", uuid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A formatter belongs to another plugin, so a failure there must not take the chat line down with it.
|
||||||
|
override fun formatLine(player: Player, message: Component): Component? {
|
||||||
|
val formatter = formatters[key.uppercase()] ?: return null
|
||||||
|
return try {
|
||||||
|
formatter.format(player, message, worldName, id)
|
||||||
|
} catch (throwable: Throwable) {
|
||||||
|
plugin.logger.warning("The chat formatter for game \"$key\" failed: $throwable")
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val DEFAULT_LOBBY_LABEL = "[%id% - Lobby]"
|
||||||
|
private const val DEFAULT_WORLD_LABEL = "[%id% - In Game]"
|
||||||
|
|
||||||
|
// Keyed by uppercased game key so registration order and config casing cannot disagree.
|
||||||
|
private val formatters = ConcurrentHashMap<String, WorldChatFormatter>()
|
||||||
|
|
||||||
|
// Worlds are named <prefix><number>-hub for a lobby and <prefix><number>-<map> for a game, so HB1-hub and
|
||||||
|
// HB1-Ancient_Plateau both belong to the configured discord.games key "HB".
|
||||||
|
fun forWorld(worldName: String): WorldChatSystem? {
|
||||||
|
var index = 0
|
||||||
|
while (index < worldName.length && worldName[index].isLetter()) index++
|
||||||
|
if (index == 0) return null
|
||||||
|
|
||||||
|
val digitStart = index
|
||||||
|
while (index < worldName.length && worldName[index].isDigit()) index++
|
||||||
|
if (index == digitStart) return null
|
||||||
|
|
||||||
|
if (index >= worldName.length || worldName[index] != '-') return null
|
||||||
|
val rest = worldName.substring(index + 1)
|
||||||
|
if (rest.isEmpty()) return null
|
||||||
|
|
||||||
|
val prefix = worldName.substring(0, digitStart)
|
||||||
|
// Resolved from the configured key alone, so turning a game's Discord channel off does not
|
||||||
|
// also drop its worlds back into global chat.
|
||||||
|
val key = config.discord.games.keys.firstOrNull { it.equals(prefix, ignoreCase = true) } ?: return null
|
||||||
|
|
||||||
|
return WorldChatSystem(worldName, key, worldName.substring(0, index), rest.equals("hub", true))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun keyForWorld(worldName: String): String? = forWorld(worldName)?.key
|
||||||
|
|
||||||
|
fun playersForKey(key: String): List<Player> =
|
||||||
|
Bukkit.getOnlinePlayers().filter { keyForWorld(it.world.name) == key }
|
||||||
|
|
||||||
|
// A null formatter clears the one registered for that game.
|
||||||
|
fun setFormatter(key: String, formatter: WorldChatFormatter?) {
|
||||||
|
if (formatter == null) formatters.remove(key.uppercase()) else formatters[key.uppercase()] = formatter
|
||||||
|
}
|
||||||
|
|
||||||
|
// In game only. Chat is what belongs in Discord, not every game announcement.
|
||||||
|
fun broadcast(worldName: String, message: Component): Int {
|
||||||
|
val recipients = Bukkit.getOnlinePlayers().filter { it.world.name == worldName }
|
||||||
|
Audience.audience(recipients).sendMessage(message)
|
||||||
|
return recipients.size
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,6 +18,13 @@ constructor(
|
|||||||
@param:JsonProperty("staff") val staff: DiscordRoleChatConfig,
|
@param:JsonProperty("staff") val staff: DiscordRoleChatConfig,
|
||||||
@param:JsonProperty("premium") val premium: DiscordRoleChatConfig,
|
@param:JsonProperty("premium") val premium: DiscordRoleChatConfig,
|
||||||
@param:JsonProperty("developer") val developer: DiscordRoleChatConfig,
|
@param:JsonProperty("developer") val developer: DiscordRoleChatConfig,
|
||||||
|
// Multi world game channels keyed by lobby prefix, e.g. "HB" for the worlds HB1-hub and HB1-<map>.
|
||||||
|
@param:JsonProperty("games")
|
||||||
|
@field:JsonSetter(nulls = Nulls.AS_EMPTY)
|
||||||
|
val games: Map<String, DiscordRoleChatConfig>,
|
||||||
|
// Null falls back to the defaults in WorldChatSystem so older configs keep loading.
|
||||||
|
@param:JsonProperty("gameLobbyLabel") val gameLobbyLabel: String?,
|
||||||
|
@param:JsonProperty("gameWorldLabel") val gameWorldLabel: String?,
|
||||||
@param:JsonProperty("listCommandName") val listCommandName: String,
|
@param:JsonProperty("listCommandName") val listCommandName: String,
|
||||||
@param:JsonProperty("listCommandText") val listCommandText: String,
|
@param:JsonProperty("listCommandText") val listCommandText: String,
|
||||||
@param:JsonProperty("useColorCodeRoles") val useColorCodeRoles: Boolean,
|
@param:JsonProperty("useColorCodeRoles") val useColorCodeRoles: Boolean,
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import net.kyori.adventure.text.format.TextColor
|
|||||||
import net.trueog.utilitiesog.UtilitiesOG
|
import net.trueog.utilitiesog.UtilitiesOG
|
||||||
import nl.skbotnl.chatog.ChatOG.Companion.config
|
import nl.skbotnl.chatog.ChatOG.Companion.config
|
||||||
import nl.skbotnl.chatog.ChatOG.Companion.plugin
|
import nl.skbotnl.chatog.ChatOG.Companion.plugin
|
||||||
|
import nl.skbotnl.chatog.chatsystem.WorldChatSystem
|
||||||
import nl.skbotnl.chatog.translation.command.TranslateMessage
|
import nl.skbotnl.chatog.translation.command.TranslateMessage
|
||||||
import nl.skbotnl.chatog.util.ChatUtil
|
import nl.skbotnl.chatog.util.ChatUtil
|
||||||
import nl.skbotnl.chatog.util.EmojiConverter
|
import nl.skbotnl.chatog.util.EmojiConverter
|
||||||
@@ -42,8 +43,13 @@ internal class DiscordBridge private constructor() {
|
|||||||
private var staffWebhook: WebhookClient? = null
|
private var staffWebhook: WebhookClient? = null
|
||||||
private var premiumWebhook: WebhookClient? = null
|
private var premiumWebhook: WebhookClient? = null
|
||||||
private var developerWebhook: WebhookClient? = null
|
private var developerWebhook: WebhookClient? = null
|
||||||
|
private val gameWebhooks = mutableMapOf<String, WebhookClient>()
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
// The discord.games key whose channel this is, or null when it is not a game channel.
|
||||||
|
private fun gameKeyOf(channelId: String): String? =
|
||||||
|
config.discord.games.entries.firstOrNull { it.value.enabled && it.value.channelId == channelId }?.key
|
||||||
|
|
||||||
fun create(): DiscordBridge? {
|
fun create(): DiscordBridge? {
|
||||||
val discordBridge = DiscordBridge()
|
val discordBridge = DiscordBridge()
|
||||||
if (config.discord.general.webhook != null) {
|
if (config.discord.general.webhook != null) {
|
||||||
@@ -84,6 +90,37 @@ internal class DiscordBridge private constructor() {
|
|||||||
} else if (config.discord.developer.enabled) {
|
} else if (config.discord.developer.enabled) {
|
||||||
plugin.logger.warning("You have enabled developer Discord but have not set up the developer webhook")
|
plugin.logger.warning("You have enabled developer Discord but have not set up the developer webhook")
|
||||||
}
|
}
|
||||||
|
for ((key, game) in config.discord.games) {
|
||||||
|
if (!game.enabled) continue
|
||||||
|
if (game.channelId == null) {
|
||||||
|
plugin.logger.warning(
|
||||||
|
"You have enabled game Discord \"$key\" but have not set up its channelId, so Discord to Minecraft will not work for it"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (game.webhook == null) {
|
||||||
|
plugin.logger.warning("You have enabled game Discord \"$key\" but have not set up its webhook")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
discordBridge.gameWebhooks[key] = WebhookClient.withUrl(game.webhook.toString())
|
||||||
|
} catch (_: IllegalArgumentException) {
|
||||||
|
plugin.logger.warning("Config option \"games.$key.webhook\" is invalid")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Two channels sharing an id would silently steal each other's messages, so say so loudly.
|
||||||
|
val channelIds =
|
||||||
|
listOfNotNull(
|
||||||
|
config.discord.general.channelId,
|
||||||
|
config.discord.staff.channelId.takeIf { config.discord.staff.enabled },
|
||||||
|
config.discord.premium.channelId.takeIf { config.discord.premium.enabled },
|
||||||
|
config.discord.developer.channelId.takeIf { config.discord.developer.enabled },
|
||||||
|
) + config.discord.games.values.filter { it.enabled }.mapNotNull { it.channelId }
|
||||||
|
channelIds
|
||||||
|
.groupingBy { it }
|
||||||
|
.eachCount()
|
||||||
|
.filterValues { it > 1 }
|
||||||
|
.keys
|
||||||
|
.forEach { plugin.logger.severe("Discord channel id \"$it\" is configured for more than one channel") }
|
||||||
|
|
||||||
if (config.discord.botToken == null) {
|
if (config.discord.botToken == null) {
|
||||||
plugin.logger.severe("You have enabled Discord but have not set up the bot token")
|
plugin.logger.severe("You have enabled Discord but have not set up the bot token")
|
||||||
@@ -146,7 +183,8 @@ internal class DiscordBridge private constructor() {
|
|||||||
(if (config.discord.premium.enabled) it.channel.id != config.discord.premium.channelId
|
(if (config.discord.premium.enabled) it.channel.id != config.discord.premium.channelId
|
||||||
else true) &&
|
else true) &&
|
||||||
(if (config.discord.developer.enabled) it.channel.id != config.discord.developer.channelId
|
(if (config.discord.developer.enabled) it.channel.id != config.discord.developer.channelId
|
||||||
else true)
|
else true) &&
|
||||||
|
gameKeyOf(it.channel.id) == null
|
||||||
) {
|
) {
|
||||||
return@listener
|
return@listener
|
||||||
}
|
}
|
||||||
@@ -293,7 +331,8 @@ internal class DiscordBridge private constructor() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
config.discord.general.channelId -> {
|
// General and the game channels are both unlabelled.
|
||||||
|
else -> {
|
||||||
Component.join(
|
Component.join(
|
||||||
JoinConfiguration.noSeparators(),
|
JoinConfiguration.noSeparators(),
|
||||||
discordComponent,
|
discordComponent,
|
||||||
@@ -302,10 +341,6 @@ internal class DiscordBridge private constructor() {
|
|||||||
contentComponent,
|
contentComponent,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
|
||||||
throw RuntimeException("Invalid channel id")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
messageComponent =
|
messageComponent =
|
||||||
@@ -347,14 +382,20 @@ internal class DiscordBridge private constructor() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Skips the game worlds, which have their own channel and their own chat.
|
||||||
config.discord.general.channelId -> {
|
config.discord.general.channelId -> {
|
||||||
for (p in Bukkit.getOnlinePlayers()) {
|
for (p in Bukkit.getOnlinePlayers()) {
|
||||||
|
if (WorldChatSystem.keyForWorld(p.world.name) != null) continue
|
||||||
p.sendMessage(messageComponent)
|
p.sendMessage(messageComponent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A game channel reaches only the players inside that game's worlds.
|
||||||
else -> {
|
else -> {
|
||||||
throw RuntimeException("Invalid channel id")
|
val gameKey = gameKeyOf(it.channel.id) ?: return@listener
|
||||||
|
for (p in WorldChatSystem.playersForKey(gameKey)) {
|
||||||
|
p.sendMessage(messageComponent)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -473,7 +514,26 @@ internal class DiscordBridge private constructor() {
|
|||||||
developerWebhook!!.send(webhookMessage.build())
|
developerWebhook!!.send(webhookMessage.build())
|
||||||
}
|
}
|
||||||
|
|
||||||
fun sendEmbed(message: String, uuid: UUID?, color: Int?) {
|
suspend fun sendGameMessage(key: String, message: String, name: String, uuid: UUID?) {
|
||||||
|
// create() already warned about this once at startup, so stay quiet rather than log per message.
|
||||||
|
val gameWebhook = gameWebhooks[key] ?: return
|
||||||
|
|
||||||
|
val webhookMessage =
|
||||||
|
WebhookMessageBuilder().apply {
|
||||||
|
setUsername(UtilitiesOG.stripFormatting(name))
|
||||||
|
setContent(
|
||||||
|
UtilitiesOG.stripFormatting(ChatUtil.stripGroupMentions((ChatUtil.convertMentions(message))))
|
||||||
|
)
|
||||||
|
if (uuid != null) setAvatarUrl("https://minotar.net/helm/$uuid.png")
|
||||||
|
}
|
||||||
|
|
||||||
|
gameWebhook.send(webhookMessage.build())
|
||||||
|
}
|
||||||
|
|
||||||
|
// A null key sends to the general channel.
|
||||||
|
fun sendEmbed(message: String, uuid: UUID?, color: Int?, key: String? = null) {
|
||||||
|
val target = if (key == null) webhook else gameWebhooks[key] ?: return
|
||||||
|
|
||||||
var iconUrl: String? = null
|
var iconUrl: String? = null
|
||||||
if (uuid != null) {
|
if (uuid != null) {
|
||||||
iconUrl = "https://minotar.net/helm/$uuid.png"
|
iconUrl = "https://minotar.net/helm/$uuid.png"
|
||||||
@@ -486,6 +546,6 @@ internal class DiscordBridge private constructor() {
|
|||||||
.setAuthor(WebhookEmbed.EmbedAuthor(UtilitiesOG.stripFormatting(message), iconUrl, null))
|
.setAuthor(WebhookEmbed.EmbedAuthor(UtilitiesOG.stripFormatting(message), iconUrl, null))
|
||||||
}
|
}
|
||||||
|
|
||||||
webhook.send(webhookMessage.build())
|
target.send(webhookMessage.build())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -58,6 +58,25 @@ discord:
|
|||||||
channelId: ^{DISCORD_DEVELOPER_ID}
|
channelId: ^{DISCORD_DEVELOPER_ID}
|
||||||
webhook: ^{DISCORD_DEVELOPER_WEBHOOK}
|
webhook: ^{DISCORD_DEVELOPER_WEBHOOK}
|
||||||
|
|
||||||
|
# Multi world game channels, keyed by the lobby prefix used by the game's own config.
|
||||||
|
# Worlds are named <prefix><number>-hub for a lobby and <prefix><number>-<map> for a game,
|
||||||
|
# so HB1-hub and HB1-Ancient_Plateau both link to the HB channel.
|
||||||
|
# Chat, joins, quits, kicks, deaths and advancements in those worlds go there instead of general.
|
||||||
|
# Every entry needs all three keys, and an unresolved webhook stops the plugin from loading.
|
||||||
|
games:
|
||||||
|
HB:
|
||||||
|
enabled: true
|
||||||
|
channelId: ^{DISCORD_HB_ID}
|
||||||
|
webhook: ^{DISCORD_HB_WEBHOOK}
|
||||||
|
SP:
|
||||||
|
enabled: true
|
||||||
|
channelId: ^{DISCORD_SP_ID}
|
||||||
|
webhook: ^{DISCORD_SP_WEBHOOK}
|
||||||
|
|
||||||
|
# Discord label for multi world game worlds. %id% is the lobby id, for example HB1.
|
||||||
|
gameLobbyLabel: "[%id% - Lobby]"
|
||||||
|
gameWorldLabel: "[%id% - In Game]"
|
||||||
|
|
||||||
# List players command
|
# List players command
|
||||||
listCommandName: list
|
listCommandName: list
|
||||||
# Available placeholders: %onlineplayers%, %maxplayers%
|
# Available placeholders: %onlineplayers%, %maxplayers%
|
||||||
|
|||||||
Reference in new issue
Block a user