Skip to content

Commit 803c6ec

Browse files
committed
pause timer when no player is in a challenge
1 parent 7023c3a commit 803c6ec

File tree

5 files changed

+167
-15
lines changed

5 files changed

+167
-15
lines changed

src/main/kotlin/li/angu/challengeplugin/listeners/PlayerConnectionListener.kt

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,11 @@ class PlayerConnectionListener(private val plugin: ChallengePluginPlugin) : List
5656
// Save player data for the challenge
5757
plugin.playerDataManager.savePlayerData(player, challenge.id)
5858

59-
// Don't remove player from challenge yet - they should remain in the challenge
60-
// data so that when they rejoin they can be put back in the same challenge
59+
// Remove the player from the challenge to pause timer if needed
60+
challenge.removePlayer(player)
61+
62+
// But keep the player in the challenge map for reconnection
63+
// The player data is saved and we'll restore it when they reconnect
6164
}
6265
}
6366

src/main/kotlin/li/angu/challengeplugin/managers/ChallengeManager.kt

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import java.util.UUID
1212
import java.util.concurrent.ConcurrentHashMap
1313
import org.bukkit.configuration.file.YamlConfiguration
1414
import java.io.File
15+
import java.time.Duration
1516
import java.time.Instant
1617

1718
class ChallengeManager(private val plugin: ChallengePluginPlugin) {
@@ -49,15 +50,30 @@ class ChallengeManager(private val plugin: ChallengePluginPlugin) {
4950
challenge.completedAt = Instant.ofEpochSecond(config.getLong("completedAt"))
5051
}
5152

53+
// Load pause data if it exists
54+
if (config.contains("pausedAt")) {
55+
challenge.pausedAt = Instant.ofEpochSecond(config.getLong("pausedAt"))
56+
}
57+
58+
if (config.contains("totalPausedDuration")) {
59+
challenge.totalPausedDuration = Duration.ofSeconds(config.getLong("totalPausedDuration"))
60+
}
61+
62+
if (config.contains("lastEmptyTimestamp")) {
63+
challenge.lastEmptyTimestamp = Instant.ofEpochSecond(config.getLong("lastEmptyTimestamp"))
64+
}
65+
5266
activeChallenges[id] = challenge
5367

5468
// Update player map
5569
challenge.players.forEach { playerId ->
5670
playerChallengeMap[playerId] = id
5771
}
5872

59-
// Load world if not loaded
60-
loadWorld(challenge.worldName)
73+
// Only load world if challenge has players or is recently empty
74+
if (challenge.players.isNotEmpty() || !challenge.isReadyForUnload()) {
75+
loadWorld(challenge.worldName)
76+
}
6177
} catch (e: Exception) {
6278
plugin.logger.warning("Failed to load challenge from file ${file.name}: ${e.message}")
6379
}
@@ -77,6 +93,11 @@ class ChallengeManager(private val plugin: ChallengePluginPlugin) {
7793
challenge.startedAt?.let { config.set("startedAt", it.epochSecond) }
7894
challenge.completedAt?.let { config.set("completedAt", it.epochSecond) }
7995

96+
// Save pause data
97+
challenge.pausedAt?.let { config.set("pausedAt", it.epochSecond) }
98+
config.set("totalPausedDuration", challenge.totalPausedDuration.seconds)
99+
challenge.lastEmptyTimestamp?.let { config.set("lastEmptyTimestamp", it.epochSecond) }
100+
80101
config.save(file)
81102
}
82103
}
@@ -117,6 +138,21 @@ class ChallengeManager(private val plugin: ChallengePluginPlugin) {
117138
*/
118139
fun addPlayerToChallenge(playerId: UUID, challengeId: UUID) {
119140
playerChallengeMap[playerId] = challengeId
141+
142+
// This method is called during reconnection, so we need to make sure
143+
// the player is also present in the challenge's player set
144+
val challenge = activeChallenges[challengeId]
145+
if (challenge != null) {
146+
// We don't use challenge.addPlayer since that's meant for Player objects
147+
// But we want to make sure the player UUID is in the challenge's set
148+
challenge.players.add(playerId)
149+
150+
// If this is the first player, resume the timer if it was paused
151+
if (challenge.players.size == 1 && challenge.pausedAt != null) {
152+
challenge.resumeTimer()
153+
challenge.lastEmptyTimestamp = null
154+
}
155+
}
120156
}
121157

122158
fun joinChallenge(player: Player, challenge: Challenge): Boolean {

src/main/kotlin/li/angu/challengeplugin/models/Challenge.kt

Lines changed: 76 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import org.bukkit.World
44
import org.bukkit.entity.Player
55
import java.util.UUID
66
import java.time.Instant
7+
import java.time.Duration
78
import org.bukkit.GameMode
89
import li.angu.challengeplugin.utils.TimeFormatter
910

@@ -21,7 +22,10 @@ class Challenge(
2122
var status: ChallengeStatus = ChallengeStatus.ACTIVE,
2223
val players: MutableSet<UUID> = mutableSetOf(),
2324
var completedAt: Instant? = null,
24-
var startedAt: Instant? = null
25+
var startedAt: Instant? = null,
26+
var pausedAt: Instant? = null,
27+
var totalPausedDuration: Duration = Duration.ZERO,
28+
var lastEmptyTimestamp: Instant? = null
2529
) {
2630

2731
fun addPlayer(player: Player): Boolean {
@@ -31,16 +35,35 @@ class Challenge(
3135

3236
val added = players.add(player.uniqueId)
3337

34-
// If this is the first player, set startedAt
35-
if (added && startedAt == null && players.size == 1) {
36-
startedAt = Instant.now()
38+
// Check if this is the first player (after a pause)
39+
if (added && players.size == 1) {
40+
// If the challenge was started but paused, resume the timer
41+
if (startedAt != null && pausedAt != null) {
42+
resumeTimer()
43+
}
44+
// If this is the first player ever, set startedAt
45+
else if (startedAt == null) {
46+
startedAt = Instant.now()
47+
}
48+
49+
// Reset the lastEmptyTimestamp since we have players now
50+
lastEmptyTimestamp = null
3751
}
3852

3953
return added
4054
}
4155

4256
fun removePlayer(player: Player): Boolean {
43-
return players.remove(player.uniqueId)
57+
val removed = players.remove(player.uniqueId)
58+
59+
// If there are no players left in the challenge, pause the timer
60+
if (removed && players.isEmpty()) {
61+
pauseTimer()
62+
// Set the timestamp when the challenge became empty
63+
lastEmptyTimestamp = Instant.now()
64+
}
65+
66+
return removed
4467
}
4568

4669
fun isPlayerInChallenge(player: Player): Boolean {
@@ -57,18 +80,60 @@ class Challenge(
5780
completedAt = Instant.now()
5881
}
5982

83+
fun pauseTimer() {
84+
if (pausedAt == null && startedAt != null) {
85+
pausedAt = Instant.now()
86+
}
87+
}
88+
89+
fun resumeTimer() {
90+
val paused = pausedAt
91+
if (paused != null) {
92+
// Calculate the duration the timer was paused
93+
val pauseDuration = Duration.between(paused, Instant.now())
94+
// Add to total paused duration
95+
totalPausedDuration = totalPausedDuration.plus(pauseDuration)
96+
// Reset the pause timestamp
97+
pausedAt = null
98+
}
99+
}
100+
101+
fun getEffectiveDuration(): Duration {
102+
if (startedAt == null) {
103+
return Duration.ZERO
104+
}
105+
106+
val end = if (status != ChallengeStatus.ACTIVE && completedAt != null) {
107+
completedAt
108+
} else if (pausedAt != null) {
109+
pausedAt
110+
} else {
111+
Instant.now()
112+
}
113+
114+
// Calculate raw duration from start to end
115+
val rawDuration = Duration.between(startedAt, end)
116+
117+
// Subtract total time paused to get effective duration
118+
return rawDuration.minus(totalPausedDuration)
119+
}
120+
60121
fun getFormattedDuration(): String {
61122
if (startedAt == null) {
62123
return "Not started"
63124
}
64125

65-
// If challenge is completed or failed, show the fixed duration
66-
if (status != ChallengeStatus.ACTIVE && completedAt != null) {
67-
return TimeFormatter.formatDuration(startedAt!!, completedAt)
68-
}
126+
// Calculate the effective duration (accounting for pauses)
127+
val effectiveDuration = getEffectiveDuration()
69128

70-
// Otherwise show the current running duration
71-
return TimeFormatter.formatDuration(startedAt!!)
129+
return TimeFormatter.formatDuration(effectiveDuration)
130+
}
131+
132+
fun isReadyForUnload(): Boolean {
133+
// If the challenge is empty and has been empty for at least 5 minutes
134+
return players.isEmpty() &&
135+
lastEmptyTimestamp != null &&
136+
Duration.between(lastEmptyTimestamp, Instant.now()).toMinutes() >= 5
72137
}
73138

74139
fun setupPlayerForChallenge(player: Player, world: World) {

src/main/kotlin/li/angu/challengeplugin/tasks/TimerTask.kt

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,51 @@ class TimerTask(private val plugin: ChallengePluginPlugin) : BukkitRunnable() {
2929

3030
companion object {
3131
fun startTimer(plugin: ChallengePluginPlugin) {
32+
// Start the timer display task (runs every second)
3233
TimerTask(plugin).runTaskTimer(plugin, 0L, 20L) // Update every second
34+
35+
// Start the world unload check task (runs every minute)
36+
WorldUnloadTask(plugin).runTaskTimer(plugin, 20L * 60L, 20L * 60L)
3337
}
3438
}
3539
}
40+
41+
class WorldUnloadTask(private val plugin: ChallengePluginPlugin) : BukkitRunnable() {
42+
43+
override fun run() {
44+
// Get all active challenges
45+
val challenges = plugin.challengeManager.getActiveChallenges()
46+
47+
for (challenge in challenges) {
48+
// Check if the challenge has been empty for 5+ minutes
49+
if (challenge.isReadyForUnload()) {
50+
// Unload the challenge world and its associated dimensions
51+
unloadChallengeWorlds(challenge.worldName)
52+
53+
plugin.logger.info("Unloaded worlds for challenge ${challenge.name} (${challenge.id}) due to inactivity")
54+
}
55+
}
56+
}
57+
58+
private fun unloadChallengeWorlds(worldName: String) {
59+
// Try to unload the main world
60+
val mainWorld = Bukkit.getWorld(worldName)
61+
if (mainWorld != null && Bukkit.unloadWorld(mainWorld, true)) {
62+
plugin.logger.info("Unloaded main world: $worldName")
63+
}
64+
65+
// Try to unload the nether world
66+
val netherName = "${worldName}_nether"
67+
val netherWorld = Bukkit.getWorld(netherName)
68+
if (netherWorld != null && Bukkit.unloadWorld(netherWorld, true)) {
69+
plugin.logger.info("Unloaded nether world: $netherName")
70+
}
71+
72+
// Try to unload the end world
73+
val endName = "${worldName}_the_end"
74+
val endWorld = Bukkit.getWorld(endName)
75+
if (endWorld != null && Bukkit.unloadWorld(endWorld, true)) {
76+
plugin.logger.info("Unloaded end world: $endName")
77+
}
78+
}
79+
}

src/main/kotlin/li/angu/challengeplugin/utils/TimeFormatter.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ object TimeFormatter {
1212
Duration.between(start, Instant.now())
1313
}
1414

15+
return formatDuration(duration)
16+
}
17+
18+
fun formatDuration(duration: Duration): String {
1519
val days = duration.toDays()
1620
val hours = duration.toHours() % 24
1721
val minutes = duration.toMinutes() % 60

0 commit comments

Comments
 (0)