From 1f239baeb9a3011cde4bf266b59c4c0c5c54fe7e Mon Sep 17 00:00:00 2001 From: Frank Date: Sun, 15 Aug 2021 15:06:36 +0200 Subject: [PATCH] *WIP* Prepared Folder Syncing - Filelist exchange --- gradle.properties | 2 +- src/main/java/ru/bclib/BCLib.java | 1 + src/main/java/ru/bclib/api/WorldDataAPI.java | 61 ++-- .../api/dataexchange/DataExchangeAPI.java | 330 ++++++++++-------- .../handler/AutoFileSyncEntry.java | 3 +- .../dataexchange/handler/DataExchange.java | 58 ++- .../api/dataexchange/handler/HelloClient.java | 38 +- .../api/dataexchange/handler/HelloServer.java | 2 + .../ru/bclib/api/datafixer/DataFixerAPI.java | 18 + 9 files changed, 323 insertions(+), 190 deletions(-) diff --git a/gradle.properties b/gradle.properties index 2b7f0d49..b717718e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -11,7 +11,7 @@ loader_version= 0.11.6 fabric_version = 0.36.1+1.17 # Mod Properties -mod_version = 0.4.0 +mod_version = 0.4.1 maven_group = ru.bclib archives_base_name = bclib diff --git a/src/main/java/ru/bclib/BCLib.java b/src/main/java/ru/bclib/BCLib.java index 681147fd..6ddbe5c7 100644 --- a/src/main/java/ru/bclib/BCLib.java +++ b/src/main/java/ru/bclib/BCLib.java @@ -39,6 +39,7 @@ public class BCLib implements ModInitializer { TagAPI.init(); CraftingRecipes.init(); WorldDataAPI.registerModCache(MOD_ID); + DataExchangeAPI.registerMod(MOD_ID); DataFixerAPI.registerPatch(() -> new BCLibPatch()); DataExchangeAPI.registerDescriptors(List.of( HelloClient.DESCRIPTOR, diff --git a/src/main/java/ru/bclib/api/WorldDataAPI.java b/src/main/java/ru/bclib/api/WorldDataAPI.java index 477807b7..5218f4f2 100644 --- a/src/main/java/ru/bclib/api/WorldDataAPI.java +++ b/src/main/java/ru/bclib/api/WorldDataAPI.java @@ -8,7 +8,6 @@ import net.minecraft.nbt.CompoundTag; import net.minecraft.nbt.NbtIo; import net.minecraft.world.level.storage.LevelStorageSource.LevelStorageAccess; import ru.bclib.BCLib; -import ru.bclib.api.dataexchange.DataExchangeAPI; import ru.bclib.api.datafixer.DataFixerAPI; import java.io.File; @@ -20,8 +19,8 @@ import java.util.function.Consumer; /** * Mod-specifix data-storage for a world. - * - * 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)} * or {@link DataFixerAPI#initializeWorldData(File, boolean)} */ @@ -32,45 +31,47 @@ public class WorldDataAPI { public static void load(File dataDir) { WorldDataAPI.dataDir = dataDir; - MODS.stream().parallel().forEach(modID -> { - File file = new File(dataDir, modID + ".nbt"); - CompoundTag root = new CompoundTag(); - if (file.exists()) { - try { - root = NbtIo.readCompressed(file); - } - catch (IOException e) { - BCLib.LOGGER.error("World data loading failed", e); - } - } - else { - Optional optional = FabricLoader.getInstance().getModContainer(modID); - if (optional.isPresent()) { - ModContainer modContainer = optional.get(); - if (BCLib.isDevEnvironment()) { - root.putString("version", "255.255.9999"); + MODS.stream() + .parallel() + .forEach(modID -> { + File file = new File(dataDir, modID + ".nbt"); + CompoundTag root = new CompoundTag(); + if (file.exists()) { + try { + root = NbtIo.readCompressed(file); } - else { - root.putString("version", modContainer.getMetadata().getVersion().toString()); + catch (IOException e) { + BCLib.LOGGER.error("World data loading failed", e); + } + } + else { + Optional 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. - *

- * Will also register the Mod for the {@link DataExchangeAPI} using {@link DataExchangeAPI#registerMod(String)} * * @param modID - {@link String} modID. */ public static void registerModCache(String modID) { MODS.add(modID); - DataExchangeAPI.registerMod(modID); } /** @@ -117,7 +118,7 @@ public class WorldDataAPI { */ public static void saveFile(String modID) { try { - if (!dataDir.exists()){ + if (!dataDir.exists()) { dataDir.mkdirs(); } NbtIo.writeCompressed(getRootTag(modID), new File(dataDir, modID + ".nbt")); diff --git a/src/main/java/ru/bclib/api/dataexchange/DataExchangeAPI.java b/src/main/java/ru/bclib/api/dataexchange/DataExchangeAPI.java index fb179444..654136f6 100644 --- a/src/main/java/ru/bclib/api/dataexchange/DataExchangeAPI.java +++ b/src/main/java/ru/bclib/api/dataexchange/DataExchangeAPI.java @@ -13,157 +13,181 @@ import java.util.List; import java.util.function.BiConsumer; public class DataExchangeAPI extends DataExchange { - private final static List MODS = Lists.newArrayList(); - - /** - * You should never need to create a custom instance of this Object. - */ - public DataExchangeAPI() { - super(); - } - - @Environment(EnvType.CLIENT) - protected ConnectorClientside clientSupplier(DataExchange api) { - return new ConnectorClientside(api); - } - - protected ConnectorServerside serverSupplier(DataExchange api) { - return new ConnectorServerside(api); - } - - /** - * Register a mod to participate in the DataExchange. - * - * @param modID - {@link String} modID. - */ - public static void registerMod(String modID) { - if (!MODS.contains(modID)) - MODS.add(modID); - } - - /** - * Returns the IDs of all registered Mods. - * - * @return List of modIDs - */ - public static List registeredMods() { - return MODS; - } - - /** - * Add a new Descriptor for a {@link DataHandler}. - * - * @param desc The Descriptor you want to add. - */ - public static void registerDescriptor(DataHandlerDescriptor desc) { - DataExchange api = DataExchange.getInstance(); - api.getDescriptors().add(desc); - } - - /** - * Bulk-Add a Descriptors for your {@link DataHandler}-Objects. - * - * @param desc The Descriptors you want to add. - */ - public static void registerDescriptors(List desc) { - DataExchange api = DataExchange.getInstance(); - api.getDescriptors().addAll(desc); - } - - /** - * Sends the Handler. - *

- * Depending on what the result of {@link DataHandler#getOriginatesOnServer()}, the Data is sent from the server - * 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 - * use this method to add the Data you need to the communication. - * - * @param h The Data that you want to send - */ - public static void send(DataHandler h) { - if (h.getOriginatesOnServer()) { - DataExchangeAPI.getInstance().server.sendToClient(h); - } else { - DataExchangeAPI.getInstance().client.sendToServer(h); - } - } - - /** - * Registers a File for automatic client syncing. - * - * @param modID The ID of the calling Mod - * @param fileName The name of the File - */ - public static void addAutoSyncFile(String modID, File fileName) { - getInstance().addAutoSyncFileData(modID, fileName, false, FileHash.NEED_TRANSFER); - } - - /** - * Registers a File for automatic client syncing. - *

- * The file is synced of the {@link FileHash} on client and server are not equal. This method will not copy the - * configs content from the client to the server. - * - * @param modID The ID of the calling Mod - * @param uniqueID A unique Identifier for the File. (see {@link ru.bclib.api.dataexchange.FileHash#uniqueID} for - * Details - * @param fileName The name of the File - */ - 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. - *

- * The content of the file is requested for comparison. This will copy the - * entire file from the client to the server. - *

- * 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} - * for comparison is sufficient. - * - * @param modID The ID of the calling Mod - * @param fileName The name of the File - * @param needTransfer If the predicate returns true, the file needs to get copied to the server. - */ - public static void addAutoSyncFile(String modID, File fileName, NeedTransferPredicate needTransfer) { - getInstance().addAutoSyncFileData(modID, fileName, true, needTransfer); - } - - /** - * Registers a File for automatic client syncing. - *

- * The content of the file is requested for comparison. This will copy the - * entire file from the client to the server. - *

- * 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} - * for comparison is sufficient. - * - * @param modID The ID of the calling Mod - * @param uniqueID A unique Identifier for the File. (see {@link ru.bclib.api.dataexchange.FileHash#uniqueID} for - * Details - * @param fileName The name of the File - * @param needTransfer If the predicate returns true, the file needs to get copied to the server. - */ - 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. - *

- * This callback is usefull if you need to reload the new content before the game is quit. - * @param callback A Function that receives the AutoSyncID as well as the Filename. - */ - public static void addOnWriteCallback(BiConsumer callback) { - onWriteCallbacks.add(callback); - } - - static { - addOnWriteCallback(Config::reloadSyncedConfig); - } + private final static List MODS = Lists.newArrayList(); + + /** + * You should never need to create a custom instance of this Object. + */ + public DataExchangeAPI() { + super(); + } + + @Environment(EnvType.CLIENT) + protected ConnectorClientside clientSupplier(DataExchange api) { + return new ConnectorClientside(api); + } + + protected ConnectorServerside serverSupplier(DataExchange api) { + return new ConnectorServerside(api); + } + + /** + * Register a mod to participate in the DataExchange. + * + * @param modID - {@link String} modID. + */ + public static void registerMod(String modID) { + if (!MODS.contains(modID)) MODS.add(modID); + } + + /** + * Returns the IDs of all registered Mods. + * + * @return List of modIDs + */ + public static List registeredMods() { + return MODS; + } + + /** + * Add a new Descriptor for a {@link DataHandler}. + * + * @param desc The Descriptor you want to add. + */ + public static void registerDescriptor(DataHandlerDescriptor desc) { + DataExchange api = DataExchange.getInstance(); + api.getDescriptors() + .add(desc); + } + + /** + * Bulk-Add a Descriptors for your {@link DataHandler}-Objects. + * + * @param desc The Descriptors you want to add. + */ + public static void registerDescriptors(List desc) { + DataExchange api = DataExchange.getInstance(); + api.getDescriptors() + .addAll(desc); + } + + /** + * Sends the Handler. + *

+ * Depending on what the result of {@link DataHandler#getOriginatesOnServer()}, the Data is sent from the server + * 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 + * use this method to add the Data you need to the communication. + * + * @param h The Data that you want to send + */ + public static void send(DataHandler h) { + if (h.getOriginatesOnServer()) { + DataExchangeAPI.getInstance().server.sendToClient(h); + } + else { + DataExchangeAPI.getInstance().client.sendToServer(h); + } + } + + /** + * Registers a File for automatic client syncing. + * + * @param modID The ID of the calling Mod + * @param fileName The name of the File + */ + public static void addAutoSyncFile(String modID, File fileName) { + getInstance().addAutoSyncFileData(modID, fileName, false, FileHash.NEED_TRANSFER); + } + + /** + * Registers a File for automatic client syncing. + *

+ * The file is synced of the {@link FileHash} on client and server are not equal. This method will not copy the + * configs content from the client to the server. + * + * @param modID The ID of the calling Mod + * @param uniqueID A unique Identifier for the File. (see {@link ru.bclib.api.dataexchange.FileHash#uniqueID} for + * Details + * @param fileName The name of the File + */ + 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. + *

+ * The content of the file is requested for comparison. This will copy the + * entire file from the client to the server. + *

+ * 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} + * for comparison is sufficient. + * + * @param modID The ID of the calling Mod + * @param fileName The name of the File + * @param needTransfer If the predicate returns true, the file needs to get copied to the server. + */ + public static void addAutoSyncFile(String modID, File fileName, NeedTransferPredicate needTransfer) { + getInstance().addAutoSyncFileData(modID, fileName, true, needTransfer); + } + + /** + * Registers a File for automatic client syncing. + *

+ * The content of the file is requested for comparison. This will copy the + * entire file from the client to the server. + *

+ * 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} + * for comparison is sufficient. + * + * @param modID The ID of the calling Mod + * @param uniqueID A unique Identifier for the File. (see {@link ru.bclib.api.dataexchange.FileHash#uniqueID} for + * Details + * @param fileName The name of the File + * @param needTransfer If the predicate returns true, the file needs to get copied to the server. + */ + 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. + *

+ * This callback is usefull if you need to reload the new content before the game is quit. + * + * @param callback A Function that receives the AutoSyncID as well as the Filename. + */ + public static void addOnWriteCallback(BiConsumer callback) { + onWriteCallbacks.add(callback); + } + + /** + * Returns the sync-folder for a given Mod. + *

+ * 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); + } } diff --git a/src/main/java/ru/bclib/api/dataexchange/handler/AutoFileSyncEntry.java b/src/main/java/ru/bclib/api/dataexchange/handler/AutoFileSyncEntry.java index 2e6236bb..8b07255c 100644 --- a/src/main/java/ru/bclib/api/dataexchange/handler/AutoFileSyncEntry.java +++ b/src/main/java/ru/bclib/api/dataexchange/handler/AutoFileSyncEntry.java @@ -103,6 +103,7 @@ class AutoFileSyncEntry extends AutoSyncID { data = buf.readByteArray(size); return data; } + public static AutoFileSyncEntry findMatching(FileHash hash) { return findMatching(hash.modID, hash.uniqueID); @@ -115,7 +116,7 @@ class AutoFileSyncEntry extends AutoSyncID { public static AutoFileSyncEntry findMatching(String modID, String uniqueID) { return DataExchange .getInstance() - .autoSyncFiles + .getAutoSyncFiles() .stream() .filter(asf -> asf.modID.equals(modID) && asf.uniqueID.equals(uniqueID)) .findFirst() diff --git a/src/main/java/ru/bclib/api/dataexchange/handler/DataExchange.java b/src/main/java/ru/bclib/api/dataexchange/handler/DataExchange.java index 9b15a7c9..7664076c 100644 --- a/src/main/java/ru/bclib/api/dataexchange/handler/DataExchange.java +++ b/src/main/java/ru/bclib/api/dataexchange/handler/DataExchange.java @@ -4,14 +4,17 @@ import net.fabricmc.api.EnvType; import net.fabricmc.api.Environment; import net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents; 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.ConnectorServerside; import ru.bclib.api.dataexchange.DataExchangeAPI; import ru.bclib.api.dataexchange.DataHandler; import ru.bclib.api.dataexchange.DataHandlerDescriptor; import ru.bclib.api.dataexchange.FileHash; +import ru.bclib.config.Configs; import java.io.File; +import java.nio.file.Path; import java.util.ArrayList; import java.util.HashSet; import java.util.List; @@ -19,6 +22,12 @@ import java.util.Set; import java.util.function.BiConsumer; 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 public interface NeedTransferPredicate { public boolean test(FileHash clientHash, FileHash serverHash, FileContentWrapper content); @@ -54,7 +63,9 @@ abstract public class DataExchange { protected ConnectorServerside server; protected ConnectorClientside client; protected final Set descriptors; - protected final List autoSyncFiles = new ArrayList<>(4); + private final List autoSyncFiles = new ArrayList<>(4); + + private boolean didLoadSyncFolder = false; abstract protected ConnectorClientside clientSupplier(DataExchange api); abstract protected ConnectorServerside serverSupplier(DataExchange api); @@ -64,7 +75,11 @@ abstract public class DataExchange { } public Set getDescriptors() { return descriptors; } - + + public List getAutoSyncFiles(){ + return autoSyncFiles; + } + @Environment(EnvType.CLIENT) protected void initClientside(){ if (client!=null) return; @@ -177,4 +192,43 @@ abstract public class DataExchange { static void didReceiveFile(AutoSyncID aid, File file){ onWriteCallbacks.forEach(fkt -> fkt.accept(aid, file)); } + + private List syncFolderContent; + protected List 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()); + } + } + + } + } } diff --git a/src/main/java/ru/bclib/api/dataexchange/handler/HelloClient.java b/src/main/java/ru/bclib/api/dataexchange/handler/HelloClient.java index 4a7af953..5226829b 100644 --- a/src/main/java/ru/bclib/api/dataexchange/handler/HelloClient.java +++ b/src/main/java/ru/bclib/api/dataexchange/handler/HelloClient.java @@ -77,11 +77,11 @@ public class HelloClient extends DataHandler { 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 final List existingAutoSyncFiles = DataExchange .getInstance() - .autoSyncFiles + .getAutoSyncFiles() .stream() .filter(e -> e.fileName.exists()) .collect(Collectors.toList()); @@ -93,9 +93,22 @@ public class HelloClient extends DataHandler { BCLib.LOGGER.info(" - Offering File " + entry); } } else { - BCLib.LOGGER.info("Server will not offer Configs."); + BCLib.LOGGER.info("Server will not offer Files."); 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 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"; @@ -124,6 +137,25 @@ public class HelloClient extends DataHandler { autoSyncedFiles.add(t); //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 files = new ArrayList<>(entries); + for (int j=0; j 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 ≥ v2 + */ + public static boolean isLargerOrEqualVersion(String v1, String v2){ + return getModVersion(v1) >= getModVersion(v2); + } }