*WIP* Prepared Folder Syncing - Filelist exchange

This commit is contained in:
Frank 2021-08-15 15:06:36 +02:00
parent 5df6de1e3a
commit 1f239baeb9
9 changed files with 323 additions and 190 deletions

View file

@ -11,7 +11,7 @@ loader_version= 0.11.6
fabric_version = 0.36.1+1.17 fabric_version = 0.36.1+1.17
# Mod Properties # Mod Properties
mod_version = 0.4.0 mod_version = 0.4.1
maven_group = ru.bclib maven_group = ru.bclib
archives_base_name = bclib archives_base_name = bclib

View file

@ -39,6 +39,7 @@ public class BCLib implements ModInitializer {
TagAPI.init(); TagAPI.init();
CraftingRecipes.init(); CraftingRecipes.init();
WorldDataAPI.registerModCache(MOD_ID); WorldDataAPI.registerModCache(MOD_ID);
DataExchangeAPI.registerMod(MOD_ID);
DataFixerAPI.registerPatch(() -> new BCLibPatch()); DataFixerAPI.registerPatch(() -> new BCLibPatch());
DataExchangeAPI.registerDescriptors(List.of( DataExchangeAPI.registerDescriptors(List.of(
HelloClient.DESCRIPTOR, HelloClient.DESCRIPTOR,

View file

@ -8,7 +8,6 @@ import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.NbtIo; import net.minecraft.nbt.NbtIo;
import net.minecraft.world.level.storage.LevelStorageSource.LevelStorageAccess; import net.minecraft.world.level.storage.LevelStorageSource.LevelStorageAccess;
import ru.bclib.BCLib; import ru.bclib.BCLib;
import ru.bclib.api.dataexchange.DataExchangeAPI;
import ru.bclib.api.datafixer.DataFixerAPI; import ru.bclib.api.datafixer.DataFixerAPI;
import java.io.File; import java.io.File;
@ -20,8 +19,8 @@ import java.util.function.Consumer;
/** /**
* Mod-specifix data-storage for a world. * Mod-specifix data-storage for a world.
* * <p>
* This class provides the ability for mod to store persistent data inside a world. The Storage for the world is * This class provides the ability for mod to store persistent data inside a world. The Storage for the world is
* currently initialized as part of the {@link DataFixerAPI} in {@link DataFixerAPI#fixData(LevelStorageAccess, boolean, Consumer)} * currently initialized as part of the {@link DataFixerAPI} in {@link DataFixerAPI#fixData(LevelStorageAccess, boolean, Consumer)}
* or {@link DataFixerAPI#initializeWorldData(File, boolean)} * or {@link DataFixerAPI#initializeWorldData(File, boolean)}
*/ */
@ -32,45 +31,47 @@ public class WorldDataAPI {
public static void load(File dataDir) { public static void load(File dataDir) {
WorldDataAPI.dataDir = dataDir; WorldDataAPI.dataDir = dataDir;
MODS.stream().parallel().forEach(modID -> { MODS.stream()
File file = new File(dataDir, modID + ".nbt"); .parallel()
CompoundTag root = new CompoundTag(); .forEach(modID -> {
if (file.exists()) { File file = new File(dataDir, modID + ".nbt");
try { CompoundTag root = new CompoundTag();
root = NbtIo.readCompressed(file); if (file.exists()) {
} try {
catch (IOException e) { root = NbtIo.readCompressed(file);
BCLib.LOGGER.error("World data loading failed", e);
}
}
else {
Optional<ModContainer> optional = FabricLoader.getInstance().getModContainer(modID);
if (optional.isPresent()) {
ModContainer modContainer = optional.get();
if (BCLib.isDevEnvironment()) {
root.putString("version", "255.255.9999");
} }
else { catch (IOException e) {
root.putString("version", modContainer.getMetadata().getVersion().toString()); BCLib.LOGGER.error("World data loading failed", e);
}
}
else {
Optional<ModContainer> optional = FabricLoader.getInstance()
.getModContainer(modID);
if (optional.isPresent()) {
ModContainer modContainer = optional.get();
if (BCLib.isDevEnvironment()) {
root.putString("version", "255.255.9999");
}
else {
root.putString("version", modContainer.getMetadata()
.getVersion()
.toString());
}
saveFile(modID);
} }
saveFile(modID);
} }
}
TAGS.put(modID, root); TAGS.put(modID, root);
}); });
} }
/** /**
* Register mod cache, world cache is located in world data folder. * Register mod cache, world cache is located in world data folder.
* <p>
* Will also register the Mod for the {@link DataExchangeAPI} using {@link DataExchangeAPI#registerMod(String)}
* *
* @param modID - {@link String} modID. * @param modID - {@link String} modID.
*/ */
public static void registerModCache(String modID) { public static void registerModCache(String modID) {
MODS.add(modID); MODS.add(modID);
DataExchangeAPI.registerMod(modID);
} }
/** /**
@ -117,7 +118,7 @@ public class WorldDataAPI {
*/ */
public static void saveFile(String modID) { public static void saveFile(String modID) {
try { try {
if (!dataDir.exists()){ if (!dataDir.exists()) {
dataDir.mkdirs(); dataDir.mkdirs();
} }
NbtIo.writeCompressed(getRootTag(modID), new File(dataDir, modID + ".nbt")); NbtIo.writeCompressed(getRootTag(modID), new File(dataDir, modID + ".nbt"));

View file

@ -13,157 +13,181 @@ import java.util.List;
import java.util.function.BiConsumer; import java.util.function.BiConsumer;
public class DataExchangeAPI extends DataExchange { public class DataExchangeAPI extends DataExchange {
private final static List<String> MODS = Lists.newArrayList(); private final static List<String> MODS = Lists.newArrayList();
/** /**
* You should never need to create a custom instance of this Object. * You should never need to create a custom instance of this Object.
*/ */
public DataExchangeAPI() { public DataExchangeAPI() {
super(); super();
} }
@Environment(EnvType.CLIENT) @Environment(EnvType.CLIENT)
protected ConnectorClientside clientSupplier(DataExchange api) { protected ConnectorClientside clientSupplier(DataExchange api) {
return new ConnectorClientside(api); return new ConnectorClientside(api);
} }
protected ConnectorServerside serverSupplier(DataExchange api) { protected ConnectorServerside serverSupplier(DataExchange api) {
return new ConnectorServerside(api); return new ConnectorServerside(api);
} }
/** /**
* Register a mod to participate in the DataExchange. * Register a mod to participate in the DataExchange.
* *
* @param modID - {@link String} modID. * @param modID - {@link String} modID.
*/ */
public static void registerMod(String modID) { public static void registerMod(String modID) {
if (!MODS.contains(modID)) if (!MODS.contains(modID)) MODS.add(modID);
MODS.add(modID); }
}
/**
/** * Returns the IDs of all registered Mods.
* Returns the IDs of all registered Mods. *
* * @return List of modIDs
* @return List of modIDs */
*/ public static List<String> registeredMods() {
public static List<String> registeredMods() { return MODS;
return MODS; }
}
/**
/** * Add a new Descriptor for a {@link DataHandler}.
* Add a new Descriptor for a {@link DataHandler}. *
* * @param desc The Descriptor you want to add.
* @param desc The Descriptor you want to add. */
*/ public static void registerDescriptor(DataHandlerDescriptor desc) {
public static void registerDescriptor(DataHandlerDescriptor desc) { DataExchange api = DataExchange.getInstance();
DataExchange api = DataExchange.getInstance(); api.getDescriptors()
api.getDescriptors().add(desc); .add(desc);
} }
/** /**
* Bulk-Add a Descriptors for your {@link DataHandler}-Objects. * Bulk-Add a Descriptors for your {@link DataHandler}-Objects.
* *
* @param desc The Descriptors you want to add. * @param desc The Descriptors you want to add.
*/ */
public static void registerDescriptors(List<DataHandlerDescriptor> desc) { public static void registerDescriptors(List<DataHandlerDescriptor> desc) {
DataExchange api = DataExchange.getInstance(); DataExchange api = DataExchange.getInstance();
api.getDescriptors().addAll(desc); api.getDescriptors()
} .addAll(desc);
}
/**
* Sends the Handler. /**
* <p> * Sends the Handler.
* Depending on what the result of {@link DataHandler#getOriginatesOnServer()}, the Data is sent from the server * <p>
* to the client (if {@code true}) or the other way around. * Depending on what the result of {@link DataHandler#getOriginatesOnServer()}, the Data is sent from the server
* <p> * to the client (if {@code true}) or the other way around.
* The method {@link DataHandler#serializeData(FriendlyByteBuf)} is called just before the data is sent. You should * <p>
* use this method to add the Data you need to the communication. * The method {@link DataHandler#serializeData(FriendlyByteBuf)} is called just before the data is sent. You should
* * use this method to add the Data you need to the communication.
* @param h The Data that you want to send *
*/ * @param h The Data that you want to send
public static void send(DataHandler h) { */
if (h.getOriginatesOnServer()) { public static void send(DataHandler h) {
DataExchangeAPI.getInstance().server.sendToClient(h); if (h.getOriginatesOnServer()) {
} else { DataExchangeAPI.getInstance().server.sendToClient(h);
DataExchangeAPI.getInstance().client.sendToServer(h); }
} else {
} DataExchangeAPI.getInstance().client.sendToServer(h);
}
/** }
* Registers a File for automatic client syncing.
* /**
* @param modID The ID of the calling Mod * Registers a File for automatic client syncing.
* @param fileName The name of the File *
*/ * @param modID The ID of the calling Mod
public static void addAutoSyncFile(String modID, File fileName) { * @param fileName The name of the File
getInstance().addAutoSyncFileData(modID, fileName, false, FileHash.NEED_TRANSFER); */
} public static void addAutoSyncFile(String modID, File fileName) {
getInstance().addAutoSyncFileData(modID, fileName, false, FileHash.NEED_TRANSFER);
/** }
* Registers a File for automatic client syncing.
* <p> /**
* The file is synced of the {@link FileHash} on client and server are not equal. This method will not copy the * Registers a File for automatic client syncing.
* configs content from the client to the server. * <p>
* * The file is synced of the {@link FileHash} on client and server are not equal. This method will not copy the
* @param modID The ID of the calling Mod * configs content from the client to the server.
* @param uniqueID A unique Identifier for the File. (see {@link ru.bclib.api.dataexchange.FileHash#uniqueID} for *
* Details * @param modID The ID of the calling Mod
* @param fileName The name of the File * @param uniqueID A unique Identifier for the File. (see {@link ru.bclib.api.dataexchange.FileHash#uniqueID} for
*/ * Details
public static void addAutoSyncFile(String modID, String uniqueID, File fileName) { * @param fileName The name of the File
getInstance().addAutoSyncFileData(modID, uniqueID, fileName, false, FileHash.NEED_TRANSFER); */
} public static void addAutoSyncFile(String modID, String uniqueID, File fileName) {
getInstance().addAutoSyncFileData(modID, uniqueID, fileName, false, FileHash.NEED_TRANSFER);
/** }
* Registers a File for automatic client syncing.
* <p> /**
* The content of the file is requested for comparison. This will copy the * Registers a File for automatic client syncing.
* entire file from the client to the server. * <p>
* <p> * The content of the file is requested for comparison. This will copy the
* You should only use this option, if you need to compare parts of the file in order to decide * entire file from the client to the server.
* if the File needs to be copied. Normally using the {@link ru.bclib.api.dataexchange.FileHash} * <p>
* for comparison is sufficient. * You should only use this option, if you need to compare parts of the file in order to decide
* * if the File needs to be copied. Normally using the {@link ru.bclib.api.dataexchange.FileHash}
* @param modID The ID of the calling Mod * for comparison is sufficient.
* @param fileName The name of the File *
* @param needTransfer If the predicate returns true, the file needs to get copied to the server. * @param modID The ID of the calling Mod
*/ * @param fileName The name of the File
public static void addAutoSyncFile(String modID, File fileName, NeedTransferPredicate needTransfer) { * @param needTransfer If the predicate returns true, the file needs to get copied to the server.
getInstance().addAutoSyncFileData(modID, fileName, true, needTransfer); */
} public static void addAutoSyncFile(String modID, File fileName, NeedTransferPredicate needTransfer) {
getInstance().addAutoSyncFileData(modID, fileName, true, needTransfer);
/** }
* Registers a File for automatic client syncing.
* <p> /**
* The content of the file is requested for comparison. This will copy the * Registers a File for automatic client syncing.
* entire file from the client to the server. * <p>
* <p> * The content of the file is requested for comparison. This will copy the
* You should only use this option, if you need to compare parts of the file in order to decide * entire file from the client to the server.
* if the File needs to be copied. Normally using the {@link ru.bclib.api.dataexchange.FileHash} * <p>
* for comparison is sufficient. * You should only use this option, if you need to compare parts of the file in order to decide
* * if the File needs to be copied. Normally using the {@link ru.bclib.api.dataexchange.FileHash}
* @param modID The ID of the calling Mod * for comparison is sufficient.
* @param uniqueID A unique Identifier for the File. (see {@link ru.bclib.api.dataexchange.FileHash#uniqueID} for *
* Details * @param modID The ID of the calling Mod
* @param fileName The name of the File * @param uniqueID A unique Identifier for the File. (see {@link ru.bclib.api.dataexchange.FileHash#uniqueID} for
* @param needTransfer If the predicate returns true, the file needs to get copied to the server. * Details
*/ * @param fileName The name of the File
public static void addAutoSyncFile(String modID, String uniqueID, File fileName, NeedTransferPredicate needTransfer) { * @param needTransfer If the predicate returns true, the file needs to get copied to the server.
getInstance().addAutoSyncFileData(modID, uniqueID, fileName, true, needTransfer); */
} public static void addAutoSyncFile(String modID, String uniqueID, File fileName, NeedTransferPredicate needTransfer) {
getInstance().addAutoSyncFileData(modID, uniqueID, fileName, true, needTransfer);
/** }
* Register a function that is called whenever the client receives a file from the server and replaced toe local
* file with the new content. /**
* <p> * Register a function that is called whenever the client receives a file from the server and replaced toe local
* This callback is usefull if you need to reload the new content before the game is quit. * file with the new content.
* @param callback A Function that receives the AutoSyncID as well as the Filename. * <p>
*/ * This callback is usefull if you need to reload the new content before the game is quit.
public static void addOnWriteCallback(BiConsumer<AutoSyncID, File> callback) { *
onWriteCallbacks.add(callback); * @param callback A Function that receives the AutoSyncID as well as the Filename.
} */
public static void addOnWriteCallback(BiConsumer<AutoSyncID, File> callback) {
static { onWriteCallbacks.add(callback);
addOnWriteCallback(Config::reloadSyncedConfig); }
}
/**
* Returns the sync-folder for a given Mod.
* <p>
* BCLib will ensure that the contents of sync-folder on the client is the same as the one on the server.
*
* @param modID ID of the Mod
* @return The path to the sync-folder
*/
public static File getSyncFolder(String modID) {
File fl = SYNC_FOLDER.resolve(modID.replace(".", "-")
.replace(":", "-")
.replace("\\", "-")
.replace("/", "-"))
.toFile();
if (!fl.exists()){
fl.mkdirs();
}
return fl;
}
static {
addOnWriteCallback(Config::reloadSyncedConfig);
}
} }

View file

@ -103,6 +103,7 @@ class AutoFileSyncEntry extends AutoSyncID {
data = buf.readByteArray(size); data = buf.readByteArray(size);
return data; return data;
} }
public static AutoFileSyncEntry findMatching(FileHash hash) { public static AutoFileSyncEntry findMatching(FileHash hash) {
return findMatching(hash.modID, hash.uniqueID); return findMatching(hash.modID, hash.uniqueID);
@ -115,7 +116,7 @@ class AutoFileSyncEntry extends AutoSyncID {
public static AutoFileSyncEntry findMatching(String modID, String uniqueID) { public static AutoFileSyncEntry findMatching(String modID, String uniqueID) {
return DataExchange return DataExchange
.getInstance() .getInstance()
.autoSyncFiles .getAutoSyncFiles()
.stream() .stream()
.filter(asf -> asf.modID.equals(modID) && asf.uniqueID.equals(uniqueID)) .filter(asf -> asf.modID.equals(modID) && asf.uniqueID.equals(uniqueID))
.findFirst() .findFirst()

View file

@ -4,14 +4,17 @@ import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment; import net.fabricmc.api.Environment;
import net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents; import net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents;
import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents; import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents;
import net.fabricmc.loader.api.FabricLoader;
import ru.bclib.api.dataexchange.ConnectorClientside; import ru.bclib.api.dataexchange.ConnectorClientside;
import ru.bclib.api.dataexchange.ConnectorServerside; import ru.bclib.api.dataexchange.ConnectorServerside;
import ru.bclib.api.dataexchange.DataExchangeAPI; import ru.bclib.api.dataexchange.DataExchangeAPI;
import ru.bclib.api.dataexchange.DataHandler; import ru.bclib.api.dataexchange.DataHandler;
import ru.bclib.api.dataexchange.DataHandlerDescriptor; import ru.bclib.api.dataexchange.DataHandlerDescriptor;
import ru.bclib.api.dataexchange.FileHash; import ru.bclib.api.dataexchange.FileHash;
import ru.bclib.config.Configs;
import java.io.File; import java.io.File;
import java.nio.file.Path;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
@ -19,6 +22,12 @@ import java.util.Set;
import java.util.function.BiConsumer; import java.util.function.BiConsumer;
abstract public class DataExchange { abstract public class DataExchange {
public final static Path SYNC_FOLDER = FabricLoader.getInstance()
.getGameDir()
.resolve("bclib-sync")
.toAbsolutePath();
public final static String SYNC_FOLDER_ID = "BCLIB-SYNC";
@FunctionalInterface @FunctionalInterface
public interface NeedTransferPredicate { public interface NeedTransferPredicate {
public boolean test(FileHash clientHash, FileHash serverHash, FileContentWrapper content); public boolean test(FileHash clientHash, FileHash serverHash, FileContentWrapper content);
@ -54,7 +63,9 @@ abstract public class DataExchange {
protected ConnectorServerside server; protected ConnectorServerside server;
protected ConnectorClientside client; protected ConnectorClientside client;
protected final Set<DataHandlerDescriptor> descriptors; protected final Set<DataHandlerDescriptor> descriptors;
protected final List<AutoFileSyncEntry> autoSyncFiles = new ArrayList<>(4); private final List<AutoFileSyncEntry> autoSyncFiles = new ArrayList<>(4);
private boolean didLoadSyncFolder = false;
abstract protected ConnectorClientside clientSupplier(DataExchange api); abstract protected ConnectorClientside clientSupplier(DataExchange api);
abstract protected ConnectorServerside serverSupplier(DataExchange api); abstract protected ConnectorServerside serverSupplier(DataExchange api);
@ -64,7 +75,11 @@ abstract public class DataExchange {
} }
public Set<DataHandlerDescriptor> getDescriptors() { return descriptors; } public Set<DataHandlerDescriptor> getDescriptors() { return descriptors; }
public List<AutoFileSyncEntry> getAutoSyncFiles(){
return autoSyncFiles;
}
@Environment(EnvType.CLIENT) @Environment(EnvType.CLIENT)
protected void initClientside(){ protected void initClientside(){
if (client!=null) return; if (client!=null) return;
@ -177,4 +192,43 @@ abstract public class DataExchange {
static void didReceiveFile(AutoSyncID aid, File file){ static void didReceiveFile(AutoSyncID aid, File file){
onWriteCallbacks.forEach(fkt -> fkt.accept(aid, file)); onWriteCallbacks.forEach(fkt -> fkt.accept(aid, file));
} }
private List<String> syncFolderContent;
protected List<String> getSyncFolderContent(){
if (syncFolderContent==null){
return new ArrayList<>(0);
}
return syncFolderContent;
}
//we call this from HelloServer to prepare transfer
protected void loadSyncFolder() {
if (Configs.MAIN_CONFIG.getBoolean(Configs.MAIN_SYNC_CATEGORY, "offserSyncFolder", true))
{
final File syncPath = SYNC_FOLDER.toFile();
if (!syncPath.exists()) {
syncPath.mkdirs();
}
if (syncFolderContent == null) {
syncFolderContent = new ArrayList<>(8);
addFilesForSyncFolder(syncPath);
}
}
}
private void addFilesForSyncFolder(File path){
for (final File f : path.listFiles()) {
if (f.isDirectory()) {
addFilesForSyncFolder(f);
} else if (f.isFile()) {
if (!f.getName().startsWith(".")) {
Path p = f.toPath();
p = SYNC_FOLDER.relativize(p);
syncFolderContent.add(p.toString());
}
}
}
}
} }

View file

@ -77,11 +77,11 @@ public class HelloClient extends DataHandler {
buf.writeInt(0); buf.writeInt(0);
} }
if (Configs.MAIN_CONFIG.getBoolean(Configs.MAIN_SYNC_CATEGORY, "offerConfigs", true)) { if (Configs.MAIN_CONFIG.getBoolean(Configs.MAIN_SYNC_CATEGORY, "offerFiles", true)) {
//do only include files that exist on the server //do only include files that exist on the server
final List<AutoFileSyncEntry> existingAutoSyncFiles = DataExchange final List<AutoFileSyncEntry> existingAutoSyncFiles = DataExchange
.getInstance() .getInstance()
.autoSyncFiles .getAutoSyncFiles()
.stream() .stream()
.filter(e -> e.fileName.exists()) .filter(e -> e.fileName.exists())
.collect(Collectors.toList()); .collect(Collectors.toList());
@ -93,9 +93,22 @@ public class HelloClient extends DataHandler {
BCLib.LOGGER.info(" - Offering File " + entry); BCLib.LOGGER.info(" - Offering File " + entry);
} }
} else { } else {
BCLib.LOGGER.info("Server will not offer Configs."); BCLib.LOGGER.info("Server will not offer Files.");
buf.writeInt(0); buf.writeInt(0);
} }
//for the moment this is only hardcoded for the sync-folder offered by BCLIB, but it can be extended in future
if (Configs.MAIN_CONFIG.getBoolean(Configs.MAIN_SYNC_CATEGORY, "offserSyncFolder", true)) {
buf.writeInt(1); //currently we do only sync a single folder
writeString(buf, DataExchange.SYNC_FOLDER_ID); //the UID of the Folder
final List<String> fileNames = DataExchange.getInstance().getSyncFolderContent();
buf.writeInt(fileNames.size());
fileNames.forEach(fl -> writeString(buf, fl));
} else {
BCLib.LOGGER.info("Server will not offer Sync Folders.");
buf.writeInt(0);
}
Configs.MAIN_CONFIG.saveChanges();
} }
String bclibVersion ="0.0.0"; String bclibVersion ="0.0.0";
@ -124,6 +137,25 @@ public class HelloClient extends DataHandler {
autoSyncedFiles.add(t); autoSyncedFiles.add(t);
//System.out.println(t.first); //System.out.println(t.first);
} }
//since this version we also send the sync folders
if (DataFixerAPI.isLargerOrEqualVersion(bclibVersion, "0.4.1")) {
final int folderCount = buf.readInt();
for (int i=0; i<folderCount; i++){
final String folderID = readString(buf);
final int entries = buf.readInt();
List<String> files = new ArrayList<>(entries);
for (int j=0; j<entries; j++){
files.add(readString(buf));
}
if (folderID.equals(DataExchange.SYNC_FOLDER_ID)) {
//TODO: implement the syncing here
} else {
BCLib.LOGGER.warning("Unknown Sync-Folder '"+folderID+"'");
}
}
}
} }

View file

@ -84,5 +84,7 @@ public class HelloServer extends DataHandler {
} else { } else {
BCLib.LOGGER.info("Auto-Sync was disabled on the server."); BCLib.LOGGER.info("Auto-Sync was disabled on the server.");
} }
DataExchange.getInstance().loadSyncFolder();
} }
} }

View file

@ -537,4 +537,22 @@ public class DataFixerAPI {
return String.format(Locale.ROOT, "%d.%d.%d", a, b, c); return String.format(Locale.ROOT, "%d.%d.%d", a, b, c);
} }
/**
* {@code true} if the version v1 is larger than v2
* @param v1 A Version string
* @param v2 Another Version string
* @return v1 &gt; v2
*/
public static boolean isLargerVersion(String v1, String v2){
return getModVersion(v1) > getModVersion(v2);
}
/**
* {@code true} if the version v1 is larger or equal v2
* @param v1 A Version string
* @param v2 Another Version string
* @return v1 &ge; v2
*/
public static boolean isLargerOrEqualVersion(String v1, String v2){
return getModVersion(v1) >= getModVersion(v2);
}
} }