Custom Sources
Custom Sources
In Imperat, a command source represents the entity executing a command. Each platform ships a default — BukkitCommandSource on Bukkit, BungeeCommandSource on Bungee, etc.
In v4, you can declare ONE custom source class and have it flow as the canonical type S through the entire framework — argument types, suggestion providers, context resolvers, dummy senders, every internal site. This replaces the v3 model where S was always the platform source and the user's type only reached @Execute parameter injection.
Custom source vs source provider — quick clarifier
This chapter covers two related-but-different concepts. They're easy to mix up at first glance, so worth pinning down before we dive in:
-
A custom source changes what your source is. You replace the platform default (e.g.
BukkitCommandSource) with your own subclass, and the whole framework now treats that as the canonical source type. Big architectural decision — everyArgumentType, every@Executemethod, every suggester sees your type. One custom source perImperatinstance. -
A source provider is a small adapter that says "given the live source, here's how to produce this other type." You can register many of them, one per parameter type you care about. They run when someone puts that type in an
@Executemethod signature.
Analogy: the custom source is the kind of car you're driving. Source providers are the views you can pull off it — speedometer, fuel gauge, GPS — each derived from the same car, each answering a different question.
You can:
- Use neither — default platform source + default origin-based resolution. Works for most plugins.
- Use only a custom source — your domain object flows everywhere, no per-type overrides.
- Use only source providers — keep the platform source, but customise how specific parameter types (
Player,World, your own domain types) are derived from it. - Use both — your custom source carries domain state, and providers project it into typed views for your
@Executemethods.
The two are independent. The rest of this chapter covers the custom-source path first (sections "Step 1" through "How it works"), then the source-provider path further down.
When you need this
Most plugins don't. The default path requires zero configuration beyond a one-liner type-witness:
BukkitImperat<BukkitCommandSource> imperat = BukkitImperat.builder(plugin).build();
Reach for a custom source when you want domain-specific data attached to every command source — locale, audit trail, cached permissions, transient state — and want it visible to your ArgumentTypes, suggesters, and context resolvers without going through the source object.
Step 1 — Declare your source class
Your class must extend the platform's source class (BukkitCommandSource, BungeeCommandSource, VelocityCommandSource, …). The <S extends P> bound is enforced at the framework level — it lets internals call platform methods on your S directly via inheritance, no unwrap needed per call site.
public final class MyCustomSource extends BukkitCommandSource {
private final String locale;
public MyCustomSource(BukkitCommandSource platform, String locale) {
super(platform.origin(), platform.adventureProvider());
this.locale = locale;
}
public String locale() {
return locale;
}
public void greet() {
reply("Hello, " + name() + "!");
}
}
Alternative: if the platform source is awkward to extend (final class, complex constructor, etc.), extend the DelegatingCommandSource<P> helper which forwards every CommandSource method to a held platform instance.
public final class MyCustomSource extends DelegatingCommandSource<BukkitCommandSource> {
private final String locale;
public MyCustomSource(BukkitCommandSource platform, String locale) {
super(platform);
this.locale = locale;
}
public String locale() { return locale; }
}
Step 2 — Build with the source class + mapper
The custom-source builder takes three arguments: the plugin instance, the source class token, and the CommandSourceMapper that lifts platform sources to your custom type. There's no .source(...) chainable method — both pieces are supplied at the same site.
BukkitImperat<MyCustomSource> imperat = BukkitImperat.builder(
plugin,
MyCustomSource.class,
CommandSourceMapper.wrapping(bukkit -> new MyCustomSource(bukkit, "en_US"))
)
.build();
CommandSourceMapper.wrapping(...)takes only theP → Slift function. The reverse direction (S → P) inherits the default identity cast on the interface — valid because the framework enforcesS extends P. UseCommandSourceMapper.of(wrap, unwrap)when your unwrap needs a non-trivial body (e.g. a wrapping subclass that surfaces a different platform instance than the one held).
The same shape replicates per platform:
// Bungee
BungeeImperat<MyProxySource> imperat = BungeeImperat.builder(plugin, MyProxySource.class, mapper).build();
// Velocity
VelocityImperat<MyPlugin, MyProxySource> imperat =
VelocityImperat.builder(plugin, proxyServer, MyProxySource.class, mapper).build();
// Minestom
MinestomImperat<MyCustomSource> imperat = MinestomImperat.builder(serverProcess, MyCustomSource.class, mapper).build();
// JDA
JdaImperat<MyDiscordSource> imperat = JdaImperat.builder(jda, MyDiscordSource.class, mapper).build();
// CLI
CommandLineImperat<MyConsoleSource> imperat =
CommandLineImperat.builder(System.in, MyConsoleSource.class, mapper).build();
// Hytale
HytaleImperat<MyCustomSource> imperat = HytaleImperat.builder(plugin, MyCustomSource.class, mapper).build();
Step 3 — Use it everywhere
Once the framework is parameterized over your S, your custom source flows through every site:
@RootCommand("greet")
public final class GreetCommand {
@Execute
public void greet(MyCustomSource source) {
source.greet();
source.reply("Your locale: " + source.locale());
}
}
@Execute parameters typed as your custom class get the live instance. Same goes for ArgumentType<MyCustomSource, T>, SuggestionProvider<MyCustomSource>, ContextArgumentProvider<MyCustomSource, T> — the framework treats MyCustomSource as canonical at compile time.
How it works
The mapper is consulted at exactly one seam: the active backend's wrapSender(Object). The platform-native source is built as it always was (Adventure-aware, stack-aware, etc.), then mapper.wrap(p) lifts it to S. Same hop in createDummySender() for tree-only phases. After the lift, S is the only type the framework sees — there's no per-call-site cast tax, no runtime reflection on hot paths, and no boilerplate at user code sites.
The default path uses CommandSourceMapper.identity() — a singleton no-op that returns its input. Zero runtime cost when you don't need a custom source.
Cross-source-type method parameters
Want Player (Bukkit) or ProxiedPlayer (Bungee) directly as a method param instead of going through source.asPlayer()?
@Execute
public void cmd(Player player, String message) {
player.sendMessage("hi " + message);
}
The framework registers gating-aware ContextArgumentProviders for these by default — Player only when not console, ConsoleCommandSender only when console, etc. Throws ResponseException on the wrong sender type.
For domain-specific types you want injected at the same slot, register your own:
config.registerContextArgumentProvider(MyDomainContext.class, (ctx, paramElement) -> {
MyCustomSource source = ctx.source();
return MyDomainContext.derive(source);
});
Source providers — per-type customisation
A source provider is the answer to "I want Player (or World, or MyDomainView) directly in my @Execute method, but I want to control exactly how it gets derived from the source."
The framework already does sensible things by default: putting Player in your method signature gives you the live player when the executor isn't console; ConsoleCommandSender works the other way around. Those defaults cover the standard Bukkit/Bungee derived-types out of the box.
A source provider is for the other 20% — when you want to override one of those defaults, or when you have your own type that doesn't fit the default path. You hand the framework a small lambda: "given my source, produce a Foo." From then on, @Execute void cmd(Foo foo) works.
BukkitImperat<MyCustomSource> imperat = BukkitImperat.builder(
plugin,
MyCustomSource.class,
CommandSourceMapper.wrapping(bukkit -> new MyCustomSource(bukkit, "en_US"))
)
// Override the default `Player` resolution — pull from a session cache instead
.sourceProvider(Player.class, src -> sessionCache.activePlayer(src.uuid()))
// Project your custom source into a domain view
.sourceProvider(LocaleScope.class, src -> new LocaleScope(src.locale()))
.build();
Now @Execute void cmd(LocaleScope scope) Just Works — the framework calls your lambda with the live MyCustomSource, hands the result to your method.
How the resolution actually flows
When the framework hits a parameter on an @Execute method, it walks this chain to figure out what to inject:
- Is it the source itself? If the parameter type matches
S(or one ofS's supertypes up toCommandSource), it just hands over the live source. No allocation, no lambda. - Did you register a source provider? If yes, it calls your lambda with the source, takes the return value.
- Does it match what's inside the source? Sources hold a platform-native sender via
origin()—PlayerorConsoleCommandSenderon Bukkit,ProxiedPlayeron Bungee, etc. If the parameter type matches that, you get it back. - Did you register a context-argument provider? Last fallback — for richer derived types that need the full execution context.
- Otherwise — throws. The type is unreachable from this source.
Step 2 is your override slot. Step 3 is the default for the obvious cases. So registering a provider for Player overrides the default; not registering anything for Player falls through to step 3 and you still get the player.
Tip: Returning
nullfrom a source provider is fine — resolution continues to step 3. Useful for conditional overrides ("for these UUIDs, use the cache; for everyone else, fall through to the default").
Source provider vs context-argument provider — when to pick which
Both can produce a typed value from the source. The difference is what they receive:
| SPI | Receives | Pick this when |
|---|---|---|
SourceProvider<S, R> |
the source S only |
the value is purely a transformation of the source — name, UUID, world, locale, a wrapping struct |
ContextArgumentProvider<S, R> |
the full ExecutionContext<S> |
the value also needs parsed arguments, resolved flags, or anything else from the live execution pipeline |
If you only need the source, prefer SourceProvider — leaner signature, runs earlier in the chain, no execution-context plumbing. If you need to peek at parsed args mid-resolution, ContextArgumentProvider is your tool.
If you register both for the same type, the source provider wins (step 2 beats step 4).
Migration from v3
Plugins not using a custom source — minimal change, one line per declaration:
- private BukkitImperat imperat;
+ private BukkitImperat<BukkitCommandSource> imperat;
Existing imperat.registerCommand(...) and ArgumentType<BukkitCommandSource, T> registrations compile unchanged.
Plugins using v3's SourceProvider for cross-type injection (Player, ConsoleCommandSender, etc.) — those standard defaults are now built into each platform's builder via ContextArgumentProvider, so no migration needed for the standard cases. The v4 SourceProvider SPI is still available as a per-type override layer (see the Source providers section above) — but its signature is leaner: provide(S source) instead of v3's provide(ExecutionContext, Type). If your v3 provider only read state from the source itself, port it directly. If it also touched the execution context, port it as a ContextArgumentProvider<S, R> instead — same body, less indirection.
Plugins using v3 with their own custom source through sourceProvider(...) for the source itself — adopt the new builder shape:
- BukkitImperat imperat = BukkitImperat.builder(plugin)
- .sourceProvider(MyCustomSource.class, (src, ctx) -> new MyCustomSource(src))
- .build();
+ BukkitImperat<MyCustomSource> imperat = BukkitImperat.builder(
+ plugin,
+ MyCustomSource.class,
+ CommandSourceMapper.wrapping(bukkit -> new MyCustomSource(bukkit))
+ )
+ .build();
Now your custom source is canonical EVERYWHERE — not just at @Execute method params.