Skip to content

Commit e41104d

Browse files
authored
Add offline player inventory editing functionality and update version (#344)
2 parents 25df7bb + b4faaa7 commit e41104d

9 files changed

Lines changed: 719 additions & 2 deletions

File tree

gradle.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,6 @@ org.jetbrains.dokka.experimental.gradle.pluginMode=V2Enabled
77
javaVersion=25
88
mcVersion=26.1.2
99
group=dev.slne.surf.api
10-
version=3.10.0
10+
version=3.11.0
1111
relocationPrefix=dev.slne.surf.api.libs
1212
snapshot=false
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
package dev.slne.surf.api.paper.nms.common.dummy
2+
3+
import org.bukkit.entity.Entity
4+
import org.bukkit.inventory.EntityEquipment
5+
import org.bukkit.inventory.EquipmentSlot
6+
import org.bukkit.inventory.ItemStack
7+
8+
abstract class DummyEntityEquipment: EntityEquipment {
9+
override fun setItem(slot: EquipmentSlot, item: ItemStack?) {
10+
throw NotImplementedError()
11+
}
12+
13+
override fun setItem(
14+
slot: EquipmentSlot,
15+
item: ItemStack?,
16+
silent: Boolean
17+
) {
18+
setItem(slot, item)
19+
}
20+
21+
override fun getItem(slot: EquipmentSlot): ItemStack {
22+
throw NotImplementedError()
23+
}
24+
25+
override fun getItemInMainHand(): ItemStack {
26+
return getItem(EquipmentSlot.HAND)
27+
}
28+
29+
override fun setItemInMainHand(item: ItemStack?) {
30+
setItem(EquipmentSlot.HAND, item)
31+
}
32+
33+
override fun setItemInMainHand(item: ItemStack?, silent: Boolean) {
34+
setItemInMainHand(item)
35+
}
36+
37+
override fun getItemInOffHand(): ItemStack {
38+
return getItem(EquipmentSlot.OFF_HAND)
39+
}
40+
41+
override fun setItemInOffHand(item: ItemStack?) {
42+
setItem(EquipmentSlot.OFF_HAND, item)
43+
}
44+
45+
override fun setItemInOffHand(item: ItemStack?, silent: Boolean) {
46+
setItemInOffHand(item)
47+
}
48+
49+
override fun getItemInHand(): ItemStack {
50+
return itemInMainHand
51+
}
52+
53+
override fun setItemInHand(stack: ItemStack?) {
54+
setItemInMainHand(stack)
55+
}
56+
57+
override fun getHelmet(): ItemStack {
58+
return getItem(EquipmentSlot.HEAD)
59+
}
60+
61+
override fun setHelmet(helmet: ItemStack?) {
62+
setItem(EquipmentSlot.HEAD, helmet)
63+
}
64+
65+
override fun setHelmet(helmet: ItemStack?, silent: Boolean) {
66+
setHelmet(helmet)
67+
}
68+
69+
override fun getChestplate(): ItemStack {
70+
return getItem(EquipmentSlot.CHEST)
71+
}
72+
73+
override fun setChestplate(chestplate: ItemStack?) {
74+
setItem(EquipmentSlot.CHEST, chestplate)
75+
}
76+
77+
override fun setChestplate(chestplate: ItemStack?, silent: Boolean) {
78+
setChestplate(chestplate)
79+
}
80+
81+
override fun getLeggings(): ItemStack {
82+
return getItem(EquipmentSlot.LEGS)
83+
}
84+
85+
override fun setLeggings(leggings: ItemStack?) {
86+
setItem(EquipmentSlot.LEGS, leggings)
87+
}
88+
89+
override fun setLeggings(leggings: ItemStack?, silent: Boolean) {
90+
setLeggings(leggings)
91+
}
92+
93+
override fun getBoots(): ItemStack {
94+
return getItem(EquipmentSlot.FEET)
95+
}
96+
97+
override fun setBoots(boots: ItemStack?) {
98+
setItem(EquipmentSlot.FEET, boots)
99+
}
100+
101+
override fun setBoots(boots: ItemStack?, silent: Boolean) {
102+
setBoots(boots)
103+
}
104+
105+
override fun getArmorContents(): Array<out ItemStack?> {
106+
return arrayOf(boots, leggings, chestplate, helmet)
107+
}
108+
109+
override fun setArmorContents(items: Array<out ItemStack>) {
110+
setBoots(items.getOrNull(0))
111+
setLeggings(items.getOrNull(1))
112+
setChestplate(items.getOrNull(2))
113+
setHelmet(items.getOrNull(3))
114+
}
115+
116+
override fun clear() {
117+
throw NotImplementedError()
118+
}
119+
120+
override fun getItemInHandDropChance(): Float {
121+
throw NotImplementedError()
122+
}
123+
124+
override fun setItemInHandDropChance(chance: Float) {
125+
throw NotImplementedError()
126+
}
127+
128+
override fun getItemInMainHandDropChance(): Float {
129+
throw NotImplementedError()
130+
}
131+
132+
override fun setItemInMainHandDropChance(chance: Float) {
133+
throw NotImplementedError()
134+
}
135+
136+
override fun getItemInOffHandDropChance(): Float {
137+
throw NotImplementedError()
138+
}
139+
140+
override fun setItemInOffHandDropChance(chance: Float) {
141+
throw NotImplementedError()
142+
}
143+
144+
override fun getHelmetDropChance(): Float {
145+
throw NotImplementedError()
146+
}
147+
148+
override fun setHelmetDropChance(chance: Float) {
149+
throw NotImplementedError()
150+
}
151+
152+
override fun getChestplateDropChance(): Float {
153+
throw NotImplementedError()
154+
}
155+
156+
override fun setChestplateDropChance(chance: Float) {
157+
throw NotImplementedError()
158+
}
159+
160+
override fun getLeggingsDropChance(): Float {
161+
throw NotImplementedError()
162+
}
163+
164+
override fun setLeggingsDropChance(chance: Float) {
165+
throw NotImplementedError()
166+
}
167+
168+
override fun getBootsDropChance(): Float {
169+
throw NotImplementedError()
170+
}
171+
172+
override fun setBootsDropChance(chance: Float) {
173+
throw NotImplementedError()
174+
}
175+
176+
override fun getHolder(): Entity {
177+
throw NotImplementedError()
178+
}
179+
180+
override fun getDropChance(slot: EquipmentSlot): Float {
181+
throw NotImplementedError()
182+
}
183+
184+
override fun setDropChance(slot: EquipmentSlot, chance: Float) {
185+
throw NotImplementedError()
186+
}
187+
}

surf-api-paper/surf-api-paper-nms/surf-api-paper-nms-v1-21-11/src/main/kotlin/dev/slne/surf/api/paper/server/nms/v1_21_11/bridges/V1_21_11SurfPaperNmsPlayerBridgeImpl.kt

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,54 @@
11
package 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
37
import dev.slne.surf.api.paper.nms.NmsUseWithCaution
48
import dev.slne.surf.api.paper.nms.bridges.SurfPaperNmsPlayerBridge
9+
import dev.slne.surf.api.paper.nms.bridges.SurfPaperNmsPlayerBridge.PlayerInventoryEdit
510
import dev.slne.surf.api.paper.nms.bridges.data.chat.PlayerChatMessageMirror
611
import dev.slne.surf.api.paper.nms.bridges.data.chat.RemoteChatSessionData
12+
import dev.slne.surf.api.paper.nms.common.dummy.DummyEntityEquipment
713
import dev.slne.surf.api.paper.server.nms.v1_21_11.extensions.toNms
814
import dev.slne.surf.api.paper.server.nms.v1_21_11.reflection.NmsReflections
915
import io.papermc.paper.adventure.PaperAdventure
1016
import kotlinx.coroutines.CoroutineScope
17+
import kotlinx.coroutines.Dispatchers
1118
import kotlinx.coroutines.launch
19+
import kotlinx.coroutines.withContext
1220
import net.kyori.adventure.chat.ChatType
1321
import net.kyori.adventure.chat.SignedMessage
1422
import 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
1527
import net.minecraft.network.chat.*
1628
import 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
1745
import org.bukkit.entity.Player
46+
import org.bukkit.inventory.EquipmentSlot
47+
import org.bukkit.inventory.ItemStack
1848
import java.util.*
1949
import java.util.concurrent.CompletableFuture
50+
import kotlin.io.path.createTempFile
51+
import kotlin.jvm.optionals.getOrNull
2052

2153
@NmsUseWithCaution
2254
class 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

Comments
 (0)