11package dev.slne.surf.api.paper.server.nms.v1_21_11.bridges
22
3+ import com.destroystokyo.paper.profile.CraftPlayerProfile
4+ import com.destroystokyo.paper.profile.PlayerProfile
5+ import dev.slne.surf.api.paper.command.util.idOrThrow
6+ import dev.slne.surf.api.paper.extensions.server
37import dev.slne.surf.api.paper.nms.NmsUseWithCaution
48import dev.slne.surf.api.paper.nms.bridges.SurfPaperNmsPlayerBridge
9+ import dev.slne.surf.api.paper.nms.bridges.SurfPaperNmsPlayerBridge.PlayerInventoryEdit
510import dev.slne.surf.api.paper.nms.bridges.data.chat.PlayerChatMessageMirror
611import dev.slne.surf.api.paper.nms.bridges.data.chat.RemoteChatSessionData
12+ import dev.slne.surf.api.paper.nms.common.dummy.DummyEntityEquipment
713import dev.slne.surf.api.paper.server.nms.v1_21_11.extensions.toNms
814import dev.slne.surf.api.paper.server.nms.v1_21_11.reflection.NmsReflections
915import io.papermc.paper.adventure.PaperAdventure
1016import kotlinx.coroutines.CoroutineScope
17+ import kotlinx.coroutines.Dispatchers
1118import kotlinx.coroutines.launch
19+ import kotlinx.coroutines.withContext
1220import net.kyori.adventure.chat.ChatType
1321import net.kyori.adventure.chat.SignedMessage
1422import net.kyori.adventure.text.Component
23+ import net.kyori.adventure.text.logger.slf4j.ComponentLogger
24+ import net.minecraft.core.NonNullList
25+ import net.minecraft.nbt.CompoundTag
26+ import net.minecraft.nbt.NbtIo
1527import net.minecraft.network.chat.*
1628import net.minecraft.network.protocol.game.ClientboundPlayerChatPacket
29+ import net.minecraft.server.MinecraftServer
30+ import net.minecraft.server.players.NameAndId
31+ import net.minecraft.util.ProblemReporter
32+ import net.minecraft.util.ProblemReporter.ScopedCollector
33+ import net.minecraft.util.Util
34+ import net.minecraft.world.ItemStackWithSlot
35+ import net.minecraft.world.entity.EntityEquipment
36+ import net.minecraft.world.entity.LivingEntity
37+ import net.minecraft.world.entity.npc.InventoryCarrier
38+ import net.minecraft.world.entity.player.Inventory
39+ import net.minecraft.world.level.storage.TagValueInput
40+ import net.minecraft.world.level.storage.TagValueOutput
41+ import net.minecraft.world.level.storage.ValueInput
42+ import net.minecraft.world.level.storage.ValueOutput
43+ import org.bukkit.craftbukkit.CraftEquipmentSlot
44+ import org.bukkit.craftbukkit.inventory.CraftItemStack
1745import org.bukkit.entity.Player
46+ import org.bukkit.inventory.EquipmentSlot
47+ import org.bukkit.inventory.ItemStack
1848import java.util.*
1949import java.util.concurrent.CompletableFuture
50+ import kotlin.io.path.createTempFile
51+ import kotlin.jvm.optionals.getOrNull
2052
2153@NmsUseWithCaution
2254class V1_21_11SurfPaperNmsPlayerBridgeImpl : SurfPaperNmsPlayerBridge {
@@ -181,4 +213,132 @@ class V1_21_11SurfPaperNmsPlayerBridgeImpl : SurfPaperNmsPlayerBridge {
181213 override fun getPaperRawChatType (): ChatType {
182214 return ChatType .chatType(PaperAdventure .asAdventureKey(NmsReflections .getPaperRaw()))
183215 }
216+
217+ override suspend fun editOfflineInventory (
218+ profile : PlayerProfile ,
219+ edit : (PlayerInventoryEdit ) -> Unit
220+ ) {
221+ val uuid = profile.idOrThrow()
222+ require(server.getPlayer(uuid) == null ) { " Player must be offline" }
223+ require(profile is CraftPlayerProfile ) { " Only CraftPlayerProfile (paper) is supported" }
224+
225+ val server = MinecraftServer .getServer()
226+ val nameAndId = NameAndId (profile.gameProfileUnsafe)
227+ val rootPathElement = ProblemReporter .PathElement { " OfflinePlayer Inventory[$nameAndId ]" }
228+ val currentTag = loadPlayerTag(server, nameAndId)
229+ val inventoryEdit = buildInventoryEdit(server, rootPathElement, currentTag)
230+
231+ edit(inventoryEdit)
232+ saveInventoryEdit(server, rootPathElement, currentTag, nameAndId, inventoryEdit)
233+ }
234+
235+ private suspend fun loadPlayerTag (server : MinecraftServer , nameAndId : NameAndId ): CompoundTag {
236+ val dataStorage = server.playerDataStorage
237+ return withContext(Dispatchers .IO ) {
238+ dataStorage.load(nameAndId).getOrNull() ? : CompoundTag ()
239+ }
240+ }
241+
242+ private fun buildInventoryEdit (
243+ server : MinecraftServer ,
244+ root : ProblemReporter .PathElement ,
245+ currentTag : CompoundTag
246+ ): PlayerInventoryEdit = ScopedCollector (root, OFFLINE_INVENTORY_EDIT_LOGGER ).use { reporter ->
247+ val input = TagValueInput .create(reporter, server.registryAccess(), currentTag)
248+ val items = loadItems(input.listOrEmpty(InventoryCarrier .TAG_INVENTORY , ItemStackWithSlot .CODEC ))
249+ val nmsEquipment = input.read(LivingEntity .TAG_EQUIPMENT , EntityEquipment .CODEC ).getOrNull()
250+ ? : EntityEquipment ()
251+
252+ PlayerInventoryEdit (items, EntityEquipmentMirror (nmsEquipment))
253+ }
254+
255+ private suspend fun saveInventoryEdit (
256+ server : MinecraftServer ,
257+ root : ProblemReporter .PathElement ,
258+ currentTag : CompoundTag ,
259+ nameAndId : NameAndId ,
260+ inventoryEdit : PlayerInventoryEdit
261+ ) {
262+ ScopedCollector (root, OFFLINE_INVENTORY_EDIT_LOGGER ).use { reporter ->
263+ val output = TagValueOutput .createWrappingWithContext(reporter, server.registryAccess(), currentTag)
264+ saveItems(
265+ inventoryEdit.items,
266+ output.list(InventoryCarrier .TAG_INVENTORY , ItemStackWithSlot .CODEC )
267+ )
268+
269+ val nmsEquipment = (inventoryEdit.equipment as EntityEquipmentMirror ).equipment
270+ if (! nmsEquipment.isEmpty) {
271+ output.store(LivingEntity .TAG_EQUIPMENT , EntityEquipment .CODEC , nmsEquipment)
272+ } else {
273+ output.discard(LivingEntity .TAG_EQUIPMENT )
274+ }
275+
276+ writePlayerTag(server, nameAndId, output.buildResult())
277+ }
278+ }
279+
280+ private suspend fun writePlayerTag (
281+ server : MinecraftServer ,
282+ nameAndId : NameAndId ,
283+ rootTag : CompoundTag
284+ ) {
285+ try {
286+ val playerDirPath = server.playerDataStorage.playerDir.toPath()
287+ val playerId = nameAndId.id.toString()
288+ val tmp = createTempFile(playerDirPath, " $playerId -" , " .dat" )
289+
290+ withContext(Dispatchers .IO ) {
291+ NbtIo .writeCompressed(rootTag, tmp)
292+ Util .safeReplaceFile(
293+ playerDirPath.resolve(" $playerId .dat" ),
294+ tmp,
295+ playerDirPath.resolve(" ${playerId} .dat_old" )
296+ )
297+ }
298+ } catch (e: Exception ) {
299+ OFFLINE_INVENTORY_EDIT_LOGGER .error(" Failed to save offline player inventory" , e)
300+ throw e
301+ }
302+ }
303+
304+ private fun loadItems (input : ValueInput .TypedInputList <ItemStackWithSlot >): MutableList <ItemStack > {
305+ val items = NonNullList .withSize(Inventory .INVENTORY_SIZE , ItemStack .empty())
306+
307+ for (item in input) {
308+ if (item.isValidInContainer(items.size)) {
309+ items[item.slot()] = item.stack().asBukkitMirror()
310+ }
311+ }
312+
313+ return items
314+ }
315+
316+ private fun saveItems (items : List <ItemStack >, output : ValueOutput .TypedOutputList <ItemStackWithSlot >) {
317+ for ((index, stack) in items.withIndex()) {
318+ if (! stack.isEmpty) {
319+ output.add(ItemStackWithSlot (index, stack.toNms()))
320+ }
321+ }
322+ }
323+
324+ private class EntityEquipmentMirror (val equipment : EntityEquipment ) : DummyEntityEquipment() {
325+ override fun setItem (slot : EquipmentSlot , item : ItemStack ? ) {
326+ val nmsSlot = CraftEquipmentSlot .getNMS(slot)
327+ val nmsStack = CraftItemStack .asNMSCopy(item)
328+ equipment.set(nmsSlot, nmsStack)
329+ }
330+
331+ override fun getItem (slot : EquipmentSlot ): ItemStack {
332+ val nmsSlot = CraftEquipmentSlot .getNMS(slot)
333+ return equipment.get(nmsSlot).asBukkitMirror()
334+ }
335+
336+ override fun clear () {
337+ equipment.clear()
338+ }
339+ }
340+
341+ companion object {
342+ private val OFFLINE_INVENTORY_EDIT_LOGGER = ComponentLogger .logger(" OfflinePlayer Inventory Edit" )
343+ }
184344}
0 commit comments