Repository navigation
FileChangeTracker
Basti edited this page Sep 27, 2026
·
1 revision
FileChangeTracker remembers the state of your plugin's data files and tells you what changed since the last time you looked, e.g. after /yourplugin reload or an automatic reload. YAML files are compared setting by setting, folders like lang/ file by file. The wording and logging stay with your plugin, so the list can be shown in every language you ship.
- Admins see at a glance what a reload actually picked up ("Detected file changes: config.yml (max_blocks=300)").
- An unchanged reload can say so ("No configuration changes detected.") instead of staying silent.
- Two plugins show the same list without copying the comparison code.
- Setting-level diff for YAML files: changed and new settings show their new value, removed ones show up as removed. Comments, blank lines, indentation and quoting don't count as changes.
- File-level diff for folders: changed, added and removed files, by content (CRC32 plus size), not by timestamp.
- Read errors never pass silently: a file that can't be read, or isn't valid YAML, logs one English
WARNING, is listed inunreadableFiles(), and keeps its previous snapshot.isEmpty()is thenfalse, so a reload never reports "no changes" for a file it couldn't read. - Old files saved as ANSI instead of UTF-8 are read as Windows-1252 (see Limitations).
- Memory only: after a restart or with a new tracker, the first call only takes a snapshot, so edits made while the server was off are never listed.
- Reads everything: each call reads every tracked YAML file and every matching folder file on the calling thread, so large folders take time. It isn't thread-safe.
-
Values compared as text: a type change such as
enabled: falseβenabled: "false"isn't listed, although Bukkit'sgetBooleanthen returns the default. Lists are joined with,, so[a, b],["a, b"]anda, blook the same, and an empty list equals"". -
Sections: adding or removing an empty section (
x: {}) isn't listed, akey:without a value shows up as removed, and keys containing a dot mix with nested paths. -
Values in full: nothing is masked or shortened, so a password or token in a tracked file ends up in the log if you log
describe(). -
Unreadable files: they keep their old snapshot and log a
WARNINGon every call while they stay broken. Their changes can't be listed, soisEmpty()andhasListedChanges()are bothfalsewhen nothing else changed. A file that was already broken at the first snapshot lists all its settings as new once it's fixed. - Missing YAML files count as empty, without a warning.
- Folders: one level deep, compared as whole files; a renamed file shows up as one removed and one added file.
-
Encodings: bytes that aren't valid UTF-8 are read as Windows-1252, while Bukkit reads the same file as UTF-8 with replacement characters. The list can then show
BΓ€umewhile the plugin loadedBοΏ½ume, and saving the same text again as UTF-8 isn't listed. -
Order: only the file lists are sorted.
keyChanges()follows the order ofyamlFile(...)calls,unreadableFiles()that order plus the file system's, andyamlFilepaths are reported as you passed them. - Warnings are in English.
- Build a tracker in
onEnable()with the files and folders you want to watch. - Call
compareAndUpdate()at the end ofonEnable(). The first call only takes the snapshot. - Call it again at the end of every reload and log the result through your
MessageService.
| Method | Purpose |
|---|---|
FileChangeTracker.builder(File dataFolder, Logger logger) |
Starts a tracker for the plugin's data folder. logger may be null (read errors aren't logged then). |
yamlFile(String relativePath) |
Adds a YAML file compared setting by setting, e.g. config.yml. |
folder(String relativePath, String suffix) |
Adds a folder whose files ending in suffix (case-insensitive, not recursive) are compared by content, e.g. lang, .yml. Hidden files (name starts with .) are skipped. |
compareAndUpdate() |
Compares with the last snapshot, stores the current state as the new snapshot, and returns the Changes. |
hasSnapshot() |
true once compareAndUpdate() has run at least once. |
Changes member |
Contents |
|---|---|
firstSnapshot() |
true on the first call: only the snapshot was taken, nothing is compared. Don't log anything then. |
isEmpty() |
true if nothing changed and every file could be read, so "no changes" is safe to report. |
hasListedChanges() |
true if keyChanges() or the folder file lists below have something to list. |
keyChanges() |
One FileKeyChanges(file, changes) per YAML file with changes; changes holds KeyChange(key, newValue), sorted by key. |
changedFiles() |
Folder files whose content changed. |
addedFiles() |
Folder files that are new since the last snapshot. |
removedFiles() |
Folder files that were deleted since the last snapshot. |
allChangedFiles() |
The three lists above together, sorted. |
unreadableFiles() |
Files that couldn't be read or parsed this time; they count as unchanged. |
-
KeyChange#newValue()isnullfor a removed setting.KeyChange#describe()giveskey=valueorkey=<removed>;FileKeyChanges#describe()joins them with,. - Lists are shown as their items joined with
,(e.g.saplings=OAK_SAPLING, BIRCH_SAPLING). - Folder file paths are relative to the data folder, with
/separators (e.g.lang/de_DE.yml), and sorted. - A deleted YAML file lists all its settings as removed; a YAML file that didn't exist yet lists all its settings as new.
import net.kroet.turtlelib.helper.FileChangeTracker;
private FileChangeTracker changeTracker;
@Override
public void onEnable() {
// ... load config and messages ...
changeTracker = FileChangeTracker.builder(getDataFolder(), getLogger())
.yamlFile("config.yml")
.folder("lang", ".yml")
.build();
changeTracker.compareAndUpdate(); // first snapshot
}
public void reload() {
// ... reload config and messages ...
FileChangeTracker.Changes changes = changeTracker.compareAndUpdate();
if (changes.firstSnapshot()) {
return;
}
if (changes.isEmpty()) {
messages.log(getLogger(), Level.INFO, "log.reload_changes_none");
return;
}
if (!changes.hasListedChanges()) {
return; // only unreadable files, already logged as a WARNING
}
messages.log(getLogger(), Level.INFO, "log.reload_changes_header");
for (FileChangeTracker.FileKeyChanges file : changes.keyChanges()) {
messages.log(getLogger(), Level.INFO, "log.reload_changes_line",
Map.of("file", file.file(), "changes", file.describe()));
}
for (String file : changes.allChangedFiles()) {
messages.log(getLogger(), Level.INFO, "log.reload_changes_locale_line", Map.of("file", file));
}
}- Take the snapshot after everything that writes files during startup or reload (key renames, new default keys, language syncing), otherwise those writes show up as the admin's changes on the next reload.
- The tracker is meant for the main thread; it keeps its snapshot in memory only, so a server restart starts over with
firstSnapshot(). - It doesn't watch files by itself. Pair it with ConfigWatcher if reloads should happen automatically.
- MessageService logs the result in the server's language.
- ConfigWatcher triggers the reloads whose changes this helper lists.