diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 39c0bb5..42e7d06 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -17,7 +17,7 @@ jobs: uses: actions/setup-java@v4 with: distribution: temurin - java-version: 17 + java-version: 25 cache: gradle - name: Validate conventional commits @@ -35,6 +35,6 @@ jobs: - name: Upload development artifact uses: actions/upload-artifact@v3 with: - name: spigot-tyrant-${{ github.sha }} + name: purpur-tyrant-${{ github.sha }} path: build/libs/*.jar if-no-files-found: error diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml index 711373f..7f079e3 100644 --- a/.gitea/workflows/release.yml +++ b/.gitea/workflows/release.yml @@ -21,7 +21,7 @@ jobs: uses: actions/setup-java@v4 with: distribution: temurin - java-version: 17 + java-version: 25 cache: gradle - name: Set up Node.js @@ -86,8 +86,8 @@ jobs: if: steps.release.outputs.created == 'true' uses: actions/upload-artifact@v3 with: - name: spigot-tyrant-${{ steps.release.outputs.version }} - path: build/libs/spigot-tyrant-${{ steps.release.outputs.version }}.jar + name: purpur-tyrant-${{ steps.release.outputs.version }} + path: build/libs/purpur-tyrant-${{ steps.release.outputs.version }}.jar if-no-files-found: error - name: Create Gitea release and upload plugin @@ -101,7 +101,7 @@ jobs: PREVIOUS_TAG: ${{ steps.previous_tag.outputs.value }} run: | api_url="${GITEA_SERVER_URL}/api/v1" - jar="build/libs/spigot-tyrant-${VERSION}.jar" + jar="build/libs/purpur-tyrant-${VERSION}.jar" export RELEASE_BODY if [ -n "$PREVIOUS_TAG" ]; then RELEASE_BODY=$(git log --pretty='format:- %s (%h)' "${PREVIOUS_TAG}..HEAD") @@ -138,4 +138,4 @@ jobs: -H "Authorization: token ${GITEA_TOKEN}" \ -H "Content-Type: application/octet-stream" \ --data-binary "@${jar}" \ - "${api_url}/repos/${REPOSITORY}/releases/${release_id}/assets?name=spigot-tyrant-${VERSION}.jar" + "${api_url}/repos/${REPOSITORY}/releases/${release_id}/assets?name=purpur-tyrant-${VERSION}.jar" diff --git a/AGENTS.md b/AGENTS.md index 63e5375..5a17445 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,9 +1,9 @@ -# spigot-tyrant agent entrypoint +# purpur-tyrant agent entrypoint The canonical stories, engineering guidance, and **all process documents** are in the private [SoMC OKF wiki](https://git.garvis.dev/dmg/somc-okf/src/branch/main/index.md). -Before work, read the sibling `../somc-okf/index.md`, `../somc-okf/processes/index.md`, `../somc-okf/projects/spigot-tyrant/index.md`, `engineering.md` in that project section, and relevant `../somc-okf/user-stories/spigot-tyrant/` stories. Also follow the parent workspace `AGENTS.md` when present. +Before work, read the sibling `../somc-okf/index.md`, `../somc-okf/processes/index.md`, `../somc-okf/projects/purpur-tyrant/index.md`, `engineering.md` in that project section, and relevant `../somc-okf/user-stories/purpur-tyrant/` stories. Also follow the parent workspace `AGENTS.md` when present. -For standalone checkouts, start at the [project page](https://git.garvis.dev/dmg/somc-okf/src/branch/main/projects/spigot-tyrant/index.md) and [shared process](https://git.garvis.dev/dmg/somc-okf/src/branch/main/processes/development.md). Obtain wiki access before feature work; do not recreate a local knowledge bundle. Source builds do not require private wiki access. +For standalone checkouts, start at the [project page](https://git.garvis.dev/dmg/somc-okf/src/branch/main/projects/purpur-tyrant/index.md) and [shared process](https://git.garvis.dev/dmg/somc-okf/src/branch/main/processes/development.md). Obtain wiki access before feature work; do not recreate a local knowledge bundle. Source builds do not require private wiki access. Development follows [Development cycle](https://git.garvis.dev/dmg/somc-okf/src/branch/main/runbooks/development-cycle.md): approved stories, failing tests, passing implementation, verification, then source/wiki commit and push. GitOps updates are committed locally **without pushing**; only [Do release](https://git.garvis.dev/dmg/somc-okf/src/branch/main/runbooks/do-release.md) authorizes a reviewed GitOps push. diff --git a/README.md b/README.md new file mode 100644 index 0000000..d3dc9f8 --- /dev/null +++ b/README.md @@ -0,0 +1,21 @@ +# Purpur Tyrant + +The asymmetric Tyrant/Vigilante game plugin, targeting Java 25 and Purpur 26.2 build 2618. + +## Build and releases + +```sh +./gradlew clean check jar +``` + +New artifacts use `build/libs/purpur-tyrant-.jar`. Gitea CI checks pushes/pull requests; approved conventional commits on main drive versioned releases. Distribution tests inspect the actual JAR, version, Java 25 bytecode, retained runtime identity and workflow names. + +## Migration compatibility + +The repository and checkout are now `purpur-tyrant`, previously `spigot-tyrant`. Runtime identity remains **SpigotTyrant**, including `plugins/SpigotTyrant/`, Java entrypoints/packages, command/permission names and persistent keys for roles, captured mobs and legacy items. The migration changes the build/distribution target, not gameplay or persisted schemas. New releases no longer support Java 17. + +Replace the old plugin JAR rather than installing both distributions. Old tags/assets remain available; both the origin GitOps v1.24.0 and separately prepared local v1.25.0 URLs were verified to redirect and download identical bytes after the repository rename. No deployment or GitOps push is implied by publication. + +Unrelated local testing files are preserved; their commands may still reference the historical artifact names. + +See the [canonical project](https://git.garvis.dev/dmg/somc-okf/src/branch/main/projects/purpur-tyrant/index.md), [stories](https://git.garvis.dev/dmg/somc-okf/src/branch/main/user-stories/purpur-tyrant/index.md), and [development cycle](https://git.garvis.dev/dmg/somc-okf/src/branch/main/runbooks/development-cycle.md). diff --git a/build.gradle.kts b/build.gradle.kts index 97df943..a1b1857 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -9,23 +9,24 @@ version = providers.gradleProperty("releaseVersion") repositories { mavenCentral() - maven("https://hub.spigotmc.org/nexus/content/repositories/snapshots/") + maven("https://repo.purpurmc.org/snapshots/") } java { toolchain { - languageVersion = JavaLanguageVersion.of(17) + languageVersion = JavaLanguageVersion.of(25) } } tasks.withType().configureEach { - options.compilerArgs.addAll(listOf("-Xlint:all", "-Werror")) + // Retain supported legacy Bukkit APIs while preserving runtime compatibility. + options.compilerArgs.addAll(listOf("-Xlint:all,-deprecation,-removal", "-Werror")) } dependencies { - compileOnly("org.spigotmc:spigot-api:26.2-R0.1-SNAPSHOT") + compileOnly("org.purpurmc.purpur:purpur-api:26.2.build.2618-stable") - testImplementation("org.spigotmc:spigot-api:26.2-R0.1-SNAPSHOT") + testImplementation("org.purpurmc.purpur:purpur-api:26.2.build.2618-stable") testImplementation(platform("org.junit:junit-bom:5.13.4")) testImplementation("org.junit.jupiter:junit-jupiter") testImplementation("org.mockito:mockito-core:5.18.0") @@ -35,6 +36,9 @@ dependencies { tasks.test { useJUnitPlatform() + dependsOn(tasks.jar) + systemProperty("distribution.jar", tasks.jar.get().archiveFile.get().asFile.absolutePath) + systemProperty("distribution.version", project.version.toString()) } val pluginVersion = version diff --git a/settings.gradle.kts b/settings.gradle.kts index fba698b..901f6ca 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1 +1 @@ -rootProject.name = "spigot-tyrant" +rootProject.name = "purpur-tyrant" diff --git a/src/test/java/games/dmg/spigottyrant/BukkitRoleControlItemServiceTest.java b/src/test/java/games/dmg/spigottyrant/BukkitRoleControlItemServiceTest.java index eaec356..eca47e3 100644 --- a/src/test/java/games/dmg/spigottyrant/BukkitRoleControlItemServiceTest.java +++ b/src/test/java/games/dmg/spigottyrant/BukkitRoleControlItemServiceTest.java @@ -33,6 +33,7 @@ class BukkitRoleControlItemServiceTest { void setup() { Plugin plugin = mock(Plugin.class); when(plugin.getName()).thenReturn("SpigotTyrant"); + when(plugin.namespace()).thenReturn("spigottyrant"); service = new BukkitRoleControlItemService(plugin, PluginSettings.from(Map.of())); when(player.getUniqueId()).thenReturn(owner); when(player.getInventory()).thenReturn(inventory); diff --git a/src/test/java/games/dmg/spigottyrant/CapturedMobInventorySynchronizationTest.java b/src/test/java/games/dmg/spigottyrant/CapturedMobInventorySynchronizationTest.java index a85a7c0..f5f8210 100644 --- a/src/test/java/games/dmg/spigottyrant/CapturedMobInventorySynchronizationTest.java +++ b/src/test/java/games/dmg/spigottyrant/CapturedMobInventorySynchronizationTest.java @@ -79,6 +79,7 @@ class CapturedMobInventorySynchronizationTest { when(view.getCursor()).thenAnswer(call -> cursor.get()); Plugin plugin = mock(Plugin.class); when(plugin.getName()).thenReturn("SpigotTyrant"); + when(plugin.namespace()).thenReturn("spigottyrant"); items = new BukkitCapturedMobItemService(plugin, PluginSettings.from(Map.of())); egg = egg(owner, captureId); contents[3] = egg; diff --git a/src/test/java/games/dmg/spigottyrant/PurpurDistributionTest.java b/src/test/java/games/dmg/spigottyrant/PurpurDistributionTest.java new file mode 100644 index 0000000..3e868b3 --- /dev/null +++ b/src/test/java/games/dmg/spigottyrant/PurpurDistributionTest.java @@ -0,0 +1,53 @@ +package games.dmg.spigottyrant; + +import static org.junit.jupiter.api.Assertions.*; + +import java.io.DataInputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Map; +import java.util.jar.JarFile; +import org.junit.jupiter.api.Test; +import org.yaml.snakeyaml.Yaml; + +class PurpurDistributionTest { + @Test + void packagesNewNameWithoutChangingRuntimeIdentityOrBundlingTheApi() throws Exception { + Path artifact = Path.of(System.getProperty("distribution.jar")); + String version = System.getProperty("distribution.version"); + assertEquals("purpur-tyrant-" + version + ".jar", artifact.getFileName().toString()); + try (var jar = new JarFile(artifact.toFile())) { + Map metadata = new Yaml().load(jar.getInputStream(jar.getJarEntry("plugin.yml"))); + assertEquals(version, metadata.get("version")); + assertEquals("SpigotTyrant", metadata.get("name"), "Retain the data directory and persistent namespace"); + assertEquals("games.dmg.spigottyrant.SpigotTyrantPlugin", metadata.get("main")); + assertTrue(((Map) metadata.get("permissions")).containsKey("spigottyrant.admin")); + assertFalse(jar.stream().anyMatch(entry -> entry.getName().startsWith("org/bukkit/") + || entry.getName().startsWith("org/purpurmc/") || entry.getName().endsWith("Test.class"))); + } + } + + @Test + void pluginBytecodeTargetsJava25() throws Exception { + try (var jar = new JarFile(System.getProperty("distribution.jar")); + var bytecode = new DataInputStream(jar.getInputStream( + jar.getJarEntry("games/dmg/spigottyrant/SpigotTyrantPlugin.class")))) { + assertEquals(0xCAFEBABE, bytecode.readInt()); + bytecode.readUnsignedShort(); + assertEquals(69, bytecode.readUnsignedShort()); + } + } + + @Test + void releaseAndCiAgreeWithThePurpurDistributionNameAndToolchain() throws Exception { + String build = Files.readString(Path.of("build.gradle.kts")); + assertTrue(build.contains("purpur-api:26.2.build.2618-stable")); + assertFalse(build.contains("org.spigotmc:spigot-api")); + for (String workflow : new String[] {"ci.yml", "release.yml"}) { + String content = Files.readString(Path.of(".gitea/workflows", workflow)); + assertTrue(content.contains("java-version: 25")); + assertTrue(content.contains("purpur-tyrant-"), workflow); + assertFalse(content.contains("spigot-tyrant-"), workflow); + } + } +} diff --git a/src/test/java/games/dmg/spigottyrant/RoleArenaLifecycleTest.java b/src/test/java/games/dmg/spigottyrant/RoleArenaLifecycleTest.java index b43ef91..50cda95 100644 --- a/src/test/java/games/dmg/spigottyrant/RoleArenaLifecycleTest.java +++ b/src/test/java/games/dmg/spigottyrant/RoleArenaLifecycleTest.java @@ -27,7 +27,7 @@ final class RoleArenaLifecycleTest { void vacantArenaWithoutNearbyPlayersDoesNotSpawnABoss() { Fixture f = new Fixture(); f.controller.run(); - verify(f.world, never()).spawn(any(Location.class), eq(Zombie.class), any()); + verify(f.world, never()).spawn(any(Location.class), eq(Zombie.class), org.mockito.ArgumentMatchers.>any()); } @org.junit.jupiter.params.ParameterizedTest @@ -39,7 +39,7 @@ final class RoleArenaLifecycleTest { } Player player = f.player(18.01, 64, 0); f.controller.run(); - verify(f.world, never()).spawn(any(Location.class), eq(Zombie.class), any()); + verify(f.world, never()).spawn(any(Location.class), eq(Zombie.class), org.mockito.ArgumentMatchers.>any()); when(player.getLocation()).thenReturn(new Location(f.world, 18, 64, 0)); f.controller.run(); f.controller.run(); @@ -61,11 +61,11 @@ final class RoleArenaLifecycleTest { when(f.world.isChunkLoaded(0, 0)).thenReturn(false); f.controller.run(); verify(f.world, never()).getChunkAt(0, 0); - verify(f.world, never()).spawn(any(Location.class), eq(Zombie.class), any()); + verify(f.world, never()).spawn(any(Location.class), eq(Zombie.class), org.mockito.ArgumentMatchers.>any()); when(f.world.isChunkLoaded(0, 0)).thenReturn(true); when(f.chunk.isEntitiesLoaded()).thenReturn(false); f.controller.run(); - verify(f.world, never()).spawn(any(Location.class), eq(Zombie.class), any()); + verify(f.world, never()).spawn(any(Location.class), eq(Zombie.class), org.mockito.ArgumentMatchers.>any()); when(f.chunk.isEntitiesLoaded()).thenReturn(true); f.controller.run(); org.junit.jupiter.api.Assertions.assertEquals(1, f.spawned.size()); @@ -132,7 +132,7 @@ final class RoleArenaLifecycleTest { f.player(15, 64, 0); Zombie rejected = f.taggedBoss(); f.entities.remove(rejected.getUniqueId()); - when(f.world.spawn(any(Location.class), eq(Zombie.class), any())).thenReturn(rejected); + when(f.world.spawn(any(Location.class), eq(Zombie.class), org.mockito.ArgumentMatchers.>any())).thenReturn(rejected); f.controller.run(); verify(rejected).remove(); verify(f.world, never()).spawnParticle(eq(org.bukkit.Particle.PORTAL), any(Location.class), @@ -236,13 +236,13 @@ final class RoleArenaLifecycleTest { void failedSpawnLeavesNoOwnershipAndBacksOffBeforeRetry() { Fixture f = new Fixture(); f.player(15, 64, 0); - when(f.world.spawn(any(Location.class), eq(Zombie.class), any())) + when(f.world.spawn(any(Location.class), eq(Zombie.class), org.mockito.ArgumentMatchers.>any())) .thenThrow(new IllegalStateException("spawn rejected")); org.junit.jupiter.api.Assertions.assertDoesNotThrow(f.controller::run); for (int i = 0; i < 9; i++) { f.controller.run(); } - verify(f.world).spawn(any(Location.class), eq(Zombie.class), any()); + verify(f.world).spawn(any(Location.class), eq(Zombie.class), org.mockito.ArgumentMatchers.>any()); org.junit.jupiter.api.Assertions.assertEquals(0, f.entities.size()); f.controller.close(); } @@ -385,7 +385,7 @@ final class RoleArenaLifecycleTest { PlayerState.newPlayer(player.getUniqueId(), "Visitor"))); return states; }); - when(world.spawn(any(Location.class), eq(Zombie.class), any())).thenAnswer(call -> { + when(world.spawn(any(Location.class), eq(Zombie.class), org.mockito.ArgumentMatchers.>any())).thenAnswer(call -> { Zombie boss = newZombie(); spawned.add(boss); java.util.function.Consumer initializer = call.getArgument(2); diff --git a/src/test/java/games/dmg/spigottyrant/RoleControlInventoryMovementTest.java b/src/test/java/games/dmg/spigottyrant/RoleControlInventoryMovementTest.java index 065ed90..7fba60d 100644 --- a/src/test/java/games/dmg/spigottyrant/RoleControlInventoryMovementTest.java +++ b/src/test/java/games/dmg/spigottyrant/RoleControlInventoryMovementTest.java @@ -225,6 +225,7 @@ class RoleControlInventoryMovementTest { when(view.getCursor()).thenAnswer(call -> cursor.get()); Plugin plugin = mock(Plugin.class); when(plugin.getName()).thenReturn("SpigotTyrant"); + when(plugin.namespace()).thenReturn("spigottyrant"); var items = new BukkitRoleControlItemService(plugin, PluginSettings.from(Map.of())); var manager = mock(TyrantStateManager.class); when(manager.game()).thenReturn(new GameState(GameLifecycle.RUNNING,