This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
ModernVillagerShop — a Paper 1.21.8+ plugin that turns Villagers into player/admin-owned shops with SELL and BUY (order-slot) trade, a Dialog + chest hybrid UI, and SQLite/MySQL storage. Java 21 only. The authoritative spec is spec.md; AGENTS.md holds the human-facing contribution guide and has full overlap with this file — keep both honest if you change one.
./gradlew build— compile + run the SQLite test suite + produce the shaded plugin JAR (this is the pre-PR check)../gradlew shadowJar— only the fat JAR (relocates HikariCP underme.f0reach.vshop.lib.hikari)../gradlew runServer— boot a local Paper test server (currently pinned to 1.21.11 in build.gradle). The working tree at run/ is the server data dir../gradlew test --tests me.f0reach.vshop.storage.sqlite.SqliteShopRepositoryTest— single test class. Append.methodNamefor one method../gradlew clean— wipebuild/.
MySQL repository tests are gated on VSHOP_TEST_MYSQL_URL via @EnabledIfEnvironmentVariable and are silently skipped without it. Each JVM allocates a throwaway vshop_test_<uuid> schema, so the user needs CREATE/DROP plus ALL on vshop_test_%.*. Local recipe:
docker run --rm -p 3307:3306 -e MYSQL_ROOT_PASSWORD=rootpw \
-e MYSQL_USER=vshop -e MYSQL_PASSWORD=vshop mysql:8.4
# then grant: GRANT ALL ON `vshop_test_%`.* TO 'vshop'@'%'; GRANT CREATE,DROP ON *.* TO 'vshop'@'%';
VSHOP_TEST_MYSQL_URL=jdbc:mysql://127.0.0.1:3307 \
VSHOP_TEST_MYSQL_USER=vshop VSHOP_TEST_MYSQL_PASSWORD=vshop \
./gradlew testCI runs both backends — see .github/workflows/ci.yml.
ModernVillagerShopPlugin.java is a hand-wired composition root. It instantiates every service in dependency order inside onEnable(), registers listeners, registers the /vshop Brigadier command via LifecycleEvents.COMMANDS, and exposes each collaborator through a public getter. There is no DI framework. When you add a service, add it here and expose the getter — other code reaches it through the plugin instance.
config—PluginConfigwraps the YAMLFileConfiguration. Treat it as immutable;/vshop reloadbuilds a fresh instance.locale—MessageManagerloadslang/messages_<locale>.yml, parses MiniMessage, and is the only place that emits player-facing text.storage—StorageManagerowns theDataSourceProvider(Hikari) and exposes one repository per concern (shops(),slots(),inventory(),transactions(),notifications(),limits(),coOwners(),playerCache(),playerPreferences()). Repository implementations live understorage/sqliteandstorage/mysql; the SQL-agnostic schema bootstrap is instorage/repo/SchemaInitializer. Cross-backend data movement is instorage/migrate/MigrationService(invoked by/vshop migrate).economy—EconomyServiceis the only caller of Vault. Fee/share math is centralized here; never callEconomydirectly elsewhere.shop— domain.ShopRegistryis the in-memory authoritative map ofUUID -> Shop.ShopServiceis the lifecycle coordinator (create/load/delete, persistence + registry + villager state in lockstep).ShopVillagerManagerhandles the live entity (AI lock, invulnerability, respawn on chunk load, custom name regen). Subpackages mirror flows:trade(purchase/sell),edit(slot/menu editing),coowner(PRIMARY/MANAGER/STAFF),egg(spawn-egg crafting),listener(Bukkit events that fan into the services),cache(player-head/online cache).ui—ui/dialogis the BedrockDialog adapter (DialogService);ui/chestbuilds the inventory-based browse/edit/restock/player-picker UIs;ui/textrenders chat output (history, search, list).command—VShopCommandbuilds the Brigadier tree and delegates per-subcommand classes incommand/sub.integration—MvshopPlaceholdersis an optional PAPI expansion, registered only when both the plugin is present andplaceholderapi.enabledis true.api— public surface registered toServicesManager(ModernVillagerShopAPI);api/price/PriceRegistryis the extension point external plugins use to influence prices (read viashop.trade.PriceResolver).item,model— data carriers (item snapshots, enums, value objects).
- Persistence is repository-per-table. There is no ORM; each repository is hand-written SQL with a SQLite and a MySQL implementation. Schema differences are kept inside the backend-specific class, not branched in shared code.
- The Villager is the source of truth for "is there a shop here," but its position/customName are derived state regenerated from DB on load. Never mutate the entity directly — go through
ShopVillagerManagerorShopService. - Co-owner role (
PRIMARY/MANAGER/STAFF) gates what an owner can do in their own shop; themodernvillagershop.*permissions in paper-plugin.yml gate whether the command/feature is available at all.*.otherspermissions bypass role for moderation. - BedrockDialog callbacks may fire off the main thread. Anything that touches Bukkit API must be wrapped in
Bukkit.getScheduler().runTask(plugin, ...)— see existing flows inshop/trade/TradeFlowandshop/edit/SlotEditFlowfor the pattern. - BedrockDialog only ships
ConfirmDialog/NoticeDialog/MultiButtonDialog/InputDialogand has noonCloseon Bedrock — design flows around explicit cancel buttons, not close detection. Sliders are banned for amount/price (useInputDialog). - Localization: every user-visible string lives in
lang/messages_*.ymland bothmessages_en.ymlandmessages_ja.ymlmust stay in sync when keys are added. - Never render a domain enum with
name()in player-facing output.locale/EnumLabelsresolvesTradeSide/LimitScope/CoOwnerRole/ShopTypetoenum.<kebab-class>.<kebab-value>(e.g.enum.trade-side.sell); insert the result withPlaceholder.componentso the surrounding message keeps supplying the color, and keep the label itself plain text. Adding an enum constant means adding the key to both locale files —EnumLabelsTestfails otherwise.name()remains correct for the DB, YAML import/export, command arguments and Dialog dropdown option IDs.
- Stack: JUnit 5 + MockBukkit (
mockbukkit-v1.21). - Repository tests follow a contract + two thin subclasses pattern. The SQL-agnostic checks live in
me.f0reach.vshop.storage.<Repo>Contract(or directly understorage/repofor shared SQL pieces); each backend hasSqlite<Repo>Test/Mysql<Repo>Testthat picks the data source viatestsupport/AbstractRepositoryContract. Add new repository tests by extending the contract on both sides — don't write backend-only tests unless the behavior is backend-specific. - Tests using Bukkit types should extend or use
testsupport/BukkitTestSupportto manage MockBukkit lifecycle. - Test naming:
<ClassName>Test; method names describe behavior (createsShopWhenVillagerIsValid).
Versioning, CHANGELOG and publishing are automated — never bump a version or write a CHANGELOG entry by hand.
- Commits drive everything. release-please parses Conventional Commits on
mainand maintains a "chore: release x.y.z" PR that carries the CHANGELOG diff and the version bump. Merging that PR creates the tag, the GitHub release, and triggers publishing. Sections and hidden types are configured in release-please-config.json; the last released version is in .release-please-manifest.json. - The version lives in gradle.properties inside the
x-release-please-start-version/x-release-please-endblock. Gradle reads it automatically (there is noversion =in build.gradle),processResourcesexpands it intopaper-plugin.yml, and Minotaur reuses it as the Modrinth version number. The block-comment form matters: a trailing# x-release-please-versionwould be swallowed into the properties value. - .github/workflows/release.yml holds both the release-please job and the publish job. They are one workflow on purpose — a release created with
GITHUB_TOKENdoes not firerelease: published, so a separate publish workflow would never run without a PAT. - Modrinth upload is the
modrinthGradle task (com.modrinth.minotaur);modrinthSyncBodypushesREADME.mdas the project description, so links in it must stay absolute. Declared Minecraft versions come frommodrinth.gameVersionsingradle.propertiesand track BedrockDialog's supported list. Modrinth dependencies arebedrockdialog(required) andplaceholderapi(optional) — Vault has no Modrinth project and can only be mentioned in the body. - PR titles are linted (.github/workflows/pr-title.yml) because squash merges turn the title into the commit message release-please reads.
- Java 21, UTF-8, 4-space indent, no tabs.
- Packages lowercase under
me.f0reach.vshop.*; classesPascalCase; methods/fieldscamelCase; constantsUPPER_SNAKE_CASE. - Prefer small classes split by domain. New storage logic goes into the matching repository — do not add SQL to services.
The repo bundles vendored Markdown references that future Claude instances should consult before touching the corresponding subsystem (they reflect API decisions newer than common training data):
- spec.md — authoritative v1 spec. Read before any behavior change.
- dialog.md — PaperMC Dialog API + BedrockDialog wrapper notes.
- adventure.md — Adventure/MiniMessage usage patterns.
- modern-commands.md — Paper Brigadier command API.
- README.md is the English project page and doubles as the Modrinth description. Refresh it when commands, permissions, requirements or config defaults change.
- docs/guide/ holds the Japanese guides (
admin.md,user-basic.md,user-advanced.md) and docs/guide/en/ the English ones. Both languages must stay in sync — editing one side means editing the other, the same rule asmessages_en.yml/messages_ja.yml. - Guides quote UI button labels and error strings. Source that wording from the matching locale file (
lang/messages_ja.ymlfor the Japanese guides,lang/messages_en.ymlfor the English ones) instead of translating the other guide — the two locales are not literal translations of each other (e.g. the BOTH-slot picker is購入する/納品するin ja butBuy from shop/Sell to shopin en). - spec.md stays Japanese and remains authoritative. Update the spec first for behavior changes, then the guides.