DataRegistry is HauntedMC's shared read/write boundary for canonical player identity and DataRegistry-owned player metadata on Velocity and Paper.
It owns player creation, username updates, active identity state, connection metadata, language and nickname
preferences, playtime summaries, and name history. Feature plugins own their own tables and should reference
players by the stable scalar playerId.
- Velocity is the authoritative writer for joins, switches, disconnects, sessions, connection info, and probes.
- Paper prepares backend identity state and exposes the same read APIs to Paper features.
- DataProvider supplies database connections and ORM bootstrap.
- Production schemas must be migration-managed. Do not rely on Hibernate schema mutation in production.
- On Velocity startup, stale player presence from an unclean shutdown is reconciled before periodic flushing starts. Open sessions, visits, playtime segments, and online flags are closed from the last durable activity timestamp instead of from startup time.
- On Velocity shutdown, queued player lifecycle writes are drained before active players are persisted offline.
- Java 25
- Maven 3.8.6+
- Docker, for the container-backed and platform-acceptance suites
- DataProvider
3.1.0 - Velocity
4.1.0-SNAPSHOTand/or Paper26.2.build.65-beta
Configure both the shell JAVA_HOME and the IDE Maven runner/importer to Java 25. The build deliberately rejects
Java 26 or newer until the bundled DataProvider/Hibernate stack is qualified for it.
Start the server once to generate plugins/DataRegistry/config.yml, then review the database, feature,
privacy, playtime, service-registry, and platform sections.
Defaults and comments live in dataregistry-core/src/main/resources/config.yml. Missing supported keys are restored on load and stale keys are removed.
Depend only on dataregistry-api as provided (replace the version with the release you target):
<dependency>
<groupId>nl.hauntedmc.dataregistry</groupId>
<artifactId>dataregistry-api</artifactId>
<version>1.11.5</version>
<scope>provided</scope>
</dependency>Use DataRegistryApi#players() for player data:
DataRegistryApiProvider apiProvider = /* platform plugin instance */;
PlayerData players = apiProvider.getDataRegistry().players();
UUID uuid = player.getUniqueId(); // snapshot platform state before async continuations
players.whenReady(uuid).thenAccept(identity -> {
identity.ifPresent(value -> {
long playerId = value.playerId();
UUID canonicalUuid = value.uuid();
String username = value.username();
});
});dataregistry-apiis the only dependency for ProxyFeatures, ServerFeatures, and other consumers. It has no DataProvider, Hibernate/Jakarta Persistence, Velocity, or Paper dependency.dataregistry-coreowns entities, repositories, ORM wiring, lifecycle writers, recovery, and query execution. It is an implementation dependency of the platform modules, never a feature dependency.dataregistry-platform-velocityowns authoritative proxy lifecycle listeners, includingPlayerStatusListener;dataregistry-platform-paperprovides the Paper identity bridge.dataregistry-migrationscontains ordered schema migration resources; the default core schema mode isvalidate.dataregistry-testkitsuppliesFakeDataRegistryApi, immutable player fixtures, temporary IDs, and async failure simulation for feature contract tests.
DataRegistryApiProvider#getDataRegistry() returns DataRegistryApi, not the core runtime. Platform plugins
implement that provider capability; consumers can depend on dataregistry-api alone. There is deliberately no
public path from that type to an ORM context, entity, repository, lifecycle writer, or DataProvider handle.
Feature maintainers migrating from the former monolithic artifact should follow
DOWNSTREAM_MIGRATION.md. A dependency-coordinate change alone is not valid for a feature
that currently maps PlayerEntity in its own ORM model.
Use whenReady(uuid) in join paths. It completes when DataRegistry has finished the authoritative lifecycle
initialization for that player, including creation or username update if needed.
Use lookup-only methods outside lifecycle paths:
players.findIdentity(uuid),players.findIdentityByUsername(name), andplayers.findIdentity(playerId)players.findIdentityByIdentifier(identifier)for command input that may be a UUID or usernameplayers.findPlayerId(uuid)andplayers.findPlayerIdByIdentifier(identifier)players.findIdentities(lookups)for bulk identity resolutionplayers.findIdentitiesByUsernamePrefix(prefix, pageRequest)for cursor-based suggestions and staff toolingplayers.findActiveIdentityCached(uuid)only when cache-only behavior is explicitly acceptable
PlayerIdentity is immutable and standalone. It is safe to pass between feature layers and does not expose
Hibernate-managed state.
Use PlayerProfile when a feature needs a read snapshot of several DataRegistry-owned fields:
players.findProfileByIdentifier(input, 20).thenAccept(profileOpt -> profileOpt.ifPresent(profile -> {
PlayerIdentity identity = profile.identity();
Optional<String> nickname = profile.nickname();
List<PlayerNameHistoryEntry> names = profile.nameHistory();
}));Profiles may include language, nickname, connection, online, activity, playtime, and name-history data depending
on enabled modules and available rows. Missing optional feature data is represented as Optional.empty() or an
empty list. Profile projection is assembled by DataRegistry in one transaction for a consistent snapshot.
Use the specific facade methods when a full profile is unnecessary:
players.findLanguage(playerId)andplayers.saveLanguage(playerId, preference, effective)players.findNickname(playerId)andplayers.saveNickname(playerId, nickname)players.findConnection(playerId)players.findOnlinePlayers(limit)players.findActivity(playerId)players.findPlaytime(playerId)and leaderboard helpersplayers.findNameHistory(playerId, limit)players.findIdentitiesSharingLastIp(playerId)andplayers.findUsernamesSharingLastIp(playerId)players.findPlayerIdsByLastIpAddress(ip, excludePlayerId)andplayers.findUsernamesByLastIpAddress(ip, excludePlayerId)
Public persistence reads and DataRegistry-owned preference writes return CompletionStage and run on DataRegistry's
query executor with configured deadlines. Returned futures support cancellation when used as CompletableFuture.
Development thread checks warn when likely event threads request queries or block pending query stages. Completion
callbacks may run on DataRegistry worker or lifecycle threads, so snapshot Bukkit/Velocity state before starting async
work and schedule platform API work back onto the platform thread when required.
Downstream plugins must not create, update, or merge canonical player rows. They may write only through the narrow DataRegistry methods for DataRegistry-owned preferences such as language and nickname.
DataRegistry also exposes a process-local service catalog for feature-owned APIs. This lets enabled features share their own data and behavior without moving their tables into DataRegistry or forcing consumers to query another feature's ORM entities.
Feature plugins should publish narrow interfaces from their own lifecycle code:
FeatureServiceHandle handle = dataRegistry.featureServices().register(
"ServerFeatures",
"Vanish",
VanishAPI.class,
vanishService
);Consumers should resolve feature services by interface:
dataRegistry.featureServices()
.find(VanishAPI.class)
.ifPresent(vanish -> vanish.isVanished(playerId));Use find for optional integrations and require only when a feature cannot run without the dependency. Close the
returned FeatureServiceHandle during feature disable, or use the ServerFeatures/ProxyFeatures lifecycle API
manager, which publishes and unregisters services automatically.
The catalog is intentionally runtime-only. It does not provide cross-server RPC, cache persistence, or schema ownership. Exported interfaces should be stable, small, and expressed in scalar IDs or immutable value objects where possible.
Keep feature-owned data such as vanish, glow, nametags, friends, sanctions, client info, 2FA, voting, messaging,
and logs in the owning feature plugin. Do not move those records into DataRegistry. Prefer scalar player_id
references for new feature-owned tables and keep feature queries in the owning feature.
Feature-owned services are the supported sharing boundary for that data. For example, a messaging feature may ask
the Vanish feature whether a playerId is hidden, but it should not read or join the vanish table directly.
Authenticated GitHub Packages access may be required for private HauntedMC dependencies. Configure repository id
github in ~/.m2/settings.xml, then run:
# Fast reactor verification: unit tests, Checkstyle, coverage and dependency hygiene.
./mvnw -B -ntp verify
# Adds the MySQL integration suite. It applies the shipped migration and validates the
# production Hibernate mappings before exercising the public DataRegistry API.
./mvnw -B -ntp -Pintegration-tests verify
# Builds the bundled Paper and Velocity artifacts, then boots each in the real target
# platform with a consumer compiled only against dataregistry-api. This suite also runs
# the fast reactor checks; it does not include the MySQL integration suite.
./mvnw -B -ntp -Pplatform-acceptance verify
# Full local release gate: fast checks, MySQL integration, and Paper/Velocity acceptance.
./mvnw -B -ntp -Pintegration-tests,platform-acceptance verifyThe integration and platform suites need a reachable Docker daemon. The platform suite additionally needs curl,
jq, sha256sum, jar, and exactly Java 25. Java 26 is intentionally rejected because the currently supported
DataProvider/Hibernate runtime is qualified against Java 25. The platform suite downloads the configured Paper and
Velocity runtime builds, checks their SHA-256 values, provisions MySQL 8.4 from the migration, checks public API
reads and writes, reloads DataProvider configuration, and requires clean DataRegistry and Hikari shutdown. Set
PLATFORM_ACCEPTANCE_KEEP_WORK_DIRECTORY=true to retain server logs after a local run.
The tag release workflow runs both profiles against the exact tagged reactor before Maven deployment. This keeps the fast checks, migration/schema compatibility, and real bundled-plugin boot checks in the release gate.
Build output:
dataregistry-api/target/dataregistry-api-*.jardataregistry-core/target/dataregistry-core-*.jardataregistry-platform-velocity/target/dataregistry-platform-velocity-*-bundled.jardataregistry-platform-paper/target/dataregistry-platform-paper-*-bundled.jar
Deploy the bundled platform JAR only. It embeds the platform's relocated core implementation while retaining the
public DataRegistryApi namespace. Do not deploy dataregistry-core as a separate server plugin and do not add it
as a dependency to feature plugins.
This project is licensed under the GNU Affero General Public License v3.0.