1.9.1
Compatibility
Changes
[1.9.1] - 2026-05-23 — Bug Fix Release
This is a patch release that resolves six bugs identified through code review.
No configuration keys were added, removed, or renamed. Existing data.yml and
config.yml files are fully compatible — no migration needed.
🐛 Bug Fixes
FIX [Critical] — Double timestamp in every audit log entry
- Root cause:
OPShield.audit()manually prepended a formatted timestamp to the message string before passing it toAuditLogger.log(). However,AuditLogger.buildLine()also wraps every line with its own timestamp, resulting in output like:
This affected every audit entry since v1.6.0.[2026-05-23 10:00:00] 2026-05-23 10:00:00 | Configuration reloaded. - Fix: Removed the manual timestamp construction from
OPShield.audit(). Timestamp formatting is now owned exclusively byAuditLogger.buildLine(). Audit lines now read correctly:[2026-05-23 10:00:00] Configuration reloaded. - Impact: Existing rotated log files (
audit.log.1etc.) retain the old double-timestamp format. New entries written after the upgrade are clean. No action required on existing files.
FIX [High] — auth_session_granted and auth_session_active messages were never sent
- Root cause: The language key
auth_session_grantedwas introduced in v1.9.0 but the correspondingsend()call was absent from bothcompletePrivilegeCommand()(session grant path) andhandlePrivilegeCommand()(session reuse path). Players had no way to know a session existed or how much time remained. - Fix:
completePrivilegeCommand()now callssendReplaced(player, "auth_session_granted", "minutes", …)immediately after the session is stored inauthenticatedSessions.handlePrivilegeCommand()now callssendReplaced(player, "auth_session_active", "minutes", …)(with the remaining minutes) on the session-reuse fast-path.
- New language key:
auth_session_activeadded toen.yml,vn.yml, andru.yml.
FIX [Medium] — Shadow-ban decoy messages lost all colour formatting
- Root cause: The fake-success message pipeline was:
PlainTextComponentSerializer.plainText().serialize(colorize(fakeMsg))colorize()correctly parsed&a,&c, etc. into an AdventureComponentwith colour, butPlainTextComponentSerializer.plainText().serialize()then stripped every colour code before wrapping the result in a hard-codedNamedTextColor.YELLOW. Language-file formatting was silently discarded. - Fix: Removed the unnecessary serialise/re-wrap chain. The
Componentreturned bycolorize(fakeMsg)is now sent directly:
Server owners can now apply any Adventure-compatible colour codes (player.sendMessage(colorize(fakeMsg));&a,&c,&l, etc.) in their language files and have them rendered correctly. - Cleanup: Removed now-unused imports
NamedTextColor,PlainTextComponentSerializer, andComponentfromOPShield.java.
FIX [Medium] — LockoutManager.cleanupExpired() discarded backoff count earlier than count_decay_hours intended
- Root cause:
cleanupExpired()removed any record whoseexpiryMs <= nowANDattempts == 0. When a lockout expired naturally,isLockedOut()setexpiryMs = 0andattempts = 0(intentionally preservingcountfor exponential backoff). On the very next cleanup tick (≤ 60 s later), the cleanup predicate matched and deleted the entire record, includingcount. Thesecurity.lockout.count_decay_hoursconfig key had no practical effect: the backoff counter was always reset within one cleanup cycle after each lockout expired. - Fix:
cleanupExpired()now accepts adecayMsparameter (derived fromlockoutDecayHoursinOPShield). A record is only removed when both conditions are true:- No active lockout and no in-progress attempts.
- The time elapsed since
lastLockoutAtMsexceedsdecayMs. Exponential backoff now behaves as documented.
- API change (internal):
LockoutManager.cleanupExpired()→cleanupExpired(long decayMs). No external callers; internal call site updated inOPShield.cleanupRuntimeState().
FIX [Medium] — /opshield reload blocked by allow_op_reload: false even for players with explicit opshield.reload permission
- Root cause: In
handleManagementCommand(), theallow_op_reloadflag was evaluated before the permission check. A player with an explicitopshield.reloadgrant from a permissions plugin was still denied whenallow_op_reload: false, making the permission effectively meaningless. - Fix: The permission check (
opshield.reload) now runs first. Theallow_op_reloadflag is applied only as a secondary restriction after the caller is confirmed to hold the permission. Console senders are unaffected.
FIX [Low] — IP tracking unreliable behind BungeeCord / Velocity (documentation + debug warning)
- Root cause:
getPlayerIpRaw()usesplayer.getAddress().getAddress().getHostAddress(). Behind a proxy, this resolves to the proxy's own address (commonly127.0.0.1or a private RFC-1918 range), not the connecting client's real IP. IP-based lockout mirroring andip_limittracking silently group all players under the same key. - Fix (partial): Full proxy-IP forwarding requires server-side configuration
outside the plugin's scope (Paper IP forwarding,
BungeeGuard, etc.). This release adds a debug-mode warning logged whendebug: trueand the resolved address is a loopback or private-range address, so administrators are alerted during setup. The warning is suppressed in production (debug: false) to avoid console spam on legitimate LAN servers.
🌍 Language File Changes
| Key | Files | Change |
|---|---|---|
auth_session_active |
en.yml, vn.yml, ru.yml |
Added — shown when an existing valid session is reused |
All other keys are unchanged and backwards-compatible.
🔧 Internal / Code Quality
- Removed four now-unused imports from
OPShield.java:LocalDateTime,ZoneId,Instant,DateTimeFormatter(timestamp moved toAuditLogger),Component,NamedTextColor,PlainTextComponentSerializer(shadow-ban message fix). LockoutManager.cleanupExpired()signature updated (internal only).- Added inline
FIX[…]comments at every corrected site for audit trail.
✅ Compatibility
| Item | Status |
|---|---|
config.yml (v1.9.0) |
✅ No changes required |
data.yml (v1.9.0) |
✅ No changes required |
| Language files (v1.9.0) | ⚠️ auth_session_active key added; old files work but players will see [Missing Message: auth_session_active] until updated |
| Paper API | ✅ 1.21+ (unchanged) |
Projects on Modrinth are automatically available through a Maven repository for use with JVM build tools such as Gradle. To learn more about the Modrinth Maven API, click here.
Note: When available, you should use the creator's maven repo instead as it will have transitive dependency information that the Modrinth Maven API does not. You may also end up with duplicate dependencies if you use a mix of Modrinth and non-Modrinth Maven repositories for your dependencies, because the group identifier will be different when served through the Modrinth Maven API.
Maven coordinates:
Version ID:
build.gradle:
repositories {
exclusiveContent {
forRepository {
maven {
name = "Modrinth"
url = "https://api.modrinth.com/maven"
}
}
// forRepositories(fg.repository) // Uncomment when using ForgeGradle
filter {
includeGroup "maven.modrinth"
}
}
}
// Standard Gradle dependency
dependencies {
implementation "maven.modrinth:Bv67n40d:aFgk0kYt"
}
// Legacy Loom dependency
dependencies {
modImplementation "maven.modrinth:Bv67n40d:aFgk0kYt"
}

