Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
26bead3702 | ||
|
|
3b87f64eef | ||
|
|
c701f75713 |
No files matched your search
@@ -1,9 +1,13 @@
|
||||
# Purpur Potato
|
||||
|
||||
Canonical knowledge and stories live in the sibling `somc-okf/` checkout, not this public source repository. Read `../AGENTS.md` when available and the wiki's `index.md`, `processes/index.md`, `processes/development.md`, `runbooks/development-cycle.md`, `projects/purpur-potato/index.md`, `engineering.md`, and relevant `user-stories/purpur-potato/` documents before work.
|
||||
Canonical technical/process documentation lives in the sibling `somc-okf/` checkout, not this public source repository. Read `../AGENTS.md` when available and the wiki's `index.md`, `processes/index.md`, `processes/development.md`, `runbooks/development-cycle.md`, `projects/purpur-potato/index.md`, `engineering.md`, and relevant Kaneo tickets with subject prefix `[purpur-potato]` before work.
|
||||
|
||||
Stories, acceptance criteria, status and evidence belong in [Kaneo Social Minecraft](https://cloud.kaneo.app/dmg/SM), with subjects `[purpur-potato] Descriptive title` (no `US-###` or `FS-*` prefix). Use the configured authenticated `kaneo` MCP server per [Kaneo access and migration](https://git.garvis.dev/dmg/somc-okf/src/branch/main/processes/development.md#kaneo-access-and-migration); report unavailable access rather than creating local story copies. OpenProject is read-only history.
|
||||
|
||||
Standalone checkout entrypoint: https://git.garvis.dev/dmg/somc-okf/src/branch/main/index.md . If unavailable, request access; do not create a substitute knowledge bundle or copy private wiki content here.
|
||||
|
||||
Obtain explicit story-change approval before implementation and operational approval before infrastructure, tooling or secret changes. Use vertical-slice TDD, retain native combat/protection decisions and verify `./gradlew clean check jar`, including mandatory non-listening native tests. Java 25, Gradle 9.6.1, Purpur API `26.2.build.2618-stable`; do not silently upgrade. Keep blocking I/O off the tick thread.
|
||||
Obtain explicit story-change approval before implementation and operational approval before infrastructure, tooling or secret changes. Use vertical-slice TDD, retain native combat/protection decisions and verify `./gradlew clean check jar`, including mandatory non-listening native tests. Java 25, Gradle 9.6.1, Purpur API `26.3.build.2651-stable`; do not silently upgrade. Keep blocking I/O off the tick thread.
|
||||
|
||||
Follow the full [development process](https://git.garvis.dev/dmg/somc-okf/src/branch/main/processes/development.md) and [development cycle](https://git.garvis.dev/dmg/somc-okf/src/branch/main/runbooks/development-cycle.md) for approvals, test-first/full-suite verification, required Gitea PR checks/reviews and merge, and merged-tree verification before ticket completion. Never push source `main` directly.
|
||||
|
||||
Publishing credentials and release-triggering pushes require separate readiness verification and authorization. Never put secret bytes in argv, URLs, logs or Git. No deployment/GitOps push follows implicitly from source work. Preserve unrelated changes and use conventional commits.
|
||||
@@ -1,6 +1,6 @@
|
||||
# Purpur Potato
|
||||
|
||||
A Java 25 plugin for Purpur 26.2 build 2618. Right-click a poisonous potato to throw it with native snowball flight and a potato visual. Each successful launch costs one potato (including Creative), has a 20-tick cooldown, and deals one health point of base damage without poison. Native damage modifiers, PvP rules and protection cancellations remain authoritative. Projectiles expire after 100 ticks; misses are not refunded.
|
||||
A Java 25 plugin for Purpur 26.3 build 2651. Right-click a poisonous potato to throw it with native snowball flight and a potato visual. Each successful launch costs one potato (including Creative), has a 20-tick cooldown, and deals one health point of base damage without poison. Native damage modifiers, PvP rules and protection cancellations remain authoritative. Projectiles expire after 100 ticks; misses are not refunded.
|
||||
|
||||
`/potato [help]` explains the mechanic to players and console. `purpurpotato.help` defaults to true. No recipe, unlock, persistent data or runtime credential is required.
|
||||
|
||||
@@ -11,7 +11,7 @@ A Java 25 plugin for Purpur 26.2 build 2618. Right-click a poisonous potato to t
|
||||
./gradlew clean check jar -PreleaseVersion=1.2.3
|
||||
```
|
||||
|
||||
Gradle 9.6.1 selects Java 25. Full checks include Java tests, mandatory non-listening native tests against the checksum-pinned Purpur runtime, and TypeScript release-tool tests (`npm ci` requires Node 20+). Native preparation runs Paperclip **patch-only**: it does not start a server, listen on a port or access production. Build downloads require network access. Generated runtime files stay under ignored `build/`.
|
||||
Gradle 9.6.1 selects Java 25 and compiles against Purpur API `26.3.build.2651-stable`. Full checks include Java tests, mandatory non-listening native tests against the checksum-pinned `purpur-26.3-2651.jar` runtime, and TypeScript release-tool tests (`npm ci` requires Node 20+). Native preparation runs Paperclip **patch-only** in a build-specific directory: it does not start a server, listen on a port, access production, or reuse an extracted runtime from another supported build. Build downloads require network access. Generated runtime files stay under ignored `build/`.
|
||||
|
||||
Outputs: `build/libs/purpur-potato-<version>.jar`. Server and test dependencies are not bundled. Do not deploy snapshot workflow artifacts.
|
||||
|
||||
|
||||
+14
-9
@@ -25,10 +25,15 @@ tasks.withType<JavaCompile>().configureEach {
|
||||
options.compilerArgs.addAll(listOf("-Xlint:all,-deprecation,-removal", "-Werror"))
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compileOnly("org.purpurmc.purpur:purpur-api:26.2.build.2618-stable")
|
||||
val purpurApi = "org.purpurmc.purpur:purpur-api:26.3.build.2651-stable"
|
||||
val nativeRuntimeName = "purpur-26.3-2651.jar"
|
||||
val nativeRuntimeUrl = "https://api.purpurmc.org/v2/purpur/26.3/2651/download"
|
||||
|
||||
testImplementation("org.purpurmc.purpur:purpur-api:26.2.build.2618-stable")
|
||||
|
||||
dependencies {
|
||||
compileOnly(purpurApi)
|
||||
|
||||
testImplementation(purpurApi)
|
||||
testImplementation(platform("org.junit:junit-bom:5.13.4"))
|
||||
testImplementation("org.junit.jupiter:junit-jupiter")
|
||||
testImplementation("org.mockito:mockito-core:5.18.0")
|
||||
@@ -44,20 +49,20 @@ tasks.test {
|
||||
}
|
||||
|
||||
// Verify native projectile adapters against the declared runtime, without starting a server.
|
||||
val nativeRuntime = layout.buildDirectory.dir("native-runtime")
|
||||
val nativeRuntime = layout.buildDirectory.dir("native-runtime/26.3-2651")
|
||||
val downloadNativeRuntime = tasks.register("downloadNativeRuntime") {
|
||||
val launcher = nativeRuntime.map { it.file("purpur-26.2-2618.jar") }
|
||||
val launcher = nativeRuntime.map { it.file(nativeRuntimeName) }
|
||||
outputs.file(launcher)
|
||||
doLast {
|
||||
val file = launcher.get().asFile
|
||||
file.parentFile.mkdirs()
|
||||
val connection = URI("https://api.purpurmc.org/v2/purpur/26.2/2618/download").toURL().openConnection()
|
||||
val connection = URI(nativeRuntimeUrl).toURL().openConnection()
|
||||
connection.connectTimeout = 30_000
|
||||
connection.readTimeout = 120_000
|
||||
val bytes = connection.getInputStream().use { it.readBytes() }
|
||||
val digest = MessageDigest.getInstance("SHA-256").digest(bytes).joinToString("") { "%02x".format(it) }
|
||||
check(digest == "4a32d046a118804d89ca74ba89b798c98f6d8d1f310c18077ac573597049de31") {
|
||||
"Purpur 2618 checksum mismatch"
|
||||
check(digest == "0677c1febecfbc2e5b0a7f76abd647b70a1f38d3cbdf3dd6a82e401b4262a598") {
|
||||
"Purpur 2651 checksum mismatch"
|
||||
}
|
||||
file.writeBytes(bytes)
|
||||
}
|
||||
@@ -65,7 +70,7 @@ val downloadNativeRuntime = tasks.register("downloadNativeRuntime") {
|
||||
val prepareNativeRuntime = tasks.register<JavaExec>("prepareNativeRuntime") {
|
||||
dependsOn(downloadNativeRuntime)
|
||||
javaLauncher = javaToolchains.launcherFor { languageVersion = JavaLanguageVersion.of(25) }
|
||||
classpath = files(nativeRuntime.map { it.file("purpur-26.2-2618.jar") })
|
||||
classpath = files(nativeRuntime.map { it.file(nativeRuntimeName) })
|
||||
mainClass = "io.papermc.paperclip.Main"
|
||||
jvmArgs("-Dpaperclip.patchonly=true")
|
||||
workingDir(nativeRuntime)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
name: PurpurPotato
|
||||
version: '${version}'
|
||||
main: games.dmg.purpurpotato.PurpurPotato
|
||||
api-version: '26.2'
|
||||
api-version: '26.3'
|
||||
description: Throw consumable poisonous potatoes with native snowball flight.
|
||||
commands:
|
||||
potato:
|
||||
|
||||
@@ -101,6 +101,7 @@ class NativeCombatTest {
|
||||
when(nativePlayer.getBukkitEntity()).thenReturn(player);
|
||||
when(player.getScoreboard()).thenReturn(mock(org.bukkit.craftbukkit.scoreboard.CraftScoreboard.class));
|
||||
when(nativePlayer.level()).thenReturn(world);
|
||||
when(nativePlayer.position()).thenReturn(Vec3.ZERO);
|
||||
when(nativePlayer.getAdvancements()).thenReturn(mock(net.minecraft.server.PlayerAdvancements.class));
|
||||
when(player.getUniqueId()).thenReturn(UUID.randomUUID());
|
||||
var inventory = mock(PlayerInventory.class);
|
||||
@@ -140,6 +141,7 @@ class NativeCombatTest {
|
||||
if (target instanceof ServerPlayer nativeVictim) {
|
||||
when(world.isPvpAllowed()).thenReturn(!mode.equals("pvp-denied"));
|
||||
when(nativeVictim.canHarmPlayer(nativePlayer)).thenCallRealMethod();
|
||||
when(nativeVictim.doTeamsAllowDamage(nativePlayer)).thenCallRealMethod();
|
||||
var abilities = net.minecraft.world.entity.player.Player.class.getDeclaredField("abilities");
|
||||
abilities.setAccessible(true);
|
||||
abilities.set(nativeVictim, new net.minecraft.world.entity.player.Abilities());
|
||||
|
||||
@@ -43,12 +43,12 @@ final class NativeRuntime {
|
||||
try (var resources = new MultiPackResourceManager(PackType.SERVER_DATA, packs.openAllSelected())) {
|
||||
LayeredRegistryAccess<RegistryLayer> layers = RegistryLayer.createRegistryAccess();
|
||||
List<Registry.PendingTags<?>> tags = TagLoader.loadTagsForExistingRegistries(resources, layers.getLayer(RegistryLayer.STATIC));
|
||||
List<HolderLookup.RegistryLookup<?>> lookups = TagLoader.buildUpdatedLookups(layers.getAccessForLoading(RegistryLayer.WORLDGEN), tags);
|
||||
RegistryAccess.Frozen worldgen = RegistryDataLoader.load(resources, lookups,
|
||||
RegistryDataLoader.WORLDGEN_REGISTRIES, Util.backgroundExecutor()).join();
|
||||
layers = layers.replaceFrom(RegistryLayer.WORLDGEN, worldgen);
|
||||
List<HolderLookup.RegistryLookup<?>> lookups = TagLoader.buildUpdatedLookups(layers.getAccessForLoading(RegistryLayer.WORLD), tags);
|
||||
RegistryAccess.Frozen world = RegistryDataLoader.load(resources, lookups,
|
||||
RegistryDataLoader.WORLD_REGISTRIES, Util.backgroundExecutor()).join();
|
||||
layers = layers.replaceFrom(RegistryLayer.WORLD, world);
|
||||
RegistryAccess.Frozen dimensions = RegistryDataLoader.load(resources,
|
||||
Stream.concat(lookups.stream(), worldgen.listRegistries()).toList(),
|
||||
Stream.concat(lookups.stream(), world.listRegistries()).toList(),
|
||||
RegistryDataLoader.DIMENSION_REGISTRIES, Util.backgroundExecutor()).join();
|
||||
layers = layers.replaceFrom(RegistryLayer.DIMENSIONS, dimensions);
|
||||
Class.forName(org.bukkit.Registry.class.getName());
|
||||
|
||||
@@ -47,7 +47,7 @@ class PluginLifecycleTest {
|
||||
when(plugin.getLogger()).thenReturn(java.util.logging.Logger.getLogger("PotatoTest"));
|
||||
try (var stream = getClass().getResourceAsStream("/plugin.yml")) {
|
||||
var description = new PluginDescriptionFile(stream);
|
||||
assertEquals("26.2", description.getAPIVersion());
|
||||
assertEquals("26.3", description.getAPIVersion());
|
||||
when(plugin.getDescription()).thenReturn(description);
|
||||
}
|
||||
var constructor = PluginCommand.class.getDeclaredConstructor(String.class, org.bukkit.plugin.Plugin.class);
|
||||
|
||||
@@ -19,7 +19,7 @@ class DistributionTest {
|
||||
assertEquals("PurpurPotato", metadata.get("name"));
|
||||
assertEquals(version, metadata.get("version"));
|
||||
assertEquals("games.dmg.purpurpotato.PurpurPotato", metadata.get("main"));
|
||||
assertEquals("26.2", metadata.get("api-version"));
|
||||
assertEquals("26.3", metadata.get("api-version"));
|
||||
var commands = (Map<?, ?>) metadata.get("commands");
|
||||
assertEquals(1, commands.size());
|
||||
var potato = (Map<?, ?>) commands.get("potato");
|
||||
@@ -27,6 +27,9 @@ class DistributionTest {
|
||||
assertEquals("purpurpotato.help", potato.get("permission"));
|
||||
var permissions = (Map<?, ?>) metadata.get("permissions");
|
||||
assertEquals(true, ((Map<?, ?>) permissions.get("purpurpotato.help")).get("default"));
|
||||
assertFalse(metadata.containsKey("depend"));
|
||||
assertFalse(metadata.containsKey("softdepend"));
|
||||
assertFalse(metadata.containsKey("loadbefore"));
|
||||
var entry = jar.getEntry("games/dmg/purpurpotato/PurpurPotato.class");
|
||||
assertNotNull(entry);
|
||||
try (var bytes = new DataInputStream(jar.getInputStream(entry))) {
|
||||
@@ -34,9 +37,15 @@ class DistributionTest {
|
||||
bytes.readUnsignedShort();
|
||||
assertEquals(69, bytes.readUnsignedShort());
|
||||
}
|
||||
assertTrue(jar.stream().filter(e -> e.getName().endsWith(".class"))
|
||||
.allMatch(e -> e.getName().startsWith("games/dmg/purpurpotato/")
|
||||
&& !e.getName().endsWith("Test.class")), "No server or test dependencies may be bundled");
|
||||
assertTrue(jar.stream().allMatch(e -> {
|
||||
String name = e.getName();
|
||||
return e.isDirectory()
|
||||
|| name.equals("META-INF/MANIFEST.MF")
|
||||
|| name.equals("plugin.yml")
|
||||
|| name.startsWith("games/dmg/purpurpotato/")
|
||||
&& name.endsWith(".class")
|
||||
&& !name.endsWith("Test.class");
|
||||
}), "Only plugin classes, metadata, and the manifest may be bundled");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user