Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
0be96c4
chore: ignore local worktrees
Jakubk15 Jul 20, 2026
6f01fb6
docs: design ParcelLockers API module
Jakubk15 Jul 20, 2026
be98f38
docs: plan ParcelLockers API module implementation
Jakubk15 Jul 20, 2026
7ecefcb
build: split API and plugin modules
Jakubk15 Jul 20, 2026
6bea75b
feat: extract parcel and locker API contracts
Jakubk15 Jul 20, 2026
9841eb0
feat: expose locker service implementation
Jakubk15 Jul 20, 2026
e60fa90
feat: expose ParcelLockers API provider
Jakubk15 Jul 23, 2026
57c52e1
build: publish ParcelLockers API artifact
Jakubk15 Jul 23, 2026
d2b29a2
fix: route public sends through dispatch workflow
Jakubk15 Jul 24, 2026
56b3d15
fix: compensate failed parcel dispatch setup
Jakubk15 Jul 24, 2026
1c0f319
fix: reserve sender storage during dispatch
Jakubk15 Jul 24, 2026
11457bd
fix: enforce item storage consistency protocol
Jakubk15 Jul 24, 2026
5d6ddc9
fix: serialize item storage owner operations
Jakubk15 Jul 24, 2026
110bd8e
style: remove trailing whitespace
Jakubk15 Jul 24, 2026
d27dbf4
fix: enforce public parcel operation contracts
Jakubk15 Jul 24, 2026
d8356fe
fix: enforce parcel operation integrity
Jakubk15 Jul 24, 2026
b2588c2
fix: serialize parcel lifecycle mutations
Jakubk15 Jul 24, 2026
7d14ed1
fix: harden parcel operation coordination
Jakubk15 Jul 24, 2026
820eea9
fix: preserve admin parcel lifecycle integrity
Jakubk15 Jul 28, 2026
7143301
Merge origin/master into codex/issue-176-api-module
Jakubk15 Jul 28, 2026
c8383a5
codestyle changes
Jakubk15 Jul 30, 2026
212e5c1
properly annotate fields
Jakubk15 Jul 30, 2026
ab8ee53
Merge branch 'master' into codex/issue-176-api-module
Jakubk15 Aug 15, 2026
76c0fec
remove unnecessary whitespace
Jakubk15 Aug 15, 2026
7713d85
Delete parcellockers-api/src/test/java/com/eternalcode/parcellockers/…
Jakubk15 Aug 15, 2026
0ae9b5e
Update AGENTS.md
Jakubk15 Aug 15, 2026
9eee03d
Merge branch 'codex/issue-176-api-module' of https://github.com/Etern…
Jakubk15 Aug 15, 2026
b659a48
dependency: Update Gradle to v9.7.1 (#243)
renovate[bot] Aug 24, 2026
28f933e
dependency: Update junit-framework monorepo to v6.1.3 (#244)
renovate[bot] Aug 24, 2026
b521b65
dependency: Update dependency com.discord4j:discord4j-core to v3.3.3 …
renovate[bot] Aug 24, 2026
be5a1b4
dependency: Update plugin xyz.jpenilla.run-paper to v3.1.0 (#245)
renovate[bot] Aug 24, 2026
8fdd624
Merge origin/master into codex/issue-176-api-module
Jakubk15 Sep 25, 2026
8743e65
test: share a single MySQL container across integration tests
Jakubk15 Sep 25, 2026
0bc6ea9
refactor: address review feedback on API module
Jakubk15 Sep 25, 2026
c015e13
fix: store locker position as VARCHAR so MySQL can index it
Jakubk15 Sep 25, 2026
bcee504
fix: keep the original cause when parcel content persistence fails
Jakubk15 Sep 25, 2026
2b9345c
test: fix flaky and server-dependent repository integration tests
Jakubk15 Sep 25, 2026
07bd48e
fix: address review findings on parcel send path and API validation
Jakubk15 Sep 25, 2026
14059a4
docs: update AGENTS.md to match current architecture
Jakubk15 Sep 25, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ build/
.codex/
.gemini/
graphify-out/
.worktrees/

.DS_Store
[Dd]esktop.ini
Expand Down
34 changes: 25 additions & 9 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
# AGENTS.md

This file provides guidance to Codex (Codex.ai/code) when working with code in this repository.
This file provides guidance to AI agents when working with code in this repository.

## Philosophy and Code Style

- You can communicate all of your views and report submit objections to the user's way of thinking.
- Use 'this' wherever possible (this.field, this.method(), etc.).
- Use SOLID, KISS, Clean Code principles, DRY (Don't Repeat Yourself), no copy-paste coding, Defensive copying for safety
- Prefer meaningful variable names
- Use fail-fast approach, validate early, throw meaningful exceptions
- Create immutable objects where possible, use builder pattern for complex objects, no magic numbers/strings - prefer constants
- If asked by user for a simple task/edit - do not invoke unnecessary skills such as TDD or planning.

## Build & Run Commands

Expand All @@ -15,39 +25,45 @@ This file provides guidance to Codex (Codex.ai/code) when working with code in t
./gradlew test --tests "com.eternalcode.parcellockers.database.ParcelRepositoryIntegrationTest"

# Start a local Paper server with the plugin loaded (downloads Paper + dependencies automatically)
./gradlew runServer
./gradlew runServer --console=plain
```

Requires JDK 21+. The `runServer` task uses an Adoptium Java 25 toolchain and auto-downloads LuckPerms, VaultUnlocked, and EssentialsX. To train its AOT cache, comment out `-XX:AOTCache=server.aot`, uncomment `-XX:AOTCacheOutput=server.aot`, run the server with a representative workload, and stop it cleanly. Restore the original comments for subsequent runs. The cache is stored as `run/server.aot` and includes all classes loaded during training, including ParcelLockers. Uncomment the DiscordSRV line in `build.gradle.kts` to test that integration locally.
Requires JDK 21+. The `runServer` task uses a Java 25 toolchain and auto-downloads LuckPerms, VaultUnlocked, and EssentialsX. Uncomment the DiscordSRV line in `build.gradle.kts` to test that integration locally.

## Architecture Overview

ParcelLockers is a Paper plugin (Minecraft 1.21) that lets players transfer items between parcel locker blocks across the world, with optional Discord notifications.
ParcelLockers is a Paper plugin (Minecraft 1.21+) that lets players transfer items between parcel locker blocks across the world, with optional Discord notifications.

### Entry Point & Wiring

`ParcelLockers.java` (`onEnable`) is the manual DI root — all components are instantiated and wired there in order: config → database → repositories → managers/services → GUIs → commands → event controllers. There is no DI framework; dependencies are passed via constructors.
The project consists of two Gradle modules:

- `parcellockers-api` — the public API other plugins' developers can hook into: `ParcelLockersApi` (obtained via `ParcelLockersProvider`), `LockerService`, `ParcelService`, the API models (`Locker`, `Parcel`, `Position`, `Page`/`PageResult`), cancellable events (`LockerCreateEvent`, `LockerDeleteEvent`, `ParcelSendEvent`, `ParcelDeliverEvent`, `ParcelCollectEvent`, `ParcelReturnEvent`) and exceptions (`ParcelLockersException`, `ValidationException`, `ParcelOperationException`).
- `parcellockers-plugin` — the plugin itself and the API implementation (e.g. `PublicParcelService` implements `ParcelService`).

`ParcelLockers.java` (`onEnable`) is the manual DI root — all components are instantiated and wired there in order: config → `NoticeService` → Vault economy lookup (the plugin disables itself if none is found) → database → repositories → managers/services → GUIs → commands → event controllers → `ParcelLockersProvider.initialize` (reverted in `onDisable`). There is no DI framework; dependencies are passed via constructors.

### Domain Layers

Each domain (`locker`, `parcel`, `content`, `delivery`, `itemstorage`, `user`, `discord`) follows a consistent layered structure:
Each domain (`locker`, `parcel`, `content`, `delivery`, `itemstorage`, `returns`, `user`, `discord`) follows a consistent layered structure:

- **Model** — plain record/class (e.g. `Locker`, `Parcel`, `User`)
- **Repository** — interface + `*OrmLite` implementation backed by H2/PostgreSQL via ORMLite
- **Repository** — interface + `*OrmLite` implementation backed by ORMLite (any supported SQL database)
- **Manager/Service** — business logic, coordinates repository calls; repositories return `CompletableFuture<T>` for all async DB operations
- **Controller** — Bukkit `Listener` handling in-game events (block place/break/interact, player join/quit)

### Key Components

| Component | Purpose |
|---|---|
| `DatabaseManager` | Manages HikariCP connection pool; supports H2 (default, embedded) and PostgreSQL |
| `DatabaseManager` | Manages HikariCP connection pool; supports SQLite (default), H2, MySQL, MariaDB and PostgreSQL (`DatabaseType`) |
| `ConfigService` + okaeri-configs | Loads `config.yml` and `messages.yml` via YAML; `PluginConfig` and `MessageConfig` are the config POJOs |
| `NoticeService` + multification | Sends MiniMessage-formatted notices to players; all user-facing text goes through `MessageConfig` |
| `ParcelDispatchService` | Orchestrates sending a parcel: validates, charges economy (Vault), schedules `ParcelSendTask` |
| `ParcelSendTask` | Runs async after a configurable delay; marks parcel DELIVERED and fires the deliver notification event |
| `GuiManager` + triumph-gui | Factory for all inventory GUIs; `LockerGui` and `MainGui` are the two root GUI entry points |
| `DiscordProviderPicker` | Selects between Discord4J (standalone bot) and DiscordSRV (delegation) at startup based on detected plugins |
| `ParcelReturnService` | Handles returning collected parcels within a configurable window; `ParcelReturnValidator` checks the returned items, `ReturnWindowPurgeTask` purges expired entries |
| `LockerPlaceController` | Uses Paper's Dialog API (unstable) to prompt for a locker description when a player places the locker item |

### Optional Integrations
Expand All @@ -57,4 +73,4 @@ Each domain (`locker`, `parcel`, `content`, `delivery`, `itemstorage`, `user`, `

### Testing

Tests live in `src/test/java/`. Integration tests (e.g. `LockerRepositoryIntegrationTest`) extend `IntegrationTestSpec` and use Testcontainers (MySQL) to test repository implementations against a real database. `ParcelPageTest` is a unit test with no container dependency.
Tests live in `parcellockers-plugin/src/test/java/` and `parcellockers-api/src/test/java/`. Repository integration tests (e.g. `LockerRepositoryIntegrationTest`) extend `MySqlIntegrationTestSpec`, which shares a single Testcontainers MySQL container across test classes and resets the schema before each test; `IntegrationTestSpec` only provides the `await()` helper for `CompletableFuture`s. `ParcelPageTest` is a unit test with no container dependency.
71 changes: 0 additions & 71 deletions CLAUDE.md

This file was deleted.

1 change: 1 addition & 0 deletions CLAUDE.md
213 changes: 4 additions & 209 deletions build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,211 +1,6 @@
import net.minecrell.pluginyml.paper.PaperPluginDescription
import xyz.jpenilla.runtask.task.AbstractRun

plugins {
id("java")
id("de.eldoria.plugin-yml.paper") version "0.9.0"
id("xyz.jpenilla.run-paper") version "3.1.0"
id("com.gradleup.shadow") version "9.6.1"
id("com.modrinth.minotaur") version "2.+"
}

group = "com.eternalcode"
version = "0.5.1-BETA"

repositories {
maven("https://maven-central.storage-download.googleapis.com/maven2/") // maven central mirror

maven("https://repo.triumphteam.dev/snapshots/")
maven("https://jitpack.io")
maven("https://repo.papermc.io/repository/maven-public/")
maven("https://repo.eternalcode.pl/releases")
maven("https://storehouse.okaeri.eu/repository/maven-public/")
maven("https://nexus.scarsz.me/content/groups/public/") // DiscordSRV
}

dependencies {
// minecraft development api
compileOnly("io.papermc.paper:paper-api:${Versions.PAPER_API}")
paperLibrary("dev.rollczi:litecommands-bukkit:${Versions.LITECOMMANDS}")
paperLibrary("dev.rollczi:litecommands-adventure:${Versions.LITECOMMANDS}")

// gui
paperLibrary("dev.triumphteam:triumph-gui-paper:${Versions.TRIUMPH_GUI}")

// configs
paperLibrary("eu.okaeri:okaeri-configs-serdes-commons:${Versions.OKAERI_CONFIGS}")
paperLibrary("eu.okaeri:okaeri-configs-serdes-bukkit:${Versions.OKAERI_CONFIGS}")
paperLibrary("eu.okaeri:okaeri-configs-yaml-bukkit:${Versions.OKAERI_CONFIGS}")

// gitcheck
paperLibrary("com.eternalcode:gitcheck:${Versions.GITCHECK}")

// metrics
implementation("org.bstats:bstats-bukkit:${Versions.BSTATS}")

// database
paperLibrary("com.zaxxer:HikariCP:${Versions.HIKARICP}")
paperLibrary("com.j256.ormlite:ormlite-jdbc:${Versions.ORMLITE}")
paperLibrary("com.h2database:h2:${Versions.H2}")
paperLibrary("org.postgresql:postgresql:${Versions.POSTGRESQL}")

// lombok
compileOnly("org.projectlombok:lombok:${Versions.LOMBOK}")
annotationProcessor("org.projectlombok:lombok:${Versions.LOMBOK}")

// jetbrains annotations
compileOnly("org.jetbrains:annotations:${Versions.JETBRAINS_ANNOTATIONS}")

// jackson-bukkit
paperLibrary("de.eldoria.jacksonbukkit:paper:${Versions.JACKSON_BUKKIT}")

// completable-futures
paperLibrary("com.spotify:completable-futures:${Versions.COMPLETABLE_FUTURES}")

// eternalcode commons
paperLibrary("com.eternalcode:eternalcode-commons-adventure:${Versions.ETERNALCODE_COMMONS}")
paperLibrary("com.eternalcode:eternalcode-commons-bukkit:${Versions.ETERNALCODE_COMMONS}")
paperLibrary("com.eternalcode:eternalcode-commons-shared:${Versions.ETERNALCODE_COMMONS}")

// multification
paperLibrary("com.eternalcode:multification-bukkit:${Versions.MULTIFICATION}")
paperLibrary("com.eternalcode:multification-okaeri:${Versions.MULTIFICATION}")

// caffeine
paperLibrary("com.github.ben-manes.caffeine:caffeine:${Versions.CAFFEINE}")

// vault
compileOnly("com.github.MilkBowl:VaultAPI:${Versions.VAULT_API}")

// discord integration library
paperLibrary("com.discord4j:discord4j-core:${Versions.DISCORD4J}")

// discordsrv (optional integration)
compileOnly("com.discordsrv:discordsrv:${Versions.DISCORDSRV}")

testImplementation("org.junit.jupiter:junit-jupiter-api:${Versions.JUNIT}")
testImplementation("org.junit.jupiter:junit-jupiter-params:${Versions.JUNIT}")
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:${Versions.JUNIT}")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")

testImplementation("org.mockito:mockito-core:${Versions.MOCKITO}")

testImplementation("org.testcontainers:junit-jupiter:${Versions.TESTCONTAINERS}")
testImplementation("org.testcontainers:mysql:${Versions.TESTCONTAINERS}")
testImplementation("com.zaxxer:HikariCP:${Versions.HIKARICP}")
testImplementation("com.j256.ormlite:ormlite-jdbc:${Versions.ORMLITE}")
testImplementation("com.h2database:h2:${Versions.H2}")
testImplementation("mysql:mysql-connector-java:${Versions.MYSQL_CONNECTOR}")
testImplementation("de.eldoria.jacksonbukkit:paper:${Versions.JACKSON_BUKKIT}")
testImplementation("com.eternalcode:eternalcode-commons-adventure:${Versions.ETERNALCODE_COMMONS}")
testImplementation("com.eternalcode:eternalcode-commons-bukkit:${Versions.ETERNALCODE_COMMONS}")
testImplementation("com.eternalcode:eternalcode-commons-shared:${Versions.ETERNALCODE_COMMONS}")
testImplementation("com.eternalcode:multification-bukkit:${Versions.MULTIFICATION}")
testImplementation("com.github.MilkBowl:VaultAPI:${Versions.VAULT_API}")
testImplementation("dev.triumphteam:triumph-gui-paper:${Versions.TRIUMPH_GUI}")

testImplementation("io.papermc.paper:paper-api:${Versions.PAPER_API}")
testImplementation("eu.okaeri:okaeri-configs-yaml-bukkit:${Versions.OKAERI_CONFIGS}")
}

java {
toolchain.languageVersion.set(JavaLanguageVersion.of(21))
}

paper {
name = "ParcelLockers"
main = "com.eternalcode.parcellockers.ParcelLockers"
version = project.version.toString()
apiVersion = "1.21"
author = "EternalCodeTeam"
website = "https://github.com/EternalCodeTeam/ParcelLockers"
loader = "com.eternalcode.parcellockers.ParcelLockersLibraryLoader"
generateLibrariesJson = true
foliaSupported = false
serverDependencies {
register("Vault") {
required = true
load = PaperPluginDescription.RelativeLoadOrder.BEFORE
}
register("DiscordSRV") {
required = false
load = PaperPluginDescription.RelativeLoadOrder.BEFORE
}
}
}

tasks.withType<JavaCompile> {
options.encoding = "UTF-8"
options.isIncremental = true
options.compilerArgs.add("-parameters")
options.release = 21
}

tasks.withType<AbstractRun> {
javaLauncher = javaToolchains.launcherFor {
vendor = JvmVendorSpec.ADOPTIUM
languageVersion = JavaLanguageVersion.of(25)
}
}

modrinth {
token.set(providers.environmentVariable("MODRINTH_TOKEN"))
projectId.set("parcellockers")
versionNumber.set(project.version.toString())
versionType.set(getVersionType(project.version.toString()))
// changelog.set(providers.environmentVariable("MODRINTH_CHANGELOG"))
debugMode.set(providers.environmentVariable("MODRINTH_DEBUG").map(String::toBoolean).orElse(false))
uploadFile.set(tasks.shadowJar)
gameVersions.addAll("1.21.11", "26.1", "26.1.1", "26.1.2", "26.2")
loaders.addAll("bukkit", "paper", "purpur")
syncBodyFrom = rootProject.file("README.md").readText()
}

tasks {
runServer {
minecraftVersion("26.2")
downloadPlugins {
modrinth("luckperms", "v5.5.53-bukkit")
modrinth("vaultunlocked", "2.20.2")
modrinth("essentialsx", "2.22.0")
// modrinth("discordsrv", "1.30.4") // uncomment to test with DiscordSRV integration
}
jvmArgs(
"-Dcom.mojang.eula.agree=true",
"-Xlog:aot=info"
)
// jvmArgs("-XX:AOTCacheOutput=server.aot")
jvmArgs("-XX:AOTCache=server.aot")
}

test {
useJUnitPlatform()
}

shadowJar {
archiveFileName.set("ParcelLockers v${project.version}.jar")

exclude(
"org/intellij/lang/annotations/**",
"org/jetbrains/annotations/**",
"META-INF/**"
)

mergeServiceFiles()

val relocationPrefix = "com.eternalcode.parcellockers.libs"

listOf(
"org.bstats"
).forEach { relocate(it, "$relocationPrefix.$it") }
}
}

fun getVersionType(version: String): String {
return when {
version.contains("SNAPSHOT") -> "beta"
version.contains("alpha", true) -> "alpha"
version.contains("beta", true) -> "beta"
else -> "release"
}
id("de.eldoria.plugin-yml.paper") version "0.9.0" apply false
id("xyz.jpenilla.run-paper") version "3.1.0" apply false
id("com.gradleup.shadow") version "9.6.1" apply false
id("com.modrinth.minotaur") version "2.+" apply false
}
1 change: 1 addition & 0 deletions buildSrc/src/main/kotlin/Versions.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ object Versions {

const val LOMBOK = "1.18.46"
const val JETBRAINS_ANNOTATIONS = "26.1.0"
const val JSPECIFY = "1.0.0"

const val JACKSON_BUKKIT = "2.0.0"
const val COMPLETABLE_FUTURES = "0.3.6"
Expand Down
Loading
Loading