Skip to content

Commit 842479e

Browse files
committed
Finalize EndCrystals v2 docs, translations, and MIT licensing
1 parent 0ccb341 commit 842479e

7 files changed

Lines changed: 179 additions & 54 deletions

File tree

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2026 mrfloris
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 28 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,15 @@ The produced jar is named:
2020
- Optionally blocks player projectiles from breaking protected crystals
2121
- Keeps The End configurable so dragon-fight style gameplay can stay intact if desired
2222
- Uses the normal Paper plugin data folder: `plugins/1MB-EndCrystals/config.yml`
23-
- Migrates the misplaced legacy home-folder config into the correct server-local folder on first startup
23+
- Uses locale-ready message files in `plugins/1MB-EndCrystals/Translations/`
2424
- Supports `/_endcrystals reload`
2525
- Supports `/_endcrystals toggle [setting] [true|false]` for live boolean config toggles
2626
- Supports `/_endcrystals debug` for runtime/build/config diagnostics
2727
- Supports configurable command aliases from `config.yml`
2828
- Uses MiniMessage formatting for plugin output
29-
- Uses strict permission nodes with op not automatically bypassing protection
29+
- Keeps protection enabled by default while admin and break access stay permission-driven
3030
- Targets Java `25` and modern Paper builds
31-
- Builds with Gradle from a fresh clone without requiring any private `/servers/` test setup
31+
- Builds with Gradle from a fresh clone
3232

3333
## Commands
3434

@@ -54,20 +54,23 @@ If another plugin or the server already owns one of those names, 1MB-EndCrystals
5454
- `onembendcrystals.toggle`
5555
- `onembendcrystals.break`
5656

57-
All of these permissions default to `false`, so being op alone does not grant them. `onembendcrystals.admin` grants the command permissions. `onembendcrystals.break` allows a player to break otherwise protected crystals.
57+
Protection is enabled by default through the config values under `protection.*`. By default, regular players and ops are both blocked from breaking protected crystals or using the management commands. `onembendcrystals.break` must be granted explicitly to allow a player to break otherwise protected crystals, and `onembendcrystals.admin` or the individual command nodes must be granted explicitly to allow management access.
5858

5959
## Config
6060

6161
The plugin reads and writes its main config here:
6262

6363
`plugins/1MB-EndCrystals/config.yml`
6464

65-
Each Paper server gets its own local config folder, which is the normal Bukkit/Paper behavior.
65+
Locale files live here:
6666

67-
If you are upgrading from an older build that wrote to `~/plugins/1MB-EndCrystals/config.yml`, the plugin will copy that legacy file into the server-local plugin folder on first startup if the new server-local config does not already exist.
67+
`plugins/1MB-EndCrystals/Translations/Locale_EN.yml`
68+
69+
Each Paper server gets its own local config and translation folder, which is the normal Bukkit/Paper behavior.
6870

6971
Important defaults:
7072

73+
- `translations.locale: Locale_EN`
7174
- `commands.aliases: [endcrystals, ec]`
7275
- `protection.prevent-block-damage: true`
7376
- `protection.prevent-player-break: true`
@@ -88,23 +91,21 @@ This section summarizes the modernization work completed for the current refresh
8891
- Retargeted the plugin for modern Paper, validated against `1.21.11` and `26.1.2`
8992
- Standardized the shipped jar name to `1MB-EndCrystals-v2.0.1-021-v25-26.1.2.jar`
9093
- Reworked config storage to use the active server's `plugins/1MB-EndCrystals/config.yml`
91-
- Added one-time migration support for the old misplaced `~/plugins/1MB-EndCrystals/config.yml`
9294
- Added `/_endcrystals reload`
9395
- Added `/_endcrystals debug`
9496
- Added live config toggles through `/_endcrystals toggle [setting] [true|false]`
9597
- Moved command aliases into `config.yml` so they can be added or removed without editing `plugin.yml`
9698
- Kept `/_endcrystals` as the permanent primary command
97-
- Switched to explicit `onembendcrystals.*` permission nodes with `default: false`
99+
- Kept protection enabled by default while moving admin and crystal-break access onto explicit `onembendcrystals.*` permission nodes
98100
- Made crystal breaking permission-driven instead of implicitly allowing ops
99101
- Expanded protection beyond blocks to cover decorative and item entities
100102
- Added protection for minecart and boat-style entities that were still vulnerable on the legacy path
101103
- Improved debug output to show build/runtime info, config path, permissions, live toggles, and active commands
102-
- Moved plugin messages into config and formatted output with MiniMessage
103-
- Updated docs so public builds do not depend on any private local `/servers/` test directory
104+
- Split plugin messages into locale files under `Translations/` and formatted output with MiniMessage
104105

105106
## Build
106107

107-
The public build does not depend on any local `servers/` directory. A normal checkout builds against the Paper API from the Paper Maven repository, and any repo-local test servers are strictly optional private development tooling.
108+
A normal checkout builds against the Paper API from the Paper Maven repository.
108109

109110
Build with:
110111

@@ -120,10 +121,23 @@ Output jar:
120121

121122
1. Build the jar.
122123
2. Copy it into a Paper server's `plugins/` folder.
123-
3. Start the server once so the plugin creates `plugins/1MB-EndCrystals/config.yml`.
124-
4. Use `/_endcrystals debug` to verify runtime information.
124+
3. Start the server once so the plugin creates `plugins/1MB-EndCrystals/config.yml` and `plugins/1MB-EndCrystals/Translations/Locale_EN.yml`.
125+
4. Use `/_endcrystals debug` to verify runtime information, including the active locale file.
125126
5. Use `/_endcrystals reload` after editing the config.
126127

128+
## Support
129+
130+
If you run into a bug, want to request a feature, or need help using the plugin, please use the [GitHub Issues](https://github.com/mrfdev/EndCrystals/issues) section for this repository.
131+
132+
## Credits
133+
134+
- mrfloris ([GitHub: `mrfdev`](https://github.com/mrfdev)) for the original plugin, project direction, and the v2 refresh goals
135+
- OpenAI for helping modernize and ship the v2 update
136+
137+
## License
138+
139+
This project is licensed under the MIT License. See [LICENSE](LICENSE) for the full text.
140+
127141
## Testing Notes
128142

129-
The plugin is meant to be testable without WorldGuard present. If an end crystal explodes near normal blocks, the explosion should still happen but the blocks should remain intact while the plugin is enabled. Any `/servers/` directory used in development is for private local testing only and is not part of the public build flow.
143+
The plugin is meant to be testable without WorldGuard present. If an end crystal explodes near normal blocks, the explosion should still happen but the blocks should remain intact while the plugin is enabled.

src/main/java/com/mrfloris/endcrystals/EndCrystalsCommand.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,8 @@ private boolean handleDebug(CommandSender sender) {
8686
.formatted(System.getProperty("java.version", "unknown")), false);
8787
plugin.sendRich(sender, "<gray>Config:</gray> <white>%s</white>"
8888
.formatted(plugin.configManager().configPath()), false);
89+
plugin.sendRich(sender, "<gray>Locale:</gray> <white>%s</white> <gray>|</gray> <white>%s</white>"
90+
.formatted(config.localeName(), plugin.configManager().localePath()), false);
8991
plugin.sendRich(sender, "<gray>Commands:</gray> <white>%s</white>"
9092
.formatted(plugin.commandSummary()), false);
9193
plugin.sendRich(sender, "<gray>Placeholders:</gray> <white>None</white>", false);

src/main/java/com/mrfloris/endcrystals/ExternalConfigManager.java

Lines changed: 102 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@
77
import java.nio.file.Files;
88
import java.nio.file.Path;
99
import java.util.ArrayList;
10-
import java.util.LinkedHashSet;
1110
import java.util.LinkedHashMap;
11+
import java.util.LinkedHashSet;
1212
import java.util.List;
1313
import java.util.Map;
1414
import java.util.Objects;
@@ -17,34 +17,32 @@
1717

1818
public final class ExternalConfigManager {
1919

20+
private static final String DEFAULT_LOCALE_FILE = "Locale_EN.yml";
21+
2022
private final EndCrystalsPlugin plugin;
2123
private final Path configDirectory;
2224
private final Path configPath;
23-
private final Path legacyConfigPath;
25+
private final Path translationsDirectory;
2426

2527
private YamlConfiguration yaml;
28+
private YamlConfiguration localeYaml;
2629
private PluginConfig currentConfig;
30+
private Path localePath;
2731

2832
public ExternalConfigManager(EndCrystalsPlugin plugin) {
2933
this.plugin = plugin;
3034
this.configDirectory = plugin.getDataFolder().toPath().toAbsolutePath().normalize();
3135
this.configPath = this.configDirectory.resolve("config.yml");
32-
this.legacyConfigPath = Path.of(System.getProperty("user.home"), "plugins", plugin.getName(), "config.yml")
33-
.toAbsolutePath()
34-
.normalize();
36+
this.translationsDirectory = this.configDirectory.resolve("Translations");
3537
}
3638

3739
public void initialize() {
3840
try {
3941
Files.createDirectories(configDirectory);
42+
Files.createDirectories(translationsDirectory);
4043

4144
if (Files.notExists(configPath)) {
42-
if (Files.exists(legacyConfigPath)) {
43-
Files.copy(legacyConfigPath, configPath);
44-
plugin.getLogger().info("Copied legacy config from " + legacyConfigPath + " to " + configPath);
45-
} else {
46-
copyEmbeddedConfig();
47-
}
45+
copyEmbeddedConfig();
4846
}
4947
} catch (IOException exception) {
5048
throw new IllegalStateException("Could not initialize config at " + configPath, exception);
@@ -68,7 +66,22 @@ public void reload() {
6866
throw new IllegalStateException("Could not merge default config values into " + configPath, exception);
6967
}
7068

71-
this.currentConfig = PluginConfig.from(yaml);
69+
String resolvedLocaleFile = normalizeLocaleFileName(yaml.getString("translations.locale", DEFAULT_LOCALE_FILE));
70+
this.localePath = translationsDirectory.resolve(resolvedLocaleFile);
71+
initializeLocaleFile(resolvedLocaleFile);
72+
this.localeYaml = YamlConfiguration.loadConfiguration(localePath.toFile());
73+
74+
try {
75+
YamlConfiguration localeDefaults = loadEmbeddedLocaleDefaults(resolvedLocaleFile);
76+
localeYaml.addDefaults(localeDefaults);
77+
localeYaml.options().copyDefaults(true);
78+
normalizeLocaleConfig(localeDefaults);
79+
localeYaml.save(localePath.toFile());
80+
} catch (IOException exception) {
81+
throw new IllegalStateException("Could not merge default locale values into " + localePath, exception);
82+
}
83+
84+
this.currentConfig = PluginConfig.from(yaml, localeYaml, resolvedLocaleFile);
7285
}
7386

7487
public boolean toggle(String path, Boolean explicitValue) {
@@ -105,6 +118,11 @@ public Path configPath() {
105118
return configPath;
106119
}
107120

121+
public Path localePath() {
122+
ensureLoaded();
123+
return localePath;
124+
}
125+
108126
public Map<String, Boolean> liveToggleStates() {
109127
ensureLoaded();
110128

@@ -124,6 +142,8 @@ public java.util.List<String> liveToggleKeys() {
124142
private void ensureLoaded() {
125143
Objects.requireNonNull(currentConfig, "Config has not been loaded yet.");
126144
Objects.requireNonNull(yaml, "Config has not been loaded yet.");
145+
Objects.requireNonNull(localeYaml, "Locale has not been loaded yet.");
146+
Objects.requireNonNull(localePath, "Locale has not been loaded yet.");
127147
}
128148

129149
private void copyEmbeddedConfig() throws IOException {
@@ -146,17 +166,6 @@ private void normalizeConfig(YamlConfiguration defaults) {
146166
yaml.set("live-toggles", liveToggles);
147167

148168
normalizeProtectedEntityTypes(defaults);
149-
150-
String legacyUsageSingular = "<yellow>Use <white>/_endcrystal debug</white>, <white>reload</white>, or <white>toggle &lt;setting&gt; [true|false]</white>.</yellow>";
151-
String legacyUsagePlural = "<yellow>Use <white>/_endcrystals debug</white>, <white>reload</white>, or <white>toggle &lt;setting&gt; [true|false]</white>.</yellow>";
152-
String legacyUsageBracket = "<yellow>Use <white>/_endcrystals debug</white>, <white>reload</white>, or <white>toggle [setting] [true|false]</white>.</yellow>";
153-
String defaultUsage = defaults.getString("messages.command-usage");
154-
String currentUsage = yaml.getString("messages.command-usage");
155-
if ((legacyUsageSingular.equals(currentUsage)
156-
|| legacyUsagePlural.equals(currentUsage)
157-
|| legacyUsageBracket.equals(currentUsage)) && defaultUsage != null) {
158-
yaml.set("messages.command-usage", defaultUsage);
159-
}
160169
}
161170

162171
private void normalizeProtectedEntityTypes(YamlConfiguration defaults) {
@@ -192,4 +201,74 @@ private Set<String> toUpperCaseSet(List<String> values) {
192201
}
193202
return normalized;
194203
}
204+
205+
private void initializeLocaleFile(String localeFileName) {
206+
try {
207+
if (Files.exists(localePath)) {
208+
return;
209+
}
210+
211+
YamlConfiguration localeDefaults = loadEmbeddedLocaleDefaults(localeFileName);
212+
213+
if (yaml.isConfigurationSection("messages")) {
214+
for (String key : yaml.getConfigurationSection("messages").getKeys(true)) {
215+
localeDefaults.set("messages." + key, yaml.get("messages." + key));
216+
}
217+
}
218+
219+
localeDefaults.save(localePath.toFile());
220+
} catch (IOException exception) {
221+
throw new IllegalStateException("Could not initialize locale at " + localePath, exception);
222+
}
223+
}
224+
225+
private YamlConfiguration loadEmbeddedLocaleDefaults(String localeFileName) throws IOException {
226+
String resourcePath = resolveEmbeddedLocaleResource(localeFileName);
227+
try (InputStream resource = plugin.getResource(resourcePath)) {
228+
if (resource == null) {
229+
throw new IllegalStateException("Embedded locale resource was not found: " + resourcePath);
230+
}
231+
232+
return YamlConfiguration.loadConfiguration(new InputStreamReader(resource, StandardCharsets.UTF_8));
233+
}
234+
}
235+
236+
private String resolveEmbeddedLocaleResource(String localeFileName) {
237+
String requested = "Translations/" + localeFileName;
238+
if (plugin.getResource(requested) != null) {
239+
return requested;
240+
}
241+
242+
if (!DEFAULT_LOCALE_FILE.equals(localeFileName)) {
243+
plugin.getLogger().warning("Locale " + localeFileName + " was not bundled; falling back to " + DEFAULT_LOCALE_FILE);
244+
}
245+
246+
return "Translations/" + DEFAULT_LOCALE_FILE;
247+
}
248+
249+
private String normalizeLocaleFileName(String configuredLocale) {
250+
String normalized = configuredLocale == null ? "" : configuredLocale.trim();
251+
if (normalized.isBlank()) {
252+
normalized = DEFAULT_LOCALE_FILE;
253+
}
254+
255+
if (!normalized.toLowerCase(java.util.Locale.ROOT).endsWith(".yml")) {
256+
normalized = normalized + ".yml";
257+
}
258+
259+
return Path.of(normalized).getFileName().toString();
260+
}
261+
262+
private void normalizeLocaleConfig(YamlConfiguration defaults) {
263+
String legacyUsageSingular = "<yellow>Use <white>/_endcrystal debug</white>, <white>reload</white>, or <white>toggle &lt;setting&gt; [true|false]</white>.</yellow>";
264+
String legacyUsagePlural = "<yellow>Use <white>/_endcrystals debug</white>, <white>reload</white>, or <white>toggle &lt;setting&gt; [true|false]</white>.</yellow>";
265+
String legacyUsageBracket = "<yellow>Use <white>/_endcrystals debug</white>, <white>reload</white>, or <white>toggle [setting] [true|false]</white>.</yellow>";
266+
String defaultUsage = defaults.getString("messages.command-usage");
267+
String currentUsage = localeYaml.getString("messages.command-usage");
268+
if ((legacyUsageSingular.equals(currentUsage)
269+
|| legacyUsagePlural.equals(currentUsage)
270+
|| legacyUsageBracket.equals(currentUsage)) && defaultUsage != null) {
271+
localeYaml.set("messages.command-usage", defaultUsage);
272+
}
273+
}
195274
}

src/main/java/com/mrfloris/endcrystals/PluginConfig.java

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import org.bukkit.entity.EntityType;
1313

1414
public record PluginConfig(
15+
String localeName,
1516
List<String> commandAliases,
1617
boolean preventBlockDamage,
1718
boolean preventPlayerBreak,
@@ -82,21 +83,22 @@ public record PluginConfig(
8283
"SPRUCE_CHEST_BOAT"
8384
);
8485

85-
public static PluginConfig from(YamlConfiguration yaml) {
86+
public static PluginConfig from(YamlConfiguration yaml, YamlConfiguration localeYaml, String localeName) {
8687
Map<String, String> messages = new LinkedHashMap<>();
87-
messages.put("no-permission", yaml.getString("messages.no-permission", "<red>You do not have permission to do that.</red>"));
88-
messages.put("reloaded", yaml.getString("messages.reloaded", "<green>Configuration reloaded from <white>%path%</white>.</green>"));
89-
messages.put("toggle-updated", yaml.getString("messages.toggle-updated", "<green><white>%setting%</white> is now <white>%value%</white>.</green>"));
90-
messages.put("unknown-setting", yaml.getString("messages.unknown-setting", "<red>Unknown toggle: <white>%setting%</white></red>"));
91-
messages.put("command-usage", yaml.getString("messages.command-usage", "<yellow>Use <white>/_endcrystals debug</white>, <white>reload</white>, or <white>toggle [setting] [true|false]</white>.</yellow>"));
92-
messages.put("player-break-blocked", yaml.getString("messages.player-break-blocked", "<red>These end crystals are protected.</red>"));
88+
messages.put("no-permission", localeYaml.getString("messages.no-permission", "<red>You do not have permission to do that.</red>"));
89+
messages.put("reloaded", localeYaml.getString("messages.reloaded", "<green>Configuration reloaded from <white>%path%</white>.</green>"));
90+
messages.put("toggle-updated", localeYaml.getString("messages.toggle-updated", "<green><white>%setting%</white> is now <white>%value%</white>.</green>"));
91+
messages.put("unknown-setting", localeYaml.getString("messages.unknown-setting", "<red>Unknown toggle: <white>%setting%</white></red>"));
92+
messages.put("command-usage", localeYaml.getString("messages.command-usage", "<yellow>Use <white>/_endcrystals debug</white>, <white>reload</white>, or <white>toggle [setting] [true|false]</white>.</yellow>"));
93+
messages.put("player-break-blocked", localeYaml.getString("messages.player-break-blocked", "<red>These end crystals are protected.</red>"));
9394

9495
List<String> configuredProtectedTypes = yaml.getStringList("protection.protected-entity-types");
9596
if (configuredProtectedTypes.isEmpty()) {
9697
configuredProtectedTypes = DEFAULT_PROTECTED_ENTITY_TYPES;
9798
}
9899

99100
return new PluginConfig(
101+
localeName,
100102
parseCommandAliases(yaml.isList("commands.aliases")
101103
? yaml.getStringList("commands.aliases")
102104
: DEFAULT_COMMAND_ALIASES),
@@ -110,7 +112,7 @@ public static PluginConfig from(YamlConfiguration yaml) {
110112
yaml.getBoolean("debug.log-crystal-breaks", false),
111113
List.copyOf(yaml.getStringList("live-toggles").isEmpty() ? DEFAULT_LIVE_TOGGLES : yaml.getStringList("live-toggles")),
112114
parseProtectedEntityTypes(configuredProtectedTypes),
113-
yaml.getString("messages.prefix", "<gray>[<gold>1MB-EndCrystals</gold>]</gray> "),
115+
localeYaml.getString("messages.prefix", "<gray>[<gold>1MB-EndCrystals</gold>]</gray> "),
114116
Map.copyOf(messages)
115117
);
116118
}

0 commit comments

Comments
 (0)