Files
core-ranks/docs/05_Provider_Integration.md
T
2026-05-26 23:16:33 -04:00

4.4 KiB

Provider Integration

How we plug into Hytale's permission system without Hytale knowing we're there.

The SPI

package com.hypixel.hytale.server.core.permissions.provider;

public interface PermissionProvider {
    String getName();
    void addUserPermissions(UUID, Set<String>);
    void removeUserPermissions(UUID, Set<String>);
    Set<String> getUserPermissions(UUID);
    void addGroupPermissions(String, Set<String>);
    void removeGroupPermissions(String, Set<String>);
    Set<String> getGroupPermissions(String);
    void addUserToGroup(UUID, String);
    void removeUserFromGroup(UUID, String);
    Set<String> getGroupsForUser(UUID);
    void setUserGroup(UUID, String);
    String getGroupParent(String);
    Set<String> getAllRegisteredGroups();
    Set<String> getEffectiveGroupPermissions(String);
}

That's the contract. Our RanksPermissionProvider implements every method.

The module's API

PermissionsModule.get() exposes:

void addProvider(PermissionProvider);
void removeProvider(PermissionProvider);
List<PermissionProvider> getProviders();
PermissionProvider getFirstPermissionProvider();
boolean areProvidersTampered();

When a hasPermission(uuid, node) call comes in, the module consults the providers in order. If you've replaced the default with ours, only ours answers. If you've left ours alongside the default, the module's logic combines them (typically OR for "has the permission").

How we register

RanksPlugin.start():

PermissionsModule perms = PermissionsModule.get();

if (config.provider.replace_default) {
    for (PermissionProvider existing : new ArrayList<>(perms.getProviders())) {
        if (!RanksPermissionProvider.NAME.equals(existing.getName())) {
            perms.removeProvider(existing);
            removedProviders.add(existing);   // remember to restore on shutdown
        }
    }
}
perms.addProvider(provider);

On shutdown() we reverse: remove ours, re-add anything we removed. This is so a clean reload of our plugin doesn't leave the server permission-less.

What "replace" actually changes

When replace_default = true:

  • Hytale's file-backed provider (which stores groups/users in JSON on disk) is removed.
  • Its data is not copied into MySQL automatically. If you had pre-existing OPs from the file, they're gone the moment we register (until you reassign them via /op or /grant, which now writes to MySQL).
  • If you ever uninstall this plugin, the file-backed provider is restored - but your MySQL data stays in MySQL, so it won't be visible to that provider.

If you have existing OPs you need to preserve:

  1. Before installing Ranks, write down who's in which group from the Hytale file.
  2. Install Ranks, boot once - this creates the schema and seeds the built-in groups.
  3. Run /op Bob, /op Alice etc. once for each existing OP - this writes to our DB.

For larger migrations (lots of users, custom groups), a one-off SQL import is the right move; we don't ship one because every server's pre-existing state is different.

Hytale's /op, /perm commands

Look at the commands/ package under com.hypixel.hytale.server.core.permissions in the SDK. Each one calls into PermissionsModule which routes to providers. With our provider registered, those commands now read/write our MySQL.

This is the main reason "replace default" is the recommended mode: it means there's only one source of truth, and all the existing Hytale tooling keeps working.

Conflicts to watch

  • If you set areProvidersTampered() somehow gets flagged, Hytale might warn about non-default providers. We are intentionally replacing; the warning is expected.
  • If another plugin also registers a PermissionProvider, the load order matters. We register in start(). If a later-loading plugin also calls addProvider, we coexist as providers 1 and 2. The module decides who answers first.

Caching considerations

Every hasPermission call goes through our provider. We aggressively cache (see 06_Cross_Server_Caching.md). The first call after a cache miss hits MySQL; subsequent calls are in-memory.

If the cache gets stale (e.g. the bus drops an invalidation message), permissions can drift for up to whatever interval the player stays connected. To force-refresh: revoke and re-grant their rank (which invalidates), or restart the server.