Skip to content

Commit 888bbf8

Browse files
committed
feat: add resourcewipe module temp
1 parent e918b6d commit 888bbf8

File tree

16 files changed

+590
-0
lines changed

16 files changed

+590
-0
lines changed
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
name: Plugin ResourceWipe Tests
2+
3+
on:
4+
push:
5+
paths:
6+
- 'plugins/plugin-resourcewipe/**'
7+
pull_request:
8+
paths:
9+
- 'plugins/plugin-resourcewipe/**'
10+
11+
jobs:
12+
test:
13+
runs-on: ubuntu-latest
14+
steps:
15+
- uses: actions/checkout@v4
16+
- name: Set up JDK 21
17+
uses: actions/setup-java@v4
18+
with:
19+
java-version: '21'
20+
distribution: 'temurin'
21+
- name: Build & test plugin module
22+
run: |
23+
./gradlew :plugins:plugin-resourcewipe:test -x :folia-api:compileJava -x :folia-api:compileKotlin --no-daemon

docs/resource-wipe.md

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
# Resource World Auto-Wipe — How It Works
2+
3+
This document explains the design and runtime behaviour of the Resource World Auto-Wipe system. It describes when wipes run, what happens before/during/after a wipe, configuration examples, commands, safety behaviour, backups, and recovery.
4+
5+
## Summary
6+
7+
- Purpose: Automatically reset (wipe) configurable resource worlds on a schedule or on-demand, while taking backups and ensuring player safety.
8+
- Default strategy: reset-by-template (copy a prepared template world into the resource world folder). The implementation also supports other strategies (e.g., chunk-regeneration) if configured.
9+
- Backups: pre-wipe compressed snapshots with retention/rotation.
10+
11+
## Core concepts
12+
13+
- Resource world: a world configured to be periodically wiped (e.g. "mining", "islands", "resource_world").
14+
- Template world: a pristine world folder used as the source for resets. Kept under the plugin data folder or a configured path.
15+
- Backup snapshot: a compressed archive of the current world created before a wipe.
16+
- Scheduler: drives automatic wipes based on human-friendly intervals or cron expressions.
17+
- WorldManager: component that performs the unload, copy, and reload operations safely (Folia-aware when running on Folia).
18+
19+
## When wipes run
20+
21+
Wipes occur under these triggers (configurable):
22+
23+
- Scheduled automatic wipe: run at configured intervals or cron expression (recommended for predictable resets).
24+
- Manual wipe: executed immediately via admin command (`/resourcewipe force <world>`).
25+
- On server start: optionally run a wipe on server startup if enabled in config.
26+
- Conditional triggers: optional plugin hooks for external criteria (player count, world size) — available via plugin API.
27+
28+
Typical schedule examples:
29+
30+
- Daily at midnight: `0 0 * * *` (cron) or `every 1d at 00:00` (human-friendly form)
31+
- Weekly on Monday at 04:00: `0 4 * * 1` or `every 7d at 04:00` (depending on config parser)
32+
33+
## High-level wipe workflow
34+
35+
1. Announcement phase
36+
- When a scheduled wipe is imminent, the system broadcasts configurable countdown messages (action bar/title/chat) at configurable intervals (e.g., 10m, 5m, 1m, 10s).
37+
2. Prepare players
38+
- Players currently inside the target world will be teleported to a safe world or spawn (configurable), or optionally moved to spectator mode until the wipe completes.
39+
- Optionally preserve and store player inventories if configured.
40+
3. Pause world access
41+
- The world is locked to prevent new joins while the wipe proceeds.
42+
4. Create backup (optional but recommended)
43+
- The BackupManager creates a compressed snapshot (zip) of the world folder and stores it under `plugins/resource-wipe/backups/<worldname>/<timestamp>.zip`.
44+
- Rotation: only the last `N` snapshots are kept (configurable; default: 7). Older snapshots are removed.
45+
5. Unload world
46+
- The plugin uses server APIs and respects Folia dispatchers to safely unload the world.
47+
6. Reset world
48+
- Template-copy strategy (default): move or rename the existing world folder out of the active folder and copy the template world folder into place.
49+
- Alternative strategies (if enabled): chunk regeneration using server APIs to change blocks back to template state.
50+
7. Load world
51+
- Load the fresh world and restore spawn, world settings, and any configured metadata.
52+
8. Post-wipe steps
53+
- Run optional post-wipe script or hook.
54+
- Re-allow player access and announce completion.
55+
56+
## Configuration (example)
57+
58+
Place configuration under `plugins/resource-wipe/config.yml` with keys similar to the example below.
59+
60+
```yaml
61+
# Example config snippet
62+
resourceWorlds:
63+
- name: resource_world
64+
templatePath: templates/resource_world
65+
wipeSchedule: "0 4 * * *" # cron expression (daily at 04:00)
66+
backup: true
67+
retention: 7
68+
preserveInventories: false
69+
teleportTarget: lobby
70+
announce:
71+
preWipeSeconds: [600,300,60,10]
72+
messages:
73+
- "Resource world will wipe in {time}. Save your stuff!"
74+
wipe:
75+
strategy: template-copy # or chunk-regen
76+
dryRun: false
77+
safety:
78+
safeWorld: hub
79+
teleportPlayers: true
80+
lockDuringWipe: true
81+
```
82+
83+
Notes:
84+
- `templatePath` can be relative to the plugin data folder or an absolute path.
85+
- `wipeSchedule` supports cron expressions and human-friendly intervals when the parser is enabled.
86+
87+
## Commands & permissions
88+
89+
- `/resourcewipe status [world]` — show next scheduled wipe and last wipe details. Permission: `vanilife.resourcewipe.status` (or read default).
90+
- `/resourcewipe force <world>` — force immediate wipe (requires confirmation or `--force`). Permission: `vanilife.resourcewipe.admin`.
91+
- `/resourcewipe backup <world>` — create a backup now. Permission: `vanilife.resourcewipe.admin`.
92+
- `/resourcewipe pause <world>` — pause scheduled wipes for the world. Permission: `vanilife.resourcewipe.admin`.
93+
- `/resourcewipe resume <world>` — resume scheduled wipes. Permission: `vanilife.resourcewipe.admin`.
94+
95+
Admin command examples:
96+
- ` /resourcewipe force resource_world --force`
97+
- ` /resourcewipe status resource_world`
98+
99+
## Safety considerations
100+
101+
- Always enable backups (default) to avoid accidental data loss.
102+
- Use the template-copy strategy for reliability and speed; ensure template is prepared and tested.
103+
- When running on Folia, the plugin uses region/world dispatchers and unload/load APIs to avoid cross-thread operations.
104+
- Use announcements and a configurable grace period to give players time to prepare.
105+
- Validate free disk space before performing backups or copy operations to avoid partial wipes.
106+
107+
## Failure modes & recovery
108+
109+
- Backup creation failed: the plugin will abort the wipe and re-open the world to players, and log an error. Admin notification will be broadcast.
110+
- Copy or filesystem error during reset: the plugin will attempt to roll back by restoring the moved world folder (if present) and loading it back; it will then notify admins and keep the world available.
111+
- Partial load failure: plugin logs error and does not mark wipe as successful; backup remains intact for manual recovery.
112+
113+
Manual recovery steps (if something goes wrong):
114+
1. Stop the server.
115+
2. Restore a backup manually by unzipping `plugins/resource-wipe/backups/<world>/<timestamp>.zip` into the server root and renaming to the world folder name.
116+
3. Start the server and verify.
117+
118+
## Storage & retention
119+
120+
- Backups default to `plugins/resource-wipe/backups` and are compressed to save space.
121+
- Default retention: keep the most recent 7 backups per world; older files are deleted automatically.
122+
- For larger deployments consider offloading backups to remote storage (S3) via custom post-backup hook or an integration; this is not enabled by default.
123+
124+
## Notifications & logs
125+
126+
- The plugin logs operations to the server log at INFO level; failures are logged at ERROR.
127+
- Admins may opt-in for more verbose debug logs in `config.yml`.
128+
- Broadcast messages are configurable and support placeholders (e.g., `{world}`, `{time}`, `{remaining}`).
129+
130+
## Recommended defaults
131+
132+
- Strategy: `template-copy`
133+
- Backups: enabled, retention 7
134+
- Pre-wipe announcements: `[600,300,60,10]` seconds
135+
- Teleport players to a safe world named `hub` or `lobby`
136+
137+
## FAQ
138+
139+
Q: Can players keep their inventories across wipes?
140+
A: Yes — enable `preserveInventories: true` in the world config. The plugin will save inventories and attempt to restore them after the wipe for safety. This can be storage/time intensive.
141+
142+
Q: How long does a wipe take?
143+
A: Depends on world size and storage speed. A template-copy for a few hundred MB may take seconds to a minute; backups add time proportional to world size and compression settings.
144+
145+
Q: Can I test a wipe without affecting players?
146+
A: Use `dryRun: true` to simulate the wipe process (no filesystem changes). You can also run backups only with `/resourcewipe backup <world>`.
147+
148+
## Where to find the docs
149+
150+
- This file: `docs/resource-wipe.md`
151+
- Plugin data folder runtime examples: `plugins/resource-wipe/config.yml` and `plugins/resource-wipe/backups`
152+
153+
---
154+
155+
If you want, I can now:
156+
- Create an initial `plugins/plugin-resourcewipe` skeleton with `plugin.yml` and `config.yml` example.
157+
- Implement the BackupManager or WorldManager next.
158+
159+
Which would you like me to do next?
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
group = "net.azisaba.vanilife"
2+
version = "0.1.0"
3+
4+
repositories {
5+
mavenLocal()
6+
maven("https://repo.papermc.io/repository/maven-public/")
7+
mavenCentral()
8+
}
9+
10+
dependencies {
11+
// Avoid building :folia-api for quick local dev; depend on Paper API for compilation instead
12+
compileOnly("io.papermc.paper:paper-api:1.20.2-R0.1-SNAPSHOT")
13+
14+
// Testing: Kotest (JUnit5)
15+
testImplementation("io.kotest:kotest-runner-junit5:5.6.2")
16+
testImplementation("io.kotest:kotest-assertions-core:5.6.2")
17+
testImplementation("org.junit.jupiter:junit-jupiter:5.9.3")
18+
}
19+
20+
tasks.test {
21+
useJUnitPlatform()
22+
}
23+
24+
tasks.test {
25+
useJUnitPlatform()
26+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package net.azisaba.vanilife.resourcewipe
2+
3+
import java.io.File
4+
import java.nio.file.Files
5+
import java.nio.file.Path
6+
import java.nio.file.StandardCopyOption
7+
import java.time.Instant
8+
import java.time.format.DateTimeFormatter
9+
10+
class BackupManager(private val dataFolder: File) {
11+
private val backupsDir: Path = dataFolder.toPath().resolve("backups")
12+
var enabled: Boolean = true
13+
14+
init {
15+
Files.createDirectories(backupsDir)
16+
}
17+
18+
fun createBackup(worldFolder: File, retention: Int): Path? {
19+
if (!enabled) return null
20+
val ts = DateTimeFormatter.ISO_INSTANT.format(Instant.now()).replace(':', '-')
21+
val dest = backupsDir.resolve(worldFolder.name).resolve("$ts.zip")
22+
Files.createDirectories(dest.parent)
23+
ZipUtils.zipDirectory(worldFolder.toPath(), dest)
24+
rotateBackups(worldFolder.name, retention)
25+
return dest
26+
}
27+
28+
fun rotateBackups(worldName: String, keep: Int) {
29+
val dir = backupsDir.resolve(worldName).toFile()
30+
val files = dir.listFiles()?.sortedByDescending { it.lastModified() } ?: return
31+
for (i in keep until files.size) {
32+
files[i].delete()
33+
}
34+
}
35+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package net.azisaba.vanilife.resourcewipe
2+
3+
import org.bukkit.configuration.file.FileConfiguration
4+
5+
data class ResourceWorldConfig(
6+
val name: String,
7+
val templatePath: String,
8+
val wipeSchedule: String?,
9+
val wipeIntervalMinutes: Long?,
10+
val backup: Boolean,
11+
val retention: Int,
12+
val preserveInventories: Boolean,
13+
val teleportTarget: String?
14+
)
15+
16+
class PluginConfig(private val cfg: FileConfiguration) {
17+
val globalBackupEnabled: Boolean
18+
get() = cfg.getBoolean("backup.enabled", true)
19+
20+
val globalDefaultRetention: Int
21+
get() = cfg.getInt("backup.defaultRetention", 7)
22+
23+
fun getResourceWorlds(): List<ResourceWorldConfig> {
24+
val list = cfg.getMapList("resourceWorlds")
25+
return list.map { m ->
26+
val name = m["name"] as? String ?: "resource_world"
27+
val templatePath = m["templatePath"] as? String ?: "templates/$name"
28+
val wipeSchedule = m["wipeSchedule"] as? String
29+
val wipeIntervalMinutes = (m["wipeIntervalMinutes"] as? Number)?.toLong()
30+
val backup = (m["backup"] as? Boolean) ?: true
31+
val retention = (m["retention"] as? Number)?.toInt() ?: globalDefaultRetention
32+
val preserveInventories = (m["preserveInventories"] as? Boolean) ?: false
33+
val teleportTarget = m["teleportTarget"] as? String
34+
ResourceWorldConfig(name, templatePath, wipeSchedule, wipeIntervalMinutes, backup, retention, preserveInventories, teleportTarget)
35+
}
36+
}
37+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package net.azisaba.vanilife.resourcewipe
2+
3+
import org.bukkit.command.Command
4+
import org.bukkit.command.CommandExecutor
5+
import org.bukkit.command.CommandSender
6+
7+
class ResourceWipeCommand(private val plugin: ResourceWipePlugin) : CommandExecutor {
8+
override fun onCommand(sender: CommandSender, command: Command, label: String, args: Array<out String>): Boolean {
9+
if (args.isEmpty()) {
10+
sender.sendMessage("Usage: /resourcewipe <status|force|backup|pause|resume|backup-toggle> [world]")
11+
return true
12+
}
13+
when (args[0].lowercase()) {
14+
"status" -> {
15+
sender.sendMessage("ResourceWipe: status command - not implemented yet")
16+
}
17+
"force" -> {
18+
val world = args.getOrNull(1) ?: run {
19+
sender.sendMessage("Specify a world")
20+
return true
21+
}
22+
plugin.logger.info("Force wipe requested for $world by ${sender.name}")
23+
sender.sendMessage("Requested force wipe for $world")
24+
}
25+
"backup" -> {
26+
val world = args.getOrNull(1) ?: run {
27+
sender.sendMessage("Specify a world to backup")
28+
return true
29+
}
30+
val worldFolder = java.io.File(org.bukkit.Bukkit.getWorldContainer(), world)
31+
val cfg = plugin.savedConfig
32+
val rwCfg = cfg.getResourceWorlds().find { it.name == world }
33+
val retention = rwCfg?.retention ?: cfg.globalDefaultRetention
34+
val path = plugin.backupManager.createBackup(worldFolder, retention)
35+
if (path != null) sender.sendMessage("Backup created: $path") else sender.sendMessage("Backup system is disabled")
36+
}
37+
"backup-toggle" -> {
38+
// toggle global backup enabled
39+
val newVal = !plugin.backupManager.enabled
40+
plugin.backupManager.enabled = newVal
41+
sender.sendMessage("Backup system enabled = $newVal")
42+
plugin.logger.info("Backup system toggled to $newVal by ${sender.name}")
43+
}
44+
else -> sender.sendMessage("Unknown subcommand")
45+
}
46+
return true
47+
}
48+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package net.azisaba.vanilife.resourcewipe
2+
3+
import org.bukkit.plugin.java.JavaPlugin
4+
5+
class ResourceWipePlugin : JavaPlugin() {
6+
lateinit var backupManager: BackupManager
7+
lateinit var worldManager: WorldManager
8+
lateinit var scheduler: Scheduler
9+
lateinit var savedConfig: PluginConfig
10+
11+
override fun onEnable() {
12+
logger.info("ResourceWipePlugin enabling")
13+
saveDefaultConfig()
14+
val cfg = PluginConfig(config)
15+
this.savedConfig = cfg
16+
backupManager = BackupManager(dataFolder)
17+
backupManager.enabled = cfg.globalBackupEnabled
18+
worldManager = WorldManager(dataFolder)
19+
scheduler = Scheduler(this)
20+
21+
// Register command
22+
getCommand("resourcewipe")?.setExecutor(ResourceWipeCommand(this))
23+
24+
// Schedule simple periodic task for each configured world if interval provided
25+
cfg.getResourceWorlds().forEach { rw ->
26+
val minutes = rw.wipeIntervalMinutes ?: 0L
27+
if (minutes > 0L) {
28+
scheduler.scheduleRepeatingMinutes(minutes) {
29+
logger.info("Scheduled wipe would run for ${rw.name}")
30+
}
31+
}
32+
}
33+
}
34+
35+
override fun onDisable() {
36+
logger.info("ResourceWipePlugin disabling")
37+
}
38+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package net.azisaba.vanilife.resourcewipe
2+
3+
import org.bukkit.Bukkit
4+
import org.bukkit.plugin.java.JavaPlugin
5+
6+
class Scheduler(private val plugin: JavaPlugin) {
7+
fun scheduleRepeatingMinutes(intervalMinutes: Long, task: () -> Unit) {
8+
if (intervalMinutes <= 0L) return
9+
val ticks = intervalMinutes * 60L * 20L
10+
Bukkit.getScheduler().runTaskTimer(plugin, Runnable { task() }, ticks, ticks)
11+
}
12+
}

0 commit comments

Comments
 (0)