Make chat events use fabric events

[no changelog]
This commit is contained in:
nea
2023-09-04 20:22:51 +02:00
parent ee5591684d
commit d202ef5439
16 changed files with 185 additions and 157 deletions

View File

@@ -7,41 +7,22 @@
package moe.nea.firmament.mixins; package moe.nea.firmament.mixins;
import com.llamalad7.mixinextras.injector.ModifyExpressionValue; import com.llamalad7.mixinextras.injector.ModifyExpressionValue;
import moe.nea.firmament.events.ClientChatLineReceivedEvent;
import moe.nea.firmament.features.fixes.Fixes; import moe.nea.firmament.features.fixes.Fixes;
import net.minecraft.client.gui.hud.ChatHud; import net.minecraft.client.gui.hud.ChatHud;
import net.minecraft.client.gui.hud.MessageIndicator;
import net.minecraft.network.message.MessageSignatureData;
import net.minecraft.text.Text;
import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.ModifyArg;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(ChatHud.class) @Mixin(ChatHud.class)
public class MixinChatHud { public class MixinChatHud {
@ModifyExpressionValue(method = "render",at = @At(value = "INVOKE",target = "Lnet/minecraft/client/gui/hud/ChatHud;isChatFocused()Z")) @ModifyExpressionValue(method = "render", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/hud/ChatHud;isChatFocused()Z"))
public boolean onGetChatHud(boolean old) { public boolean onGetChatHud(boolean old) {
return old || Fixes.INSTANCE.shouldPeekChat(); return old || Fixes.INSTANCE.shouldPeekChat();
} }
@ModifyExpressionValue(method = "getHeight",at = @At(value = "INVOKE",target = "Lnet/minecraft/client/gui/hud/ChatHud;isChatFocused()Z"))
@ModifyExpressionValue(method = "getHeight", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/hud/ChatHud;isChatFocused()Z"))
public boolean onGetChatHudHeight(boolean old) { public boolean onGetChatHudHeight(boolean old) {
return old || Fixes.INSTANCE.shouldPeekChat(); return old || Fixes.INSTANCE.shouldPeekChat();
} }
@ModifyArg(at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/hud/ChatHud;addMessage(Lnet/minecraft/text/Text;Lnet/minecraft/network/message/MessageSignatureData;ILnet/minecraft/client/gui/hud/MessageIndicator;Z)V"), method = "addMessage(Lnet/minecraft/text/Text;Lnet/minecraft/network/message/MessageSignatureData;Lnet/minecraft/client/gui/hud/MessageIndicator;)V")
public Text onAddMessage(Text message) {
var event = new ClientChatLineReceivedEvent(message);
if (ClientChatLineReceivedEvent.Companion.publish(event).getCancelled()) {
return null;
}
return event.getReplaceWith();
}
@Inject(method = "addMessage(Lnet/minecraft/text/Text;Lnet/minecraft/network/message/MessageSignatureData;ILnet/minecraft/client/gui/hud/MessageIndicator;Z)V", at = @At("HEAD"), cancellable = true)
public void onAddMessage2(Text message, MessageSignatureData signature, int ticks, MessageIndicator indicator, boolean refresh, CallbackInfo ci) {
if (message == null) ci.cancel();
}
} }

View File

@@ -1,14 +0,0 @@
/*
* SPDX-FileCopyrightText: 2023 Linnea Gräf <nea@nea.moe>
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package moe.nea.firmament.mixins;
import net.minecraft.client.gui.screen.ChatScreen;
import org.spongepowered.asm.mixin.Mixin;
@Mixin(ChatScreen.class)
public class MixinChatScreen {
}

View File

@@ -1,40 +0,0 @@
/*
* SPDX-FileCopyrightText: 2023 Linnea Gräf <nea@nea.moe>
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package moe.nea.firmament.mixins;
import com.mojang.authlib.GameProfile;
import moe.nea.firmament.events.ServerChatLineReceivedEvent;
import net.minecraft.client.network.message.MessageHandler;
import net.minecraft.network.message.MessageType;
import net.minecraft.network.message.SignedMessage;
import net.minecraft.text.Text;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(MessageHandler.class)
public class MixinMessageHandler {
@Inject(method = "onChatMessage", cancellable = true, at = @At("HEAD"))
public void onOnChatMessage(SignedMessage message, GameProfile sender, MessageType.Parameters params, CallbackInfo ci) {
var decoratedText = params.applyChatDecoration(message.unsignedContent() != null ? message.unsignedContent() : message.getContent());
var event = new ServerChatLineReceivedEvent(decoratedText);
if (ServerChatLineReceivedEvent.Companion.publish(event).getCancelled()) {
ci.cancel();
}
}
@Inject(method = "onGameMessage", at = @At("HEAD"), cancellable = true)
public void onOnGameMessage(Text message, boolean overlay, CallbackInfo ci) {
if (!overlay) {
var event = new ServerChatLineReceivedEvent(message);
if (ServerChatLineReceivedEvent.Companion.publish(event).getCancelled()) {
ci.cancel();
}
}
}
}

View File

@@ -14,8 +14,19 @@ import io.ktor.client.plugins.compression.*
import io.ktor.client.plugins.contentnegotiation.* import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.plugins.logging.* import io.ktor.client.plugins.logging.*
import io.ktor.serialization.kotlinx.json.* import io.ktor.serialization.kotlinx.json.*
import java.nio.file.Files import kotlinx.coroutines.*
import java.nio.file.Path import kotlinx.serialization.json.Json
import moe.nea.firmament.commands.registerFirmamentCommand
import moe.nea.firmament.dbus.FirmamentDbusObject
import moe.nea.firmament.events.ItemTooltipEvent
import moe.nea.firmament.events.ScreenRenderPostEvent
import moe.nea.firmament.events.TickEvent
import moe.nea.firmament.events.registration.registerFirmamentChatEvents
import moe.nea.firmament.features.FeatureManager
import moe.nea.firmament.repo.HypixelStaticData
import moe.nea.firmament.repo.RepoManager
import moe.nea.firmament.util.SBData
import moe.nea.firmament.util.data.IDataHolder
import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback
import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents
@@ -25,30 +36,15 @@ import net.fabricmc.fabric.api.client.screen.v1.ScreenEvents
import net.fabricmc.loader.api.FabricLoader import net.fabricmc.loader.api.FabricLoader
import net.fabricmc.loader.api.Version import net.fabricmc.loader.api.Version
import net.fabricmc.loader.api.metadata.ModMetadata import net.fabricmc.loader.api.metadata.ModMetadata
import net.minecraft.command.CommandRegistryAccess
import net.minecraft.util.Identifier
import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.LogManager
import org.apache.logging.log4j.Logger import org.apache.logging.log4j.Logger
import org.freedesktop.dbus.connections.impl.DBusConnectionBuilder import org.freedesktop.dbus.connections.impl.DBusConnectionBuilder
import org.freedesktop.dbus.exceptions.DBusException import org.freedesktop.dbus.exceptions.DBusException
import kotlinx.coroutines.CoroutineName import java.nio.file.Files
import kotlinx.coroutines.CoroutineScope import java.nio.file.Path
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.plus
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.json.Json
import kotlin.coroutines.EmptyCoroutineContext import kotlin.coroutines.EmptyCoroutineContext
import net.minecraft.command.CommandRegistryAccess
import net.minecraft.util.Identifier
import moe.nea.firmament.commands.registerFirmamentCommand
import moe.nea.firmament.dbus.FirmamentDbusObject
import moe.nea.firmament.events.ItemTooltipEvent
import moe.nea.firmament.events.ScreenRenderPostEvent
import moe.nea.firmament.events.TickEvent
import moe.nea.firmament.features.FeatureManager
import moe.nea.firmament.repo.HypixelStaticData
import moe.nea.firmament.repo.RepoManager
import moe.nea.firmament.util.SBData
import moe.nea.firmament.util.data.IDataHolder
object Firmament { object Firmament {
const val MOD_ID = "firmament" const val MOD_ID = "firmament"
@@ -134,6 +130,7 @@ object Firmament {
globalJob.cancel() globalJob.cancel()
} }
}) })
registerFirmamentChatEvents()
ItemTooltipCallback.EVENT.register { a, b, c -> ItemTooltipCallback.EVENT.register { a, b, c ->
ItemTooltipEvent.publish(ItemTooltipEvent(a, b, c)) ItemTooltipEvent.publish(ItemTooltipEvent(a, b, c))
} }
@@ -145,5 +142,6 @@ object Firmament {
}) })
} }
fun identifier(path: String) = Identifier(MOD_ID, path) fun identifier(path: String) = Identifier(MOD_ID, path)
} }

View File

@@ -0,0 +1,20 @@
/*
* SPDX-FileCopyrightText: 2023 Linnea Gräf <nea@nea.moe>
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package moe.nea.firmament.events
import moe.nea.firmament.util.unformattedString
import net.minecraft.text.Text
/**
* Filter whether the user should see a chat message altogether. May or may not be called for every chat packet sent by
* the server. When that quality is desired, consider [ProcessChatEvent] instead.
*/
data class AllowChatEvent(val text: Text) : FirmamentEvent.Cancellable() {
val unformattedString = text.unformattedString
companion object : FirmamentEventBus<AllowChatEvent>()
}

View File

@@ -1,21 +0,0 @@
/*
* SPDX-FileCopyrightText: 2023 Linnea Gräf <nea@nea.moe>
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package moe.nea.firmament.events
import net.minecraft.text.Text
import moe.nea.firmament.util.unformattedString
/**
* Published just before a message is added to the chat gui. Cancelling only prevents rendering, not logging to the
* console.
*/
data class ClientChatLineReceivedEvent(val text: Text) : FirmamentEvent.Cancellable() {
val unformattedString = text.unformattedString
var replaceWith: Text = text
companion object : FirmamentEventBus<ClientChatLineReceivedEvent>()
}

View File

@@ -20,10 +20,10 @@ open class FirmamentEventBus<T : FirmamentEvent> {
private val toHandle: MutableList<Handler<T>> = CopyOnWriteArrayList() private val toHandle: MutableList<Handler<T>> = CopyOnWriteArrayList()
fun subscribe(handle: (T) -> Unit) { fun subscribe(handle: (T) -> Unit) {
subscribe(handle, false) subscribe(false, handle)
} }
fun subscribe(handle: (T) -> Unit, receivesCancelled: Boolean) { fun subscribe(receivesCancelled: Boolean, handle: (T) -> Unit) {
toHandle.add(Handler(handle, receivesCancelled)) toHandle.add(Handler(handle, receivesCancelled))
} }

View File

@@ -0,0 +1,25 @@
/*
* SPDX-FileCopyrightText: 2023 Linnea Gräf <nea@nea.moe>
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package moe.nea.firmament.events
import moe.nea.firmament.util.unformattedString
import net.minecraft.text.Text
/**
* Allow modification of a chat message before it is sent off to the user. Intended for display purposes.
*/
data class ModifyChatEvent(val originalText: Text) : FirmamentEvent() {
var unformattedString = originalText.unformattedString
private set
var replaceWith: Text = originalText
set(value) {
field = value
unformattedString = value.unformattedString
}
companion object : FirmamentEventBus<ModifyChatEvent>()
}

View File

@@ -0,0 +1,25 @@
/*
* SPDX-FileCopyrightText: 2023 Linnea Gräf <nea@nea.moe>
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package moe.nea.firmament.events
import moe.nea.firmament.util.unformattedString
import net.minecraft.text.Text
/**
* Behaves like [AllowChatEvent], but is triggered even when cancelled by other mods. Intended for data collection.
* Make sure to subscribe to cancellable events as well when using.
*/
data class ProcessChatEvent(val text: Text, val wasExternallyCancelled: Boolean) : FirmamentEvent.Cancellable() {
val unformattedString = text.unformattedString
init {
if (wasExternallyCancelled)
cancelled = true
}
companion object : FirmamentEventBus<ProcessChatEvent>()
}

View File

@@ -1,19 +0,0 @@
/*
* SPDX-FileCopyrightText: 2023 Linnea Gräf <nea@nea.moe>
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package moe.nea.firmament.events
import net.minecraft.text.Text
import moe.nea.firmament.util.unformattedString
/**
* This event gets published whenever the client receives a chat message from the server.
* This event is cancellable, but should not get cancelled. Use [ClientChatLineReceivedEvent] for that instead. */
data class ServerChatLineReceivedEvent(val text: Text) : FirmamentEvent.Cancellable() {
val unformattedString = text.unformattedString
companion object : FirmamentEventBus<ServerChatLineReceivedEvent>()
}

View File

@@ -0,0 +1,42 @@
/*
* SPDX-FileCopyrightText: 2023 Linnea Gräf <nea@nea.moe>
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package moe.nea.firmament.events.registration
import moe.nea.firmament.events.AllowChatEvent
import moe.nea.firmament.events.ModifyChatEvent
import moe.nea.firmament.events.ProcessChatEvent
import net.fabricmc.fabric.api.client.message.v1.ClientReceiveMessageEvents
import net.minecraft.text.Text
private var lastReceivedMessage: Text? = null
fun registerFirmamentChatEvents() {
ClientReceiveMessageEvents.ALLOW_CHAT.register(ClientReceiveMessageEvents.AllowChat { message, signedMessage, sender, params, receptionTimestamp ->
lastReceivedMessage = message
!ProcessChatEvent.publish(ProcessChatEvent(message, false)).cancelled
&& !AllowChatEvent.publish(AllowChatEvent(message)).cancelled
})
ClientReceiveMessageEvents.ALLOW_GAME.register(ClientReceiveMessageEvents.AllowGame { message, overlay ->
lastReceivedMessage = message
overlay || (!ProcessChatEvent.publish(ProcessChatEvent(message, false)).cancelled &&
!AllowChatEvent.publish(AllowChatEvent(message)).cancelled)
})
ClientReceiveMessageEvents.MODIFY_GAME.register(ClientReceiveMessageEvents.ModifyGame { message, overlay ->
if (overlay) message
else ModifyChatEvent.publish(ModifyChatEvent(message)).replaceWith
})
ClientReceiveMessageEvents.GAME_CANCELED.register(ClientReceiveMessageEvents.GameCanceled { message, overlay ->
if (!overlay && lastReceivedMessage !== message) {
ProcessChatEvent.publish(ProcessChatEvent(message, true))
}
})
ClientReceiveMessageEvents.CHAT_CANCELED.register(ClientReceiveMessageEvents.ChatCanceled { message, signedMessage, sender, params, receptionTimestamp ->
if (lastReceivedMessage !== message) {
ProcessChatEvent.publish(ProcessChatEvent(message, true))
}
})
}

View File

@@ -12,6 +12,7 @@ import moe.nea.firmament.Firmament
import moe.nea.firmament.features.chat.ChatLinks import moe.nea.firmament.features.chat.ChatLinks
import moe.nea.firmament.features.debug.DebugView import moe.nea.firmament.features.debug.DebugView
import moe.nea.firmament.features.debug.DeveloperFeatures import moe.nea.firmament.features.debug.DeveloperFeatures
import moe.nea.firmament.features.debug.MinorTrolling
import moe.nea.firmament.features.fixes.Fixes import moe.nea.firmament.features.fixes.Fixes
import moe.nea.firmament.features.inventory.CraftingOverlay import moe.nea.firmament.features.inventory.CraftingOverlay
import moe.nea.firmament.features.inventory.PriceData import moe.nea.firmament.features.inventory.PriceData
@@ -41,6 +42,7 @@ object FeatureManager : DataHolder<FeatureManager.Config>(serializer(), "feature
fun autoload() { fun autoload() {
synchronized(this) { synchronized(this) {
if (hasAutoloaded) return if (hasAutoloaded) return
loadFeature(MinorTrolling)
loadFeature(FairySouls) loadFeature(FairySouls)
// TODO: loadFeature(FishingWarning) // TODO: loadFeature(FishingWarning)
loadFeature(SlotLocking) loadFeature(SlotLocking)

View File

@@ -26,7 +26,7 @@ import net.minecraft.text.Text
import net.minecraft.util.Formatting import net.minecraft.util.Formatting
import net.minecraft.util.Identifier import net.minecraft.util.Identifier
import moe.nea.firmament.Firmament import moe.nea.firmament.Firmament
import moe.nea.firmament.events.ClientChatLineReceivedEvent import moe.nea.firmament.events.ModifyChatEvent
import moe.nea.firmament.events.ScreenRenderPostEvent import moe.nea.firmament.events.ScreenRenderPostEvent
import moe.nea.firmament.features.FirmamentFeature import moe.nea.firmament.features.FirmamentFeature
import moe.nea.firmament.gui.config.ManagedConfig import moe.nea.firmament.gui.config.ManagedConfig
@@ -98,9 +98,9 @@ object ChatLinks : FirmamentFeature {
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
override fun onLoad() { override fun onLoad() {
ClientChatLineReceivedEvent.subscribe { ModifyChatEvent.subscribe {
if (TConfig.enableLinks) if (TConfig.enableLinks)
it.replaceWith = it.text.transformEachRecursively { child -> it.replaceWith = it.replaceWith.transformEachRecursively { child ->
val text = child.string val text = child.string
if ("://" !in text) return@transformEachRecursively child if ("://" !in text) return@transformEachRecursively child
val s = Text.empty().setStyle(child.style) val s = Text.empty().setStyle(child.style)

View File

@@ -0,0 +1,31 @@
/*
* SPDX-FileCopyrightText: 2023 Linnea Gräf <nea@nea.moe>
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package moe.nea.firmament.features.debug
import moe.nea.firmament.events.ModifyChatEvent
import moe.nea.firmament.features.FirmamentFeature
import net.minecraft.text.Text
// In memorian Dulkir
object MinorTrolling : FirmamentFeature {
override val identifier: String
get() = "minor-trolling"
val trollers = listOf("nea89o", "lrg89")
val t = "From(?: \\[[^\\]]+])? ([^:]+): (.*)".toRegex()
override fun onLoad() {
ModifyChatEvent.subscribe {
val m = t.matchEntire(it.unformattedString) ?: return@subscribe
val (_, name, text) = m.groupValues
if (name !in trollers) return@subscribe
if (!text.startsWith("c:")) return@subscribe
it.replaceWith = Text.literal(text.substring(2).replace("&", "§"))
}
}
}

View File

@@ -17,7 +17,7 @@ import net.minecraft.client.render.VertexFormat
import net.minecraft.client.render.VertexFormats import net.minecraft.client.render.VertexFormats
import net.minecraft.text.Text import net.minecraft.text.Text
import net.minecraft.util.math.Vec3d import net.minecraft.util.math.Vec3d
import moe.nea.firmament.events.ServerChatLineReceivedEvent import moe.nea.firmament.events.AllowChatEvent
import moe.nea.firmament.events.SkyblockServerUpdateEvent import moe.nea.firmament.events.SkyblockServerUpdateEvent
import moe.nea.firmament.events.WorldRenderLastEvent import moe.nea.firmament.events.WorldRenderLastEvent
import moe.nea.firmament.features.FirmamentFeature import moe.nea.firmament.features.FirmamentFeature
@@ -125,7 +125,7 @@ object FairySouls : FirmamentFeature {
updateWorldSouls() updateWorldSouls()
updateMissingSouls() updateMissingSouls()
} }
ServerChatLineReceivedEvent.subscribe { AllowChatEvent.subscribe {
when (it.text.unformattedString) { when (it.text.unformattedString) {
"You have already found that Fairy Soul!" -> { "You have already found that Fairy Soul!" -> {
markNearestSoul() markNearestSoul()

View File

@@ -6,18 +6,17 @@
package moe.nea.firmament.util package moe.nea.firmament.util
import java.util.*
import kotlinx.serialization.SerializationException import kotlinx.serialization.SerializationException
import kotlinx.serialization.decodeFromString import kotlinx.serialization.decodeFromString
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds
import net.minecraft.network.packet.c2s.play.CommandExecutionC2SPacket
import moe.nea.firmament.Firmament import moe.nea.firmament.Firmament
import moe.nea.firmament.events.ClientChatLineReceivedEvent
import moe.nea.firmament.events.OutgoingPacketEvent import moe.nea.firmament.events.OutgoingPacketEvent
import moe.nea.firmament.events.ServerChatLineReceivedEvent import moe.nea.firmament.events.ProcessChatEvent
import moe.nea.firmament.events.SkyblockServerUpdateEvent import moe.nea.firmament.events.SkyblockServerUpdateEvent
import moe.nea.firmament.events.WorldReadyEvent import moe.nea.firmament.events.WorldReadyEvent
import net.minecraft.network.packet.c2s.play.CommandExecutionC2SPacket
import java.util.*
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds
object SBData { object SBData {
private val profileRegex = "Profile ID: ([a-z0-9\\-]+)".toRegex() private val profileRegex = "Profile ID: ([a-z0-9\\-]+)".toRegex()
@@ -38,7 +37,7 @@ object SBData {
anyLocrawSent.markNow() anyLocrawSent.markNow()
} }
} }
ServerChatLineReceivedEvent.subscribe { event -> ProcessChatEvent.subscribe(receivesCancelled = true) { event ->
val profileMatch = profileRegex.matchEntire(event.unformattedString) val profileMatch = profileRegex.matchEntire(event.unformattedString)
if (profileMatch != null) { if (profileMatch != null) {
try { try {
@@ -54,23 +53,22 @@ object SBData {
} }
if (event.unformattedString.startsWith("{")) { if (event.unformattedString.startsWith("{")) {
if (tryReceiveLocraw(event.unformattedString)) { if (tryReceiveLocraw(event.unformattedString)) {
if (lastLocrawSent.timePassed() < locrawRoundtripTime) {
lastLocrawSent.markFarPast()
event.cancel()
}
if (!hasValidLocraw && !hasSentLocraw && hasReceivedProfile) { if (!hasValidLocraw && !hasSentLocraw && hasReceivedProfile) {
sendLocraw() sendLocraw()
} }
} }
} }
} }
ClientChatLineReceivedEvent.subscribe { event ->
if (event.unformattedString.startsWith("{") && tryReceiveLocraw(event.unformattedString) && lastLocrawSent.timePassed() < locrawRoundtripTime) {
lastLocrawSent.markFarPast()
event.cancel()
}
}
WorldReadyEvent.subscribe { WorldReadyEvent.subscribe {
locraw = null locraw = null
hasSentLocraw = false hasSentLocraw = false
hasReceivedProfile = false hasReceivedProfile = false
profileId = null
} }
} }