4 Commits
Author SHA1 Message Date
dmg b0493bbf57 docs: point story tracking to Kaneo
Release / release (push) Successful in 1m52s
CI / build (push) Successful in 1m28s
2026-10-28 20:36:15 -04:00
dmg 78271bd9d1 feat(ignis): support Purpur 26.3 (#2)
Release / release (push) Successful in 1m24s
CI / build (push) Successful in 46s
OpenProject #410
2026-10-17 03:28:40 +00:00
dmg 4bd6576bba feat(ignis): declare Purpur 26.3 support
CI / build (push) Successful in 57s
CI / build (pull_request) Successful in 1m18s
2026-10-16 23:26:28 -04:00
dmg e3e2c63048 build(ignis): support Purpur 26.3
CI / build (push) Successful in 1m3s
2026-10-16 23:25:51 -04:00
8 changed files with 54 additions and 24 deletions

No files matched your search

+6 -2
View File
@@ -1,9 +1,13 @@
# Purpur Ignis agent entrypoint
The canonical knowledge bundle is the sibling `../somc-okf/` repository, remotely at https://git.garvis.dev/dmg/somc-okf/src/branch/main/index.md.
The canonical technical/process wiki is the sibling `../somc-okf/` repository, remotely at https://git.garvis.dev/dmg/somc-okf/src/branch/main/index.md.
Before changes read its root index, `processes/development.md`, `runbooks/development-cycle.md`, `projects/purpur-ignis/index.md`, `projects/purpur-ignis/engineering.md`, and the relevant `user-stories/purpur-ignis/` documents. Follow the parent workspace instructions. Do not create a local knowledge/design bundle.
Stories, acceptance criteria, status and evidence belong in [Kaneo Social Minecraft](https://cloud.kaneo.app/dmg/SM), with subjects `[purpur-ignis] 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.
Before changes read its root index, `processes/development.md`, `runbooks/development-cycle.md`, `projects/purpur-ignis/index.md`, `projects/purpur-ignis/engineering.md`, and the relevant Kaneo tickets with subject prefix `[purpur-ignis]`. Follow the parent workspace instructions. Do not create a local knowledge/design bundle.
Use Java 25 and the declared Purpur API. Test behavior first with meaningful failures, implement minimally, then run `./gradlew clean check jar`. Never mark server/client behavior verified through domain tests alone.
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.
Source/wiki publication, local-only GitOps preparation, and explicitly approved production release are separate boundaries. Never push GitOps during development. Do not expose credentials or place them in process arguments. Repository creation does not authorize credential provisioning.
+1 -1
View File
@@ -13,7 +13,7 @@ Requires Java 17+ to launch Gradle; the wrapper resolves a Java 25 toolchain for
./gradlew clean jar -PreleaseVersion=0.1.0
```
The runtime requires Java 25. New JARs are `build/libs/purpur-ignis-<version>.jar`. Targets Purpur `26.2.build.2618-stable` and requires **ProtocolLib 5.4.0** for Mask helmet projection. Neither server API nor ProtocolLib is bundled. Source builds do not require the private wiki.
The runtime requires Java 25. New JARs are `build/libs/purpur-ignis-<version>.jar`. Targets Purpur `26.3.build.2651-stable` and requires **ProtocolLib 5.4.0** for Mask helmet projection. Neither server API nor ProtocolLib is bundled. Source builds do not require the private wiki.
## Knowledge and delivery
+6 -6
View File
@@ -17,10 +17,10 @@ tasks.withType<JavaCompile>().configureEach {
}
dependencies {
compileOnly("org.purpurmc.purpur:purpur-api:26.2.build.2618-stable")
compileOnly("org.purpurmc.purpur:purpur-api:26.3.build.2651-stable")
compileOnly("net.dmulloy2:ProtocolLib:5.4.0")
testImplementation("net.dmulloy2:ProtocolLib:5.4.0")
testImplementation("org.purpurmc.purpur:purpur-api:26.2.build.2618-stable")
testImplementation("org.purpurmc.purpur:purpur-api:26.3.build.2651-stable")
testImplementation(platform("org.junit:junit-bom:5.13.4"))
testImplementation("org.junit.jupiter:junit-jupiter")
testImplementation("org.mockito:mockito-core:5.18.0")
@@ -36,24 +36,24 @@ tasks.test {
// Native API/event/delivery regressions run without starting a listening server.
val ignisRuntime = layout.buildDirectory.dir("ignis-runtime")
val downloadIgnisRuntime = tasks.register("downloadIgnisRuntime") {
val launcher = ignisRuntime.map { it.file("purpur-26.2-2618.jar") }
val launcher = ignisRuntime.map { it.file("purpur-26.3-2651.jar") }
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("https://api.purpurmc.org/v2/purpur/26.3/2651/download").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)
}
}
val prepareIgnisRuntime = tasks.register<JavaExec>("prepareIgnisRuntime") {
dependsOn(downloadIgnisRuntime)
javaLauncher = javaToolchains.launcherFor { languageVersion = JavaLanguageVersion.of(25) }
classpath = files(ignisRuntime.map { it.file("purpur-26.2-2618.jar") })
classpath = files(ignisRuntime.map { it.file("purpur-26.3-2651.jar") })
mainClass = "io.papermc.paperclip.Main"
jvmArgs("-Dpaperclip.patchonly=true")
workingDir(ignisRuntime)
@@ -29,9 +29,9 @@ class AnnouncementPluginLifecycleTest {
var platform = mockStatic(Bukkit.class, call -> switch (call.getMethod().getName()) {
case "getServer" -> server;
case "isPrimaryThread" -> true;
case "getVersion" -> "Purpur 2618 (MC: 26.2)";
case "getMinecraftVersion" -> "26.2";
case "getBukkitVersion" -> "26.2-R0.1-SNAPSHOT";
case "getVersion" -> "Purpur 2651 (MC: 26.3)";
case "getMinecraftVersion" -> "26.3";
case "getBukkitVersion" -> "26.3-R0.1-SNAPSHOT";
default -> call.callRealMethod();
})) {
var packets = new ArrayList<PacketListener>();
@@ -187,6 +187,8 @@ final class NativeDaggerFixture implements AutoCloseable {
doCallRealMethod().when(nativePlayer).getBoundingBox();
when(nativePlayer.getDeltaMovement()).thenReturn(Vec3.ZERO);
when(nativePlayer.getKnownMovement()).thenReturn(Vec3.ZERO);
when(nativePlayer.getClientPositionAndRotation()).thenAnswer(call ->
net.minecraft.core.PositionAndRotation.of(nativePlayer.position(), nativePlayer.getYRot(), nativePlayer.getXRot()));
doCallRealMethod().when(nativePlayer).setPos(anyDouble(), anyDouble(), anyDouble());
doCallRealMethod().when(nativePlayer).setBoundingBox(any(net.minecraft.world.phys.AABB.class));
doCallRealMethod().when(nativePlayer).setPosRaw(anyDouble(), anyDouble(), anyDouble());
@@ -273,8 +275,10 @@ final class NativeDaggerFixture implements AutoCloseable {
var key = net.minecraft.resources.ResourceKey.<net.minecraft.world.item.crafting.Recipe<?>>create(
net.minecraft.core.registries.Registries.RECIPE, net.minecraft.resources.Identifier.fromNamespaceAndPath("spigotignis", "dagger"));
var holder = new net.minecraft.world.item.crafting.RecipeHolder<net.minecraft.world.item.crafting.CraftingRecipe>(key, nativeRecipe);
var recipes = new net.minecraft.world.item.crafting.RecipeManager(org.bukkit.craftbukkit.CraftRegistry.getMinecraftRegistry());
recipes.recipes = net.minecraft.world.item.crafting.RecipeMap.create(List.of(holder));
var recipes = NativeRuntime.recipes();
recipes.recipes = net.minecraft.world.item.crafting.RecipeMap.createEmpty();
recipes.recipes.addRecipe(holder);
recipes.finalizeRecipeLoading(net.minecraft.world.flag.FeatureFlags.VANILLA_SET);
when(level.recipeAccess()).thenReturn(recipes);
menu.craftSlots.setCurrentRecipe(holder);
menu.resultSlots.setRecipeUsed(holder);
@@ -51,9 +51,9 @@ class NativeEchoAnnouncementTest {
try (var platform = mockStatic(Bukkit.class, call -> switch (call.getMethod().getName()) {
case "getServer" -> server;
case "isPrimaryThread" -> true;
case "getVersion" -> "Purpur 2618 (MC: 26.2)";
case "getMinecraftVersion" -> "26.2";
case "getBukkitVersion" -> "26.2-R0.1-SNAPSHOT";
case "getVersion" -> "Purpur 2651 (MC: 26.3)";
case "getMinecraftVersion" -> "26.3";
case "getBukkitVersion" -> "26.3-R0.1-SNAPSHOT";
default -> call.callRealMethod();
})) {
UUID owner = UUID.randomUUID(), receipt = UUID.randomUUID();
@@ -21,6 +21,7 @@ import net.minecraft.server.permissions.LevelBasedPermissionSet;
import net.minecraft.tags.TagLoader;
import net.minecraft.util.Util;
import net.minecraft.world.flag.FeatureFlags;
import net.minecraft.world.item.crafting.RecipeManager;
import net.minecraft.world.level.DataPackConfig;
import net.minecraft.world.level.WorldDataConfiguration;
import org.bukkit.craftbukkit.CraftRegistry;
@@ -29,6 +30,7 @@ import org.bukkit.craftbukkit.CraftRegistry;
final class NativeRuntime {
private NativeRuntime() { }
private static boolean ready;
private static RecipeManager recipes;
static synchronized void bootstrap() throws Exception {
if (ready) { return; }
SharedConstants.tryDetectVersion();
@@ -40,12 +42,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());
@@ -54,7 +56,16 @@ final class NativeRuntime {
Util.backgroundExecutor(), Runnable::run).join();
datapack.updateComponentsAndStaticRegistryTags();
CraftRegistry.setMinecraftRegistry(layers.compositeAccess().freeze());
recipes = datapack.getRecipeManager();
recipes.finalizeRecipeLoading(flags);
}
ready = true;
}
static RecipeManager recipes() {
if (!ready) {
throw new IllegalStateException("Native runtime has not been bootstrapped");
}
return recipes;
}
}
@@ -21,6 +21,9 @@ class PurpurDistributionTest {
assertEquals(version, metadata.get("version"));
assertEquals("SpigotIgnis", metadata.get("name"), "Retain the data directory and persistent namespace");
assertEquals("games.dmg.spigotignis.SpigotIgnisPlugin", metadata.get("main"));
assertEquals("1.21", metadata.get("api-version"));
assertTrue(((Map<?, ?>) metadata.get("commands")).keySet().containsAll(
java.util.Set.of("ignis", "ignisadmin")));
assertTrue(((Map<?, ?>) metadata.get("permissions")).containsKey("ignis.admin"));
assertEquals(java.util.List.of("ProtocolLib"), metadata.get("depend"));
var descriptor = new org.bukkit.plugin.PluginDescriptionFile(jar.getInputStream(jar.getJarEntry("plugin.yml")));
@@ -42,10 +45,18 @@ class PurpurDistributionTest {
}
@Test
void releaseAndCiAgreeWithThePurpurDistributionNameAndToolchain() throws Exception {
void releaseAndCiAgreeWithThePurpur26_3DistributionAndToolchain() throws Exception {
String build = Files.readString(Path.of("build.gradle.kts"));
assertTrue(build.contains("purpur-api:26.2.build.2618-stable"));
assertTrue(build.contains("purpur-api:26.3.build.2651-stable"));
assertTrue(build.contains("purpur-26.3-2651.jar"));
assertTrue(build.contains("https://api.purpurmc.org/v2/purpur/26.3/2651/download"));
assertTrue(build.contains("0677c1febecfbc2e5b0a7f76abd647b70a1f38d3cbdf3dd6a82e401b4262a598"));
assertTrue(build.contains("net.dmulloy2:ProtocolLib:5.4.0"));
assertFalse(build.contains("26.2") || build.contains("2618"));
assertFalse(build.contains("org.spigotmc:spigot-api"));
String readme = Files.readString(Path.of("README.md"));
assertTrue(readme.contains("Purpur `26.3.build.2651-stable`"));
assertFalse(readme.contains("26.2") || readme.contains("2618"));
for (String workflow : new String[] {"ci.yml", "release.yml"}) {
String content = Files.readString(Path.of(".gitea/workflows", workflow));
assertTrue(content.contains("java-version: 25"));