All versions
๐ด
๐ด
๐ด
๐ก
๐ก
๐ก
๐ก
๐ก
๐ก
2.0.7
Release
CombatGunSSS 2.0.75 months ago 203
Compatibility
Minecraft: Java Edition
1.21.x
Platforms
Bukkit
Paper
Purpur
Spigot
Changes
[2.0.7] - 2026-05-22
Fixed
๐ด StatsManager.flushBuffer() โ Kill data silently lost under concurrent load (Critical)
- Root cause: The old flush implementation called
new HashMap<>(buffer)to snapshot the map, then immediately calledbuffer.clear(). The gap between these two statements allowed the main thread to callrecordKill()and insert a new entry viacomputeIfAbsent. That new entry was then deleted bybuffer.clear()โ it was neither in the snapshot (inserted after it was taken) nor survived in the buffer (immediately cleared), so it was never written to SQLite. - Fix: Replaced the snapshot-and-clear pattern with a per-key drain loop: each entry is removed from the
ConcurrentHashMapindividually usingremove(key). SinceConcurrentHashMap.remove()is per-key atomic, any new entry inserted by the main thread after its key has already been drained is guaranteed to remain in the map and be picked up by the next flush cycle. No kill data can be lost.
๐ด ThrowableManager โ BukkitRunnable tasks not cancelled on plugin disable (Critical)
- Root cause:
onDisable()stopped the HUD task, bleeding tasks, reload tasks, and the SQLite connection โ but never cancelled tasks created byThrowableManager. The fuse-timerBukkitRunnableinhandleThrow()and the smoke particle loop indetonateSmoke()continued to execute after the plugin was disabled, throwingIllegalPluginAccessExceptionfor every remaining tick until the tasks expired naturally. - Fix 1: Added a
cancelAll()method toThrowableManagerthat clearsliveProjectilesand cancels all tracked smoke tasks. - Fix 2:
detonateSmoke()now stores the returnedBukkitTaskreference in aConcurrentHashMap<UUID, BukkitTask>keyed by a random UUID. The task removes its own entry when it completes normally.cancelAll()iterates this map and cancels every outstanding task. - Fix 3:
CombatGunSSSPlugin.onDisable()now callsthrowableManager.cancelAll()alongside the other existing cleanup calls.
๐ด GunListener โ Duplicate import statement causes compiler warning (Low / Code Quality)
- Root cause:
GunListener.javacontained two identicalimport java.util.concurrent.ThreadLocalRandom;declarations โ one was introduced during a merge and never removed. - Fix: Removed the duplicate import. The class compiles cleanly with a single declaration.
๐ก GunListener.clearAds() โ ADS exit incorrectly removes scope slowness (Medium)
- Root cause:
clearAds()used the conditionexisting.getAmplifier() <= 5before removing the Slowness effect. This range is too broad:SCOPE_SLOWNESS = 3falls within it. When a player was simultaneously scoped (Slowness level 3 applied byonSneak) and then exited ADS mode,clearAds()removed the Slowness effect entirely โ stripping the scope movement penalty and leaving the player able to move at full speed while scoped. - Fix: Changed the condition to
existing.getAmplifier() != SCOPE_SLOWNESS. ADS exit now only removes the Slowness effect when its amplifier level is not the scope level โ leaving scope behaviour intact.
๐ก ReloadManager โ Hardcoded English strings bypass i18n system (Medium)
- Root cause: Four player-facing messages in
ReloadManagerwere hardcoded English strings instead of routing throughLangManager: "Magazine is already full!", "No<ammo>in inventory.", "Reload failed: no ammo left.", and the entire "โ Reloaded! [x/y] -z ammo" completion message. Servers configured withlanguage: vidisplayed English during all reload feedback. - Fix: All four messages now call
plugin.getLangManager().get(key)/.format(key, args)using the existing lang keys (gun.reload_full,gun.reload_no_ammo,gun.reload_failed,gun.reload_done) that were already defined in bothlang/en.ymlandlang/vi.ymlbut never wired up inReloadManager.
๐ก BleedingManager.tryApply() โ Inconsistent RNG usage (Medium)
- Root cause:
tryApply()usedMath.random(), which delegates to a single sharedjava.util.Randominstance. This is inconsistent withGunListener, which explicitly replacedstatic RandomwithThreadLocalRandom.current()(with a comment explaining the reasoning). On servers with many simultaneous bleed chance rolls (burst fire hitting multiple targets), the shared lock inMath.random()causes minor contention. - Fix: Replaced
Math.random()withThreadLocalRandom.current().nextDouble(), consistent with the rest of the codebase.
๐ก ThrowableManager.detonateSmoke() โ Inconsistent RNG usage (Medium)
- Root cause: Same issue as
BleedingManagerโMath.random()used for particle position offsets inside a hot loop (20 calls per 4-tick interval per active smoke grenade). - Fix: Replaced all three
Math.random()calls withThreadLocalRandom.current().nextDouble().
๐ก AttachmentManager โ Duplicate import statement (Code Quality)
- Root cause:
AttachmentManager.javacontained two identicalimport org.bukkit.entity.Player;declarations introduced during a refactor. - Fix: Removed the duplicate import.
๐ก GunListener.getRecentDamage() โ Mutable map reference exposed via public API (Medium)
- Root cause:
getRecentDamage()returned the internalHashMap<UUID, DamageRecord>reference directly. Any external code (addon plugins accessingGunListenerviaCombatGunAPI) callinggetRecentDamage().clear()or.put(...)could silently corrupt kill-attribution logic without any indication. - Fix 1:
getRecentDamage()now returnsCollections.unmodifiableMap(recentDamage)โ external callers can read the map but cannot mutate it. - Fix 2: Added
getDamageRecord(UUID entityId)โ a safer lookup method that returns the record only if it is non-null and within the TTL window.EntityDeathListenershould use this instead ofgetRecentDamage().get(uuid)for correct TTL filtering.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
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:BSt2YZOI:gQ7tunKR"
}
// Legacy Loom dependency
dependencies {
modImplementation "maven.modrinth:BSt2YZOI:gQ7tunKR"
}

