Feature/better start (#107)

This commit is contained in:
alexcrea 2026-02-28 15:17:02 +01:00 committed by GitHub
commit f13503f873
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 422 additions and 54 deletions

3
.gitignore vendored
View file

@ -14,6 +14,9 @@
/impl/*/build
/impl/*/.gradle
# run folder
/run/
# other random folders
/htmlReport
/.kotlin/errors

9
.run/Server.run.xml Normal file
View file

@ -0,0 +1,9 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Server" type="JarApplication">
<option name="JAR_PATH" value="$PROJECT_DIR$/run/server.jar" />
<option name="PROGRAM_PARAMETERS" value="-nogui" />
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/run" />
<option name="ALTERNATIVE_JRE_PATH" />
<method v="2" />
</configuration>
</component>

52
COMPATIBILITY.MD Normal file
View file

@ -0,0 +1,52 @@
### Bedrock issue
For server using geyser, bedrock player cannot use custom "recipes" in the anvil.
This is cannot be fixed on geyser or my side.
### Plugin Compatibility
Here is various plugins that had issues with CustomAnvil
where efforts was made for compatibility and should be working right:
some of them are cool I recommend checking them out !
## Supported By CustomAnvil
These plugins have compatibility handled by custom anvil. seek help on custom anvil and do not bother these developers
#### Enchantment plugins
- [ExcellentEnchants](https://www.spigotmc.org/resources/excellentenchants-%E2%AD%90-75-vanilla-like-enchantments.61693/):
Use ExcellentEnchants item type
- [EcoEnchant](https://www.spigotmc.org/resources/ecoenchants-%E2%AD%95-250-enchantments-%E2%9C%85-create-custom-enchants-%E2%9C%A8-essentials-cmi-support.79573/):
Need to use /anvilconfigreload or a server restart to add newly added enchantment.
Use EcoEnchant restriction system but new restriction can be added in custom anvil
- [Enchantment²](https://www.spigotmc.org/resources/enchants-squared-the-enchantsplus-rewrite-custom-enchantments-that-act-like-vanilla-ones.86747/):
Support by Custom Anvil but still experimental. Automatic configuration. Plugin is not actively developed anymore
#### Anvil Mechanics
- [Disenchantment](https://www.spigotmc.org/resources/disenchantment-1-21-1-1-20-6-new-book-splitting-mechanics.110741/)
Partially use Custom Anvil maximum XP settings (>= 6.1.5)
- [HavenBags](https://www.spigotmc.org/resources/havenbags-shulker-like-player-bound-bags-1-17-1-21-4.110420/)
For bag upgrade and skin via anvil. (version >= 1.31.0)
- [AxPlayerWarp](https://modrinth.com/project/QDJHDKvi)
For its anvil inventory usage
- [ToolsStats](https://modrinth.com/project/oBZj9E15)
For token application using anvil
### Known Partially Incompatible
- [UberEnchant](https://modrinth.com/plugin/uberenchant)
Anvil handling as they are doing something similar to CustomAnvil.
It is by no mean there faults and I recomend checking them out
- [SuperEnchant](https://modrinth.com/plugin/superenchants)
Reported potential incompatibility
- [AdvencedEnchantments](https://ae.advancedplugins.net/)
Paid plugin I do not own as I did not get commissioned for support.
may be able to use api but cannot test on my side
If you like Custom Anvil to support a specific plugin (custom enchant or anvil mechanic).
You can ask, but please note implementing compatibility will be considered
as low priority as I work for the plugin as an hobby on my free time for free.

38
CREDITS.MD Normal file
View file

@ -0,0 +1,38 @@
Thanks **DelilahEve** for making [Unsafe Enchants](https://github.com/DelilahEve/UnsafeEnchants). \
CustomAnvil was initially a fork of Unsafe Enchants where I wanted to add more and more and here we are now !
Thanks for all the contributors of bukkit, spigot, the paper team and the adventure API developers \
Thanks JetBrain for making IntelliJ
### Dependencies
Here dependencies are used by custom anvil
- [IF](https://github.com/stefvanschie/IF) an inventory framework by stefvanschie
- [Mockbukkit](https://github.com/MockBukkit/MockBukkit) for unit testing
- [CentralPortalPlus](https://github.com/lalakii/central-portal-plus) by lalakii
- [test-summary-action](https://github.com/jeantessier/test-summary-action) by jeantessier
- [modrinth-publish](https://github.com/cloudnode-pro/modrinth-publish) by Zefir
- [discord-webhook](https://github.com/tsickert/discord-webhook) by tsickert
- Thanks [bstats](https://bstats.org/) for keeping me motivated
- [ModrinthUpdateChecker](https://github.com/Clickism/ModrinthUpdateChecker) by Clickism and thanks to the modrinth team
### Compatibility
Here is to credits all the author of plugins
It partially repeat the the [Compatibility list](https://github.com/alexcrea/CustomAnvil/blob/v1.x.x/COMPATIBILITY.md)
- Big Thanks for H7KZ for [Disenchantment](https://github.com/H7KZ/Disenchantment)
- [Enchantment²](https://www.spigotmc.org/resources/enchants-squared-the-enchantsplus-rewrite-custom-enchantments-that-act-like-vanilla-ones.86747/) by Athlaeos
- [EcoEnchant](https://www.spigotmc.org/resources/ecoenchants-%E2%AD%95-250-enchantments-%E2%9C%85-create-custom-enchants-%E2%9C%A8-essentials-cmi-support.79573/) by Auxilor
- [ExcellentEnchants](https://www.spigotmc.org/resources/excellentenchants-%E2%AD%90-75-vanilla-like-enchantments.61693/) by NightExpress
- [HavenBags](https://www.spigotmc.org/resources/havenbags-shulker-like-player-bound-bags-1-17-1-21-4.110420/) by hyperdefined
- [AxPlayerWarp](https://modrinth.com/project/QDJHDKvi) by ArtillexStudios
- [ToolsStats](https://modrinth.com/project/oBZj9E15) by Valorless
### Special Thanks
Thanks for Microsoft leading me into using a better operating system \
Thanks for all the users trying my plugin for these niche use cases
and for reporting issues and giving ideas !
Thanks coltonj96 for [UberEnchant](https://modrinth.com/plugin/uberenchant).
we may be incompatible with the anvil, but I do think it is a good alternative ! \
I wish one day to work on cross compatibiltiy

View file

@ -4,15 +4,10 @@
It is expected to work on 1.18 to 1.21.7 minecraft servers running spigot or paper.
(the plugin support of 1.16.5 to 1.17.1 is experimental and may encounter issues)
**Custom Anvil** was previously named **Unsafe Enchants+**.
It was renamed because it now affects every anvil aspect and not only unsafe enchants\
**Custom Anvil** is based on [Unsafe Enchants](https://github.com/DelilahEve/UnsafeEnchants) by DelilahEve.
### Download Locations:
the plugin can be downloaded on
[Spigot](https://www.spigotmc.org/resources/custom-anvil.114884),
[modrinth](https://modrinth.com/plugin/customanvil),
[Modrinth](https://modrinth.com/plugin/customanvil),
[Hangar](https://hangar.papermc.io/alexcrea/CustomAnvil)
or here [on GitHub](https://github.com/alexcrea/CustomAnvil/releases/latest)
@ -57,32 +52,7 @@ anvilconfigreload or carl: Reload every config of this plugin
customanvilconfig or configanvil: open a menu for administrator to edit plugin's config in game
```
### Supported Plugins
Custom Anvil can be compatible with some custom enchantments and anvil mechanics plugins.
Here is a list of supported custom enchantment plugins with support status:
- [Enchantment²](https://www.spigotmc.org/resources/enchants-squared-the-enchantsplus-rewrite-custom-enchantments-that-act-like-vanilla-ones.86747/):
Support by Custom Anvil but still experimental. Automatic configuration.
- [EcoEnchant](https://www.spigotmc.org/resources/ecoenchants-%E2%AD%95-250-enchantments-%E2%9C%85-create-custom-enchants-%E2%9C%A8-essentials-cmi-support.79573/):
Support by Custom Anvil but still experimental. Need to use /anvilconfigreload or a server restart to add newly added enchantment.
Use EcoEnchant restriction system by default.
- [ExcellentEnchants](https://www.spigotmc.org/resources/excellentenchants-%E2%AD%90-75-vanilla-like-enchantments.61693/):
Support by Custom Anvil but still experimental. Use ExcellentEnchants item type.
- [Superenchants](https://modrinth.com/plugin/superenchants)
support by Superenchants. Use CustomAnvil to combine enchantment in anvil in survival.
Here is a list of supported anvil mechanic plugins with support status:
- [Disenchantment](https://www.spigotmc.org/resources/disenchantment-1-21-1-1-20-6-new-book-splitting-mechanics.110741/)
support by Custom Anvil but still experimental. Mostly use Custom Anvil basic XP settings. (version >= 6.1.5)
- [HavenBags](https://www.spigotmc.org/resources/havenbags-shulker-like-player-bound-bags-1-17-1-21-4.110420/)
support by Custom Anvil. Not really enchantment related but CustomAnvil should not impact bag upgrade and skin via anvil. (version >= 1.31.0)
If you like Custom Anvil to support a specific plugin (custom enchant or anvil mechanic).
You can ask, but please note implementing compatibility will be considered
as low priority as I work for the plugin on my free time for free.
See the [Compatibility list](https://github.com/alexcrea/CustomAnvil/blob/v1.x.x/COMPATIBILITY.md)
### Overriding Too Expensive
@ -108,9 +78,12 @@ see [Here](https://github.com/alexcrea/CustomAnvil/tree/master/defaultconfigs)
---
Custom anvil [use bstat](https://bstats.org/plugin/bukkit/Unsafe%20Enchants%20Plus/20923) for metric. You can [disable it](https://bstats.org/getting-started) if you like.
### Credits and Thanks
Credits and thanks can be seen [here](https://github.com/alexcrea/CustomAnvil/blob/v1.x.x/CREDITS.md)
### Planned:
- Better Folia support (make gui work. fix some dirty handled parts)
- Get restriction on unknown enchantments
- Get restriction on unknown enchantments (planned for V2)
- More features for custom anvil craft
### Known issue:

View file

@ -0,0 +1,214 @@
/*
* MIT License
*
* Copyright (c) 2025 Clickism
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
* OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
* ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*/
package xyz.alexcrea.cuanvil.update;
import com.google.gson.*;
import org.jetbrains.annotations.Nullable;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import java.util.function.Function;
/**
* Utility class to check for newer versions of a project hosted on Modrinth.
*/
public class ModrinthUpdateChecker {
private static final String API_URL = "https://api.modrinth.com/v2/project/{id}/version";
private final String projectId;
private final String loader;
@Nullable
private final String minecraftVersion;
@Nullable
private Boolean featured = null;
@Nullable
public Consumer<Exception> onError = null;
@Nullable
public Function<String, String> getRawVersion = ModrinthUpdateChecker::getRawVersion;
/**
* Create a new update checker for the given project.
* This will check the latest version for the given loader and any minecraft version.
*
* @param projectId the project ID
* @param loader the loader
*/
public ModrinthUpdateChecker(String projectId, String loader) {
this(projectId, loader, null);
}
/**
* Create a new update checker for the given project.
* This will check the latest version for the given loader and minecraft version.
*
* @param projectId the project ID
* @param loader the loader
* @param minecraftVersion the minecraft version, or null for any version
*/
public ModrinthUpdateChecker(String projectId, String loader, @Nullable String minecraftVersion) {
this.projectId = projectId;
this.loader = loader;
this.minecraftVersion = minecraftVersion;
}
/**
* Check the latest version of the project for the given loader and minecraft version
* and call the consumer with it.
*
* @param consumer the consumer
*/
public void checkVersion(Consumer<String> consumer) {
try {
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(prepareURI())
.GET()
.build();
client.sendAsync(request, HttpResponse.BodyHandlers.ofString())
.thenAcceptAsync(response -> {
if (response.statusCode() != 200) {
if(onError != null)
onError.accept(new RuntimeException("wrong response status code: " + response.statusCode()));
return;
}
JsonArray versionsArray = JsonParser.parseString(response.body()).getAsJsonArray();
String latestVersion = getLatestVersion(versionsArray);
if (latestVersion == null) {
if(onError != null)
onError.accept(new RuntimeException("latest version is null"));
return;
}
consumer.accept(latestVersion);
});
} catch (Exception e) {
if(onError != null) onError.accept(e);
}
}
/**
* Get the latest compatible version from the versions array.
*
* @param versions the versions array
* @return the latest compatible version
*/
@Nullable
protected String getLatestVersion(JsonArray versions) {
return versions.asList().stream().findFirst()
.map(JsonElement::getAsJsonObject)
.map(version -> version.get("version_number").getAsString())
.map(getRawVersion != null ? getRawVersion : (v -> v))
.orElse(null);
}
/**
* Gets the raw version from a version string.
* i.E: "fabric-1.2+1.17.1" -> "1.2"
*
* @param version the version string
* @return the raw version string
*/
public static String getRawVersion(String version) {
if (version.isEmpty()) return version;
version = version.replaceAll("^\\D+", "");
String[] split = version.split("\\+");
return split[0];
}
/**
* Prepare this request uri based on current parameters.
* @return the request uri
*/
private URI prepareURI() {
var url = new StringBuilder(API_URL.replace("{id}", projectId));
var parameters = prepareParameters();
String[] paramArray = new String[parameters.size()];
int i = 0;
for (Map.Entry<String, String> entry : parameters.entrySet()) {
paramArray[i++] = entry.getKey() + '=' + entry.getValue();
}
url.append('?').append(String.join("&", paramArray));
return URI.create(url.toString());
}
/**
* Get the parameters for the version request.
*
* @return a map of key-value map of the request parameters
*/
private Map<String, String> prepareParameters(){
var parameters = new HashMap<String, String>();
parameters.put("loaders", List.of(loader).toString());
if(minecraftVersion != null) parameters.put("game_versions", List.of(minecraftVersion).toString());
if(featured != null) parameters.put("featured", featured.toString());
parameters.put("include_changelog", "false");
return parameters;
}
/**
* Only get featured or non-featured versions.
* Null represent no filter.
* @param featured should be restricted to featured version ? default null if not called
* @return this
*/
public ModrinthUpdateChecker setFeatured(@Nullable Boolean featured) {
this.featured = featured;
return this;
}
/**
* Function called on error calling the api.
* @param onError What should happen on error
* @return this
*/
public ModrinthUpdateChecker setOnError(@Nullable Consumer<Exception> onError) {
this.onError = onError;
return this;
}
/**
* Set the function to get raw version from the modrinth version.
* If null provided raw version will act as in the identity function.
* @param getRawVersion The function transforming modrinth version to raw version
* @return this
*/
public ModrinthUpdateChecker setGetRawVersion(@Nullable Function<String, String> getRawVersion) {
this.getRawVersion = getRawVersion;
return this;
}
}

View file

@ -18,6 +18,7 @@ import xyz.alexcrea.cuanvil.listener.AnvilCloseListener
import xyz.alexcrea.cuanvil.listener.AnvilResultListener
import xyz.alexcrea.cuanvil.listener.ChatEventListener
import xyz.alexcrea.cuanvil.listener.PrepareAnvilListener
import xyz.alexcrea.cuanvil.update.ModrinthUpdateChecker
import xyz.alexcrea.cuanvil.update.PluginSetDefault
import xyz.alexcrea.cuanvil.update.UpdateHandler
import xyz.alexcrea.cuanvil.util.Metrics
@ -31,8 +32,9 @@ import java.util.logging.Level
open class CustomAnvil : JavaPlugin() {
companion object {
// bstats plugin id
// pluginIDS
private const val bstatsPluginId = 20923
private const val modrinthPluginID = "S75Ueiq9"
// Permission string required to use the plugin's features
const val affectedByPluginPermission = "ca.affected"
@ -81,12 +83,91 @@ open class CustomAnvil : JavaPlugin() {
}
// stop plugin if we do not force a dirty start (true by default)
// Return true if start was stopped
private fun tryDirtyStart(): Boolean {
if(!ConfigHolder.DEFAULT_CONFIG.config.getBoolean("dirty_start", false)) {
Bukkit.getPluginManager().disablePlugin(this)
return true
}
return false
}
// stop plugin if we force a safe start (false by default)
// Return true if start was stopped
private fun trySafeStart(): Boolean {
if(ConfigHolder.DEFAULT_CONFIG.config.getBoolean("safe_start", false)) {
Bukkit.getPluginManager().disablePlugin(this)
return true
}
return false
}
/**
* Setup plugin for use
*/
override fun onEnable() {
instance = this
try {
legacyCheck()
} catch (e: Exception) {
logger.log(Level.SEVERE, "error trying to check for legacy system" , e)
if(trySafeStart()) return
}
// Add commands
try {
prepareCommand()
} catch (e: Exception) {
logger.log(Level.SEVERE, "error trying to register commands" , e)
if(trySafeStart()) return
}
// Load default configuration
try {
if(!ConfigHolder.loadDefaultConfig())
throw RuntimeException("Error loading configuration file")
} catch (e: Exception) {
logger.log(Level.SEVERE, "error occurred loading default configuration", e)
if(tryDirtyStart()) return
}
// Load dependency
try {
DependencyManager.loadDependency()
} catch (e: Exception) {
logger.log(Level.SEVERE, "error loading dependency compatibility", e)
if(tryDirtyStart()) return
}
// Register listeners
try {
registerListeners()
} catch (e: Exception) {
logger.log(Level.SEVERE, "error registering listeners", e)
if(tryDirtyStart()) return
}
// Load metrics
try {
Metrics(this, bstatsPluginId)
} catch (_: Exception) {}
// Load other thing later.
// It is so other dependent plugins can implement there event listener before we fire them.
DependencyManager.scheduler.scheduleGlobally(this) { loadEnchantmentSystemDirty() }
}
private fun loadEnchantmentSystemDirty() {
try {
loadEnchantmentSystem()
} catch (e: Exception) {
logger.log(Level.SEVERE, "error initializing enchantment ssytem", e)
tryDirtyStart()
}
}
private fun legacyCheck() {
// Disable old plugin name if exist
val potentialPlugin = Bukkit.getPluginManager().getPlugin("UnsafeEnchantsPlus")
if (potentialPlugin != null) {
@ -95,38 +176,36 @@ open class CustomAnvil : JavaPlugin() {
logger.warning("Please note CustomAnvil is a more recent version of UnsafeEnchantsPlus")
}
if(!PlatformUtil.isPaper) {
val isPaper = PlatformUtil.isPaper
if(!isPaper) {
logger.warning("It seems you are using spigot")
logger.warning("Please take notice that spigot is less supported than paper and derivatives")
}
// Add commands
prepareCommand()
val loader = if(isPaper) "paper" else "spigot"
// Load chat listener
val version = description.version
val featured = if(version.contains("dev")) null else true
ModrinthUpdateChecker(modrinthPluginID, loader, null)
.setFeatured(featured)
.setOnError { logger.log(Level.WARNING, "error trying to fetch latest update", it) }
.checkVersion { latestver: String? ->
if(latestver == null || version.contains(latestver)) return@checkVersion
logger.warning("An update may be available: $latestver")
}
}
private fun registerListeners() {
// Register chat listener
chatListener = ChatEventListener()
server.pluginManager.registerEvents(chatListener, this)
// Load default configuration
if (!ConfigHolder.loadDefaultConfig()) {
logger.log(Level.SEVERE,"could not load default config.")
return
}
// Load dependency
DependencyManager.loadDependency()
// Register anvil events
server.pluginManager.registerEvents(PrepareAnvilListener(), this)
server.pluginManager.registerEvents(AnvilResultListener(), this)
server.pluginManager.registerEvents(AnvilCloseListener(DependencyManager.packetManager), this)
// Load metrics
Metrics(this, bstatsPluginId)
// Load other thing later.
// It is so other dependent plugins can implement there event listener before we fire them.
DependencyManager.scheduler.scheduleGlobally(this, {loadEnchantmentSystem()})
}
private fun loadEnchantmentSystem(){