Skip to content

Commit 25df7bb

Browse files
authored
✨ feat(block-pdc): enhance persistent data container handling for blocks (#343)
2 parents 3504bd7 + c2bd7b2 commit 25df7bb

9 files changed

Lines changed: 410 additions & 58 deletions

File tree

gradle.properties

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ kapt.use.k2=true
55
org.gradle.jvmargs=-Xmx8G
66
org.jetbrains.dokka.experimental.gradle.pluginMode=V2Enabled
77
javaVersion=25
8-
mcVersion=26.1.1
8+
mcVersion=26.1.2
99
group=dev.slne.surf.api
10-
version=3.9.5
10+
version=3.10.0
1111
relocationPrefix=dev.slne.surf.api.libs
1212
snapshot=false

surf-api-paper/surf-api-paper-plugin-test/build.gradle.kts

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import net.minecrell.pluginyml.paper.PaperPluginDescription
2+
import xyz.jpenilla.runpaper.task.RunServer
23

34
plugins {
45
`core-convention`
@@ -24,6 +25,7 @@ paper {
2425
description = "Test plugin for Surf API for Paper"
2526
author = "twisti"
2627
apiVersion = "1.21"
28+
foliaSupported = true
2729

2830
serverDependencies {
2931
register("CommandAPI") {
@@ -40,21 +42,36 @@ paper {
4042
}
4143
}
4244

43-
tasks {
44-
runServer {
45-
dependsOn(":surf-api-paper:surf-api-paper-server:shadowJar")
46-
pluginJars.from(project(":surf-api-paper:surf-api-paper-server").tasks.shadowJar)
45+
fun RunServer.configure(folia: Boolean) {
46+
dependsOn(":surf-api-paper:surf-api-paper-server:shadowJar")
47+
pluginJars.from(project(":surf-api-paper:surf-api-paper-server").tasks.shadowJar)
48+
49+
minecraftVersion(findProperty("mcVersion") as String)
4750

48-
minecraftVersion(findProperty("mcVersion") as String)
51+
downloadPlugins {
52+
hangar("CommandAPI", libs.versions.commandapi.get())
53+
modrinth("packetevents", libs.versions.packetevents.plugin.get() + "+spigot")
4954

50-
downloadPlugins {
51-
hangar("CommandAPI", libs.versions.commandapi.get())
52-
modrinth("packetevents", libs.versions.packetevents.plugin.get() + "+spigot")
55+
if (!folia) {
5356
modrinth("luckperms", libs.versions.luckpermsplugin.bukkit.get())
57+
} else {
58+
url("https://ci.lucko.me/job/LuckPerms-Folia/11/artifact/bukkit/loader/build/libs/LuckPerms-Bukkit-5.5.29.jar")
5459
}
5560
}
5661
}
5762

63+
runPaper {
64+
folia.registerTask {
65+
configure(true)
66+
}
67+
}
68+
69+
tasks {
70+
runServer {
71+
configure(false)
72+
}
73+
}
74+
5875
tasks {
5976
shadowJar {
6077
val relocationPrefix: String by project

surf-api-paper/surf-api-paper-plugin-test/src/main/java/dev/slne/surf/api/paper/test/command/SurfApiTestCommand.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
package dev.slne.surf.api.paper.test.command;
22

3-
import dev.jorel.commandapi.CommandAPICommand;
3+
import dev.jorel.commandapi.*;
44
import dev.slne.surf.api.paper.test.command.subcommands.*;
55
import dev.slne.surf.surfapi.bukkit.test.command.subcommands.*;
66

@@ -30,7 +30,8 @@ public SurfApiTestCommand() {
3030
new SurfEventHandlerTest("eventhandler"),
3131
new ShowItemCommand("showitem"),
3232
new SortInvCommand("sortInv"),
33-
new SignedMessageArgumentTest("signedmessage")
33+
new SignedMessageArgumentTest("signedmessage"),
34+
new BlockPdcContainerTest("blockpdc")
3435
);
3536
}
3637
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
package dev.slne.surf.surfapi.bukkit.test.command.subcommands
2+
3+
import com.github.shynixn.mccoroutine.folia.regionDispatcher
4+
import dev.jorel.commandapi.CommandAPICommand
5+
import dev.jorel.commandapi.arguments.LocationType
6+
import dev.jorel.commandapi.kotlindsl.*
7+
import dev.slne.surf.api.paper.command.executors.playerExecutorSuspend
8+
import dev.slne.surf.api.paper.pdc.block.pdc
9+
import dev.slne.surf.api.paper.util.chunkX
10+
import dev.slne.surf.api.paper.util.chunkZ
11+
import dev.slne.surf.api.paper.util.doInChunkAsync
12+
import dev.slne.surf.surfapi.bukkit.test.plugin
13+
import kotlinx.coroutines.withContext
14+
import org.bukkit.Location
15+
import org.bukkit.NamespacedKey
16+
import org.bukkit.entity.Player
17+
import org.bukkit.persistence.PersistentDataType
18+
import kotlin.math.abs
19+
import kotlin.math.max
20+
21+
class BlockPdcContainerTest(name: String) : CommandAPICommand(name) {
22+
init {
23+
setCommand()
24+
getCommand()
25+
listCommand()
26+
clearCommand()
27+
copyNearCommand()
28+
copyFarCommand()
29+
}
30+
31+
private fun setCommand() = subcommand("set") {
32+
locationArgument("location", LocationType.BLOCK_POSITION)
33+
textArgument("key")
34+
greedyStringArgument("value")
35+
36+
playerExecutorSuspend { sender, args ->
37+
val location: Location by args
38+
val key: String by args
39+
val value: String by args
40+
41+
val world = location.world ?: run {
42+
sender.sendMessage("Location has no world.")
43+
return@playerExecutorSuspend
44+
}
45+
46+
val nsKey = NamespacedKey(plugin, key)
47+
48+
world.doInChunkAsync(location.chunkX, location.chunkZ) { chunk ->
49+
val block = chunk.getBlock(location.blockX and 15, location.blockY, location.blockZ and 15)
50+
block.pdc().set(nsKey, PersistentDataType.STRING, value)
51+
}
52+
53+
sender.sendMessage("Set '$key' = '$value' on block at (${location.blockX}, ${location.blockY}, ${location.blockZ}).")
54+
}
55+
}
56+
57+
private fun getCommand() = subcommand("get") {
58+
locationArgument("location", LocationType.BLOCK_POSITION)
59+
textArgument("key")
60+
61+
playerExecutorSuspend { sender, args ->
62+
val location: Location by args
63+
val key: String by args
64+
65+
val world = location.world ?: run {
66+
sender.sendMessage("Location has no world.")
67+
return@playerExecutorSuspend
68+
}
69+
70+
val nsKey = NamespacedKey(plugin, key)
71+
72+
val result = world.doInChunkAsync(location.chunkX, location.chunkZ) { chunk ->
73+
val block = chunk.getBlock(location.blockX and 15, location.blockY, location.blockZ and 15)
74+
block.pdc().get(nsKey, PersistentDataType.STRING)
75+
}
76+
77+
if (result != null) {
78+
sender.sendMessage("Block PDC [$key] = '$result' at (${location.blockX}, ${location.blockY}, ${location.blockZ}).")
79+
} else {
80+
sender.sendMessage("No value for key '$key' on block at (${location.blockX}, ${location.blockY}, ${location.blockZ}).")
81+
}
82+
}
83+
}
84+
85+
private fun listCommand() = subcommand("list") {
86+
locationArgument("location", LocationType.BLOCK_POSITION)
87+
88+
playerExecutorSuspend { sender, args ->
89+
val location: Location by args
90+
91+
val world = location.world ?: run {
92+
sender.sendMessage("Location has no world.")
93+
return@playerExecutorSuspend
94+
}
95+
96+
val keys = world.doInChunkAsync(location.chunkX, location.chunkZ) { chunk ->
97+
val block = chunk.getBlock(location.blockX and 15, location.blockY, location.blockZ and 15)
98+
block.pdc().keys.map { it.toString() }
99+
}
100+
101+
if (keys.isEmpty()) {
102+
sender.sendMessage("Block PDC at (${location.blockX}, ${location.blockY}, ${location.blockZ}) is empty.")
103+
} else {
104+
sender.sendMessage("Block PDC keys at (${location.blockX}, ${location.blockY}, ${location.blockZ}) [${keys.size}]:")
105+
keys.forEach { sender.sendMessage(" - $it") }
106+
}
107+
}
108+
}
109+
110+
private fun clearCommand() = subcommand("clear") {
111+
locationArgument("location", LocationType.BLOCK_POSITION)
112+
113+
playerExecutorSuspend { sender, args ->
114+
val location: Location by args
115+
116+
val world = location.world ?: run {
117+
sender.sendMessage("Location has no world.")
118+
return@playerExecutorSuspend
119+
}
120+
121+
world.doInChunkAsync(location.chunkX, location.chunkZ) { chunk ->
122+
val block = chunk.getBlock(location.blockX and 15, location.blockY, location.blockZ and 15)
123+
block.pdc().clear()
124+
}
125+
126+
sender.sendMessage("Cleared block PDC at (${location.blockX}, ${location.blockY}, ${location.blockZ}).")
127+
}
128+
}
129+
130+
private fun copyNearCommand() = subcommand("copy-near") {
131+
locationArgument("source", LocationType.BLOCK_POSITION)
132+
locationArgument("target", LocationType.BLOCK_POSITION)
133+
134+
playerExecutorSuspend { sender, args ->
135+
val source: Location by args
136+
val target: Location by args
137+
138+
if (!isSameWorld(sender, source, target)) {
139+
return@playerExecutorSuspend
140+
}
141+
142+
val chunkDistance = chunkDistance(source, target)
143+
if (chunkDistance > 1) {
144+
sender.sendMessage("copy-near expects source and target in the same region (chunk distance <= 1), got $chunkDistance.")
145+
return@playerExecutorSuspend
146+
}
147+
148+
runCopyTest(sender, source, target, "near")
149+
}
150+
}
151+
152+
private fun copyFarCommand() = subcommand("copy-far") {
153+
locationArgument("source", LocationType.BLOCK_POSITION)
154+
locationArgument("target", LocationType.BLOCK_POSITION)
155+
156+
playerExecutorSuspend { sender, args ->
157+
val source: Location by args
158+
val target: Location by args
159+
160+
if (!isSameWorld(sender, source, target)) {
161+
return@playerExecutorSuspend
162+
}
163+
164+
val chunkDistance = chunkDistance(source, target)
165+
if (chunkDistance < FAR_MIN_CHUNK_DISTANCE) {
166+
sender.sendMessage("copy-far expects blocks to be far apart (chunk distance >= $FAR_MIN_CHUNK_DISTANCE), got $chunkDistance.")
167+
return@playerExecutorSuspend
168+
}
169+
170+
runCopyTest(sender, source, target, "far")
171+
}
172+
}
173+
174+
private fun isSameWorld(sender: Player, source: Location, target: Location): Boolean {
175+
if (source.world == null || target.world == null) {
176+
sender.sendMessage("Both source and target must include a world.")
177+
return false
178+
}
179+
180+
if (source.world != target.world) {
181+
sender.sendMessage("Source and target must be in the same world.")
182+
return false
183+
}
184+
185+
return true
186+
}
187+
188+
private suspend fun runCopyTest(sender: Player, source: Location, target: Location, label: String) {
189+
val value = "$label-copy-${System.currentTimeMillis()}"
190+
191+
val result = runCatching {
192+
val sourcePdc = withContext(plugin.regionDispatcher(source)) {
193+
val sourceBlock = source.block
194+
val sourcePdc = sourceBlock.pdc()
195+
sourcePdc.set(TEST_KEY, PersistentDataType.STRING, value)
196+
sourcePdc
197+
}
198+
199+
withContext(plugin.regionDispatcher(target)) {
200+
val targetBlock = target.block
201+
sourcePdc.copyTo(targetBlock)
202+
targetBlock.pdc().get(TEST_KEY, PersistentDataType.STRING)
203+
}
204+
}
205+
206+
result.onSuccess { copiedValue ->
207+
if (copiedValue == value) {
208+
sender.sendMessage("Block PDC copy test [$label] passed. Value '$copiedValue' was copied successfully.")
209+
} else {
210+
sender.sendMessage("Block PDC copy test [$label] failed. Expected '$value', got '${copiedValue ?: "null"}'.")
211+
}
212+
}.onFailure { error ->
213+
sender.sendMessage("Block PDC copy test [$label] failed with exception: ${error::class.simpleName}: ${error.message}")
214+
}
215+
}
216+
217+
private fun chunkDistance(source: Location, target: Location): Int {
218+
val dx = abs(source.chunkX - target.chunkX)
219+
val dz = abs(source.chunkZ - target.chunkZ)
220+
return max(dx, dz)
221+
}
222+
223+
companion object {
224+
private const val FAR_MIN_CHUNK_DISTANCE = 32
225+
private val TEST_KEY = NamespacedKey(plugin, "block-pdc-copy-test")
226+
}
227+
}
228+

0 commit comments

Comments
 (0)