All hooks follow fail-open behavior:
- No hook registered → action is allowed
- Hook throws exception → action is allowed (with sampled warning log via FaultReporter)
- Hook returns 0 or negative → action is allowed
This ensures that if the protection mod crashes or hasn't loaded yet, players can still interact with the world normally.
All hooks (except interaction_log) return int verdicts:
public int evaluate(UUID playerUuid, String worldName, int x, int y, int z) {
if (!isProtected(worldName, x, y, z)) return 0; // ALLOW
if (!hasBypass(playerUuid)) return 1; // DENY_WITH_MESSAGE
return 0; // ALLOW (bypass)
}
public String fetchDenyReason(UUID playerUuid, String worldName, int x, int y, int z) {
return "&redThis area is protected!";
}| Verdict | Name | Behavior |
|---|---|---|
0 |
ALLOW | Action proceeds |
1 |
DENY_WITH_MESSAGE | Blocked; mixin calls fetch*DenyReason() and sends to player |
2 |
DENY_SILENT | Blocked, no message sent |
3 |
DENY_MOD_HANDLES | Blocked, consumer mod sends its own messages |
| negative | ALLOW | Fail-open safety |
HyperProtect-Mixin does NOT check bypass permissions. The mixin layer passes all actions to your hook — your hook implementation decides whether to allow or deny.
public class MyProtectionHook {
public int evaluate(UUID playerUuid, String worldName, int x, int y, int z) {
// Your mod handles bypass logic
if (hasBypass(playerUuid)) {
return 0; // ALLOW
}
if (isProtected(worldName, x, y, z)) {
return 1; // DENY_WITH_MESSAGE
}
return 0; // ALLOW
}
public String fetchDenyReason(UUID playerUuid, String worldName, int x, int y, int z) {
return "&#FF5555This area is protected!";
}
}This design means:
- Different mods can implement different bypass rules
- No coupling between the mixin layer and any specific permission system
- The hook is the single source of truth for allow/deny decisions
You don't need a compile-time dependency on HyperProtect-Mixin. Register hooks via the shared AtomicReferenceArray:
@SuppressWarnings("unchecked")
private void registerHooks() {
AtomicReferenceArray<Object> bridge = (AtomicReferenceArray<Object>)
System.getProperties().get("hyperprotect.bridge");
if (bridge == null) {
getLogger().warning("HyperProtect-Mixin bridge not found!");
return;
}
bridge.set(0, new BlockBreakHook()); // block_break
bridge.set(1, new ExplosionHook()); // explosion
bridge.set(8, new SpawnHook()); // mob_spawn
// ... register more hooks
}Deny messages returned from fetch*DenyReason() support &-code formatting:
| Code | Effect |
|---|---|
&0-&9, &a-&f |
Standard color codes |
&#RRGGBB |
Hex color (e.g., &#FF5555) |
&#RGB |
Short hex (e.g., &#F55) |
&red, &blue, etc. |
Named colors |
&l |
Bold |
&o |
Italic |
&m |
Monospace |
&r |
Reset formatting |
Available named colors: black, dark_blue, dark_green, dark_aqua, dark_red, dark_purple, gold, gray, dark_gray, blue, green, aqua, red, light_purple, yellow, white, orange, pink, cyan, brown, lime, magenta
Example:
return "&#FF5555&lProtected! &rYou cannot break blocks in &gold" + regionName;Each slot supports only one handler. The last bridge.set(index, ...) wins. If two mods register the same slot, the second overwrites the first.
Design your mod to be the single source of truth for each hook you register.
Hooks are called from server threads (typically the world thread). Your hook implementations must be thread-safe:
- Use
ConcurrentHashMapfor shared state - Avoid blocking operations in hook methods
- The same hook may be called concurrently for different worlds
To cleanly remove a hook (e.g., on mod disable):
@SuppressWarnings("unchecked")
AtomicReferenceArray<Object> bridge = (AtomicReferenceArray<Object>)
System.getProperties().get("hyperprotect.bridge");
if (bridge != null) {
bridge.set(0, null); // Remove block_break hook
}When the use hook (slot 20) fires for NPC interactions, HyperProtect-Mixin extracts the NPC's role name via reflection and stores it in a system property:
public int evaluateUse(UUID playerUuid, String worldName, int x, int y, int z) {
// Read NPC role context (set by SimpleInstantInteractionGate)
String npcRole = (String) System.getProperties().remove("hyperprotect.context.npc_role");
if (npcRole != null) {
// Classify: is this a tameable creature or a shop/quest NPC?
if (isTameableRole(npcRole)) {
return checkTamePermission(playerUuid, worldName, x, y, z);
} else {
return checkInteractPermission(playerUuid, worldName, x, y, z);
}
}
return 0; // ALLOW
}The remove() call atomically reads and clears the context property.
When the use, block_place, hammer, or other block interaction hooks fire, the block type at the target position is available via system properties:
public int evaluateUse(UUID playerUuid, String worldName, int x, int y, int z) {
String blockId = (String) System.getProperties().remove("hyperprotect.context.block_id");
String blockState = (String) System.getProperties().remove("hyperprotect.context.block_state");
// Use block type for fine-grained protection decisions
if ("crafting_bench".equals(blockId)) {
return checkBenchPermission(playerUuid, worldName, x, y, z);
}
return 0; // ALLOW
}Since v1.2.1, the slot 23 (crafting_resource) check is integrated directly into CraftingGateInterceptor's @Redirect on craftItem(). The interceptor has access to bench coordinates via @Shadow fields and player UUID via ComponentAccessor, so no cross-mixin ThreadLocal context is needed.
BenchPositionCapture still stores bench coords and player UUID in system-property-backed ThreadLocals (hyperprotect.ctx.craftingPlayerUuid, hyperprotect.ctx.benchCoords) for any consumer code that needs them outside the mixin pipeline.
public boolean evaluateChestAccess(UUID playerUuid, String worldName,
int benchX, int benchY, int benchZ,
int benchX2, int benchY2, int benchZ2) {
// benchX/Y/Z = crafting bench position (from @Shadow fields)
// playerUuid = crafter's UUID (from ComponentAccessor)
Territory territory = territories.getTerritoryAt(worldName, benchX, benchY, benchZ);
if (territory == null) return true; // allow
if (territory.isMember(playerUuid)) return true; // allow
return false; // Deny non-members from crafting at this bench
}