Reorganized Imports/Packages

This commit is contained in:
Frank 2022-05-18 23:56:23 +02:00
parent cb9459f176
commit 3ee10482ab
721 changed files with 34873 additions and 33558 deletions

View file

@ -0,0 +1,109 @@
package org.betterx.bclib.api.dataexchange;
import net.minecraft.client.Minecraft;
import net.minecraft.client.multiplayer.ClientPacketListener;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.server.network.ServerGamePacketListenerImpl;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
import net.fabricmc.fabric.api.networking.v1.PacketSender;
import java.nio.charset.StandardCharsets;
import java.util.Objects;
import org.jetbrains.annotations.NotNull;
public abstract class BaseDataHandler {
private final boolean originatesOnServer;
@NotNull
private final ResourceLocation identifier;
protected BaseDataHandler(ResourceLocation identifier, boolean originatesOnServer) {
this.originatesOnServer = originatesOnServer;
this.identifier = identifier;
}
final public boolean getOriginatesOnServer() {
return originatesOnServer;
}
final public ResourceLocation getIdentifier() {
return identifier;
}
@Environment(EnvType.CLIENT)
abstract void receiveFromServer(Minecraft client,
ClientPacketListener handler,
FriendlyByteBuf buf,
PacketSender responseSender);
private ServerPlayer lastMessageSender;
void receiveFromClient(MinecraftServer server,
ServerPlayer player,
ServerGamePacketListenerImpl handler,
FriendlyByteBuf buf,
PacketSender responseSender) {
lastMessageSender = player;
}
final protected boolean reply(BaseDataHandler message, MinecraftServer server) {
if (lastMessageSender == null) return false;
message.sendToClient(server, lastMessageSender);
return true;
}
abstract void sendToClient(MinecraftServer server);
abstract void sendToClient(MinecraftServer server, ServerPlayer player);
@Environment(EnvType.CLIENT)
abstract void sendToServer(Minecraft client);
protected boolean isBlocking() {
return false;
}
@Override
public String toString() {
return "BasDataHandler{" + "originatesOnServer=" + originatesOnServer + ", identifier=" + identifier + '}';
}
/**
* Write a String to a buffer (Convenience Method)
*
* @param buf The buffer to write to
* @param s The String you want to write
*/
public static void writeString(FriendlyByteBuf buf, String s) {
buf.writeByteArray(s.getBytes(StandardCharsets.UTF_8));
}
/**
* Read a string from a buffer (Convenience Method)
*
* @param buf Thea buffer to read from
* @return The received String
*/
public static String readString(FriendlyByteBuf buf) {
byte[] data = buf.readByteArray();
return new String(data, StandardCharsets.UTF_8);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof BaseDataHandler)) return false;
BaseDataHandler that = (BaseDataHandler) o;
return originatesOnServer == that.originatesOnServer && identifier.equals(that.identifier);
}
@Override
public int hashCode() {
return Objects.hash(originatesOnServer, identifier);
}
}

View file

@ -0,0 +1,19 @@
package org.betterx.bclib.api.dataexchange;
import org.betterx.bclib.api.dataexchange.handler.DataExchange;
import java.util.Set;
abstract class Connector {
protected final DataExchange api;
Connector(DataExchange api) {
this.api = api;
}
public abstract boolean onClient();
protected Set<DataHandlerDescriptor> getDescriptors() {
return api.getDescriptors();
}
}

View file

@ -0,0 +1,77 @@
package org.betterx.bclib.api.dataexchange;
import net.minecraft.client.Minecraft;
import net.minecraft.client.multiplayer.ClientPacketListener;
import net.minecraft.network.FriendlyByteBuf;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking;
import net.fabricmc.fabric.api.networking.v1.PacketSender;
import org.betterx.bclib.BCLib;
import org.betterx.bclib.api.dataexchange.handler.DataExchange;
/**
* This is an internal class that handles a Clienetside players Connection to a Server
*/
@Environment(EnvType.CLIENT)
public class ConnectorClientside extends Connector {
private Minecraft client;
ConnectorClientside(DataExchange api) {
super(api);
this.client = null;
}
@Override
public boolean onClient() {
return true;
}
public void onPlayInit(ClientPacketListener handler, Minecraft client) {
if (this.client != null && this.client != client) {
BCLib.LOGGER.warning("Client changed!");
}
this.client = client;
for (DataHandlerDescriptor desc : getDescriptors()) {
ClientPlayNetworking.registerReceiver(desc.IDENTIFIER, (_client, _handler, _buf, _responseSender) -> {
receiveFromServer(desc, _client, _handler, _buf, _responseSender);
});
}
}
public void onPlayReady(ClientPacketListener handler, PacketSender sender, Minecraft client) {
for (DataHandlerDescriptor desc : getDescriptors()) {
if (desc.sendOnJoin) {
BaseDataHandler h = desc.JOIN_INSTANCE.get();
if (!h.getOriginatesOnServer()) {
h.sendToServer(client);
}
}
}
}
public void onPlayDisconnect(ClientPacketListener handler, Minecraft client) {
for (DataHandlerDescriptor desc : getDescriptors()) {
ClientPlayNetworking.unregisterReceiver(desc.IDENTIFIER);
}
}
void receiveFromServer(DataHandlerDescriptor desc,
Minecraft client,
ClientPacketListener handler,
FriendlyByteBuf buf,
PacketSender responseSender) {
BaseDataHandler h = desc.INSTANCE.get();
h.receiveFromServer(client, handler, buf, responseSender);
}
public void sendToServer(BaseDataHandler h) {
if (client == null) {
throw new RuntimeException("[internal error] Client not initialized yet!");
}
h.sendToServer(this.client);
}
}

View file

@ -0,0 +1,82 @@
package org.betterx.bclib.api.dataexchange;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.server.network.ServerGamePacketListenerImpl;
import net.fabricmc.fabric.api.networking.v1.PacketSender;
import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking;
import org.betterx.bclib.BCLib;
import org.betterx.bclib.api.dataexchange.handler.DataExchange;
/**
* This is an internal class that handles a Serverside Connection to a Client-Player
*/
public class ConnectorServerside extends Connector {
private MinecraftServer server;
ConnectorServerside(DataExchange api) {
super(api);
server = null;
}
@Override
public boolean onClient() {
return false;
}
public void onPlayInit(ServerGamePacketListenerImpl handler, MinecraftServer server) {
if (this.server != null && this.server != server) {
BCLib.LOGGER.warning("Server changed!");
}
this.server = server;
for (DataHandlerDescriptor desc : getDescriptors()) {
ServerPlayNetworking.registerReceiver(handler,
desc.IDENTIFIER,
(_server, _player, _handler, _buf, _responseSender) -> {
receiveFromClient(desc,
_server,
_player,
_handler,
_buf,
_responseSender);
});
}
}
public void onPlayReady(ServerGamePacketListenerImpl handler, PacketSender sender, MinecraftServer server) {
for (DataHandlerDescriptor desc : getDescriptors()) {
if (desc.sendOnJoin) {
BaseDataHandler h = desc.JOIN_INSTANCE.get();
if (h.getOriginatesOnServer()) {
h.sendToClient(server, handler.player);
}
}
}
}
public void onPlayDisconnect(ServerGamePacketListenerImpl handler, MinecraftServer server) {
for (DataHandlerDescriptor desc : getDescriptors()) {
ServerPlayNetworking.unregisterReceiver(handler, desc.IDENTIFIER);
}
}
void receiveFromClient(DataHandlerDescriptor desc,
MinecraftServer server,
ServerPlayer player,
ServerGamePacketListenerImpl handler,
FriendlyByteBuf buf,
PacketSender responseSender) {
BaseDataHandler h = desc.INSTANCE.get();
h.receiveFromClient(server, player, handler, buf, responseSender);
}
public void sendToClient(BaseDataHandler h) {
if (server == null) {
throw new RuntimeException("[internal error] Server not initialized yet!");
}
h.sendToClient(this.server);
}
}

View file

@ -0,0 +1,214 @@
package org.betterx.bclib.api.dataexchange;
import net.minecraft.network.FriendlyByteBuf;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
import com.google.common.collect.Lists;
import org.betterx.bclib.BCLib;
import org.betterx.bclib.api.dataexchange.handler.DataExchange;
import org.betterx.bclib.api.dataexchange.handler.autosync.AutoSync;
import org.betterx.bclib.api.dataexchange.handler.autosync.AutoSyncID;
import org.betterx.bclib.config.Config;
import org.betterx.bclib.util.ModUtil;
import java.io.File;
import java.util.List;
import java.util.function.BiConsumer;
public class DataExchangeAPI extends DataExchange {
private final static List<String> 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);
}
/**
* Register a mod dependency to participate in the DataExchange.
*
* @param modID - {@link String} modID.
*/
public static void registerModDependency(String modID) {
if (ModUtil.getModInfo(modID, false) != null && !"0.0.0".equals(ModUtil.getModVersion(modID))) {
registerMod(modID);
} else {
BCLib.LOGGER.info("Mod Dependency '" + modID + "' not found. This is probably OK.");
}
}
/**
* Returns the IDs of all registered Mods.
*
* @return List of modIDs
*/
public static List<String> 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<DataHandlerDescriptor> desc) {
DataExchange api = DataExchange.getInstance();
api.getDescriptors()
.addAll(desc);
}
/**
* Sends the Handler.
* <p>
* 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.
* <p>
* The method {@link DataHandler#serializeData(FriendlyByteBuf, boolean)} 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(BaseDataHandler 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) {
AutoSync.addAutoSyncFileData(modID, fileName, false, SyncFileHash.NEED_TRANSFER);
}
/**
* Registers a File for automatic client syncing.
* <p>
* The file is synced of the {@link SyncFileHash} 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 SyncFileHash#uniqueID} for
* Details
* @param fileName The name of the File
*/
public static void addAutoSyncFile(String modID, String uniqueID, File fileName) {
AutoSync.addAutoSyncFileData(modID, uniqueID, fileName, false, SyncFileHash.NEED_TRANSFER);
}
/**
* Registers a File for automatic client syncing.
* <p>
* The content of the file is requested for comparison. This will copy the
* entire file from the client to the server.
* <p>
* 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 SyncFileHash}
* 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, AutoSync.NeedTransferPredicate needTransfer) {
AutoSync.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
* entire file from the client to the server.
* <p>
* 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 SyncFileHash}
* for comparison is sufficient.
*
* @param modID The ID of the calling Mod
* @param uniqueID A unique Identifier for the File. (see {@link SyncFileHash#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,
AutoSync.NeedTransferPredicate needTransfer) {
AutoSync.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>
* 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<AutoSyncID, File> callback) {
AutoSync.addOnWriteCallback(callback);
}
/**
* 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 getModSyncFolder(String modID) {
File fl = AutoSync.SYNC_FOLDER.localFolder.resolve(modID.replace(".", "-")
.replace(":", "-")
.replace("\\", "-")
.replace("/", "-"))
.normalize()
.toFile();
if (!fl.exists()) {
fl.mkdirs();
}
return fl;
}
static {
addOnWriteCallback(Config::reloadSyncedConfig);
}
}

View file

@ -0,0 +1,315 @@
package org.betterx.bclib.api.dataexchange;
import net.minecraft.client.Minecraft;
import net.minecraft.client.multiplayer.ClientPacketListener;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.server.network.ServerGamePacketListenerImpl;
import net.minecraft.world.entity.player.Player;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking;
import net.fabricmc.fabric.api.networking.v1.PacketByteBufs;
import net.fabricmc.fabric.api.networking.v1.PacketSender;
import net.fabricmc.fabric.api.networking.v1.PlayerLookup;
import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking;
import org.betterx.bclib.BCLib;
import org.betterx.bclib.api.dataexchange.handler.autosync.Chunker;
import org.betterx.bclib.api.dataexchange.handler.autosync.Chunker.PacketChunkSender;
import java.util.Collection;
import java.util.List;
public abstract class DataHandler extends BaseDataHandler {
public abstract static class WithoutPayload extends DataHandler {
protected WithoutPayload(ResourceLocation identifier, boolean originatesOnServer) {
super(identifier, originatesOnServer);
}
@Override
protected boolean prepareData(boolean isClient) {
return true;
}
@Override
protected void serializeData(FriendlyByteBuf buf, boolean isClient) {
}
@Override
protected void deserializeIncomingData(FriendlyByteBuf buf, PacketSender responseSender, boolean isClient) {
}
}
protected DataHandler(ResourceLocation identifier, boolean originatesOnServer) {
super(identifier, originatesOnServer);
}
protected boolean prepareData(boolean isClient) {
return true;
}
abstract protected void serializeData(FriendlyByteBuf buf, boolean isClient);
abstract protected void deserializeIncomingData(FriendlyByteBuf buf, PacketSender responseSender, boolean isClient);
abstract protected void runOnGameThread(Minecraft client, MinecraftServer server, boolean isClient);
@Environment(EnvType.CLIENT)
@Override
void receiveFromServer(Minecraft client,
ClientPacketListener handler,
FriendlyByteBuf buf,
PacketSender responseSender) {
deserializeIncomingData(buf, responseSender, true);
final Runnable runner = () -> runOnGameThread(client, null, true);
if (isBlocking()) client.executeBlocking(runner);
else client.execute(runner);
}
@Override
void receiveFromClient(MinecraftServer server,
ServerPlayer player,
ServerGamePacketListenerImpl handler,
FriendlyByteBuf buf,
PacketSender responseSender) {
super.receiveFromClient(server, player, handler, buf, responseSender);
deserializeIncomingData(buf, responseSender, false);
final Runnable runner = () -> runOnGameThread(null, server, false);
if (isBlocking()) server.executeBlocking(runner);
else server.execute(runner);
}
@Override
void sendToClient(MinecraftServer server) {
if (prepareData(false)) {
FriendlyByteBuf buf = PacketByteBufs.create();
serializeData(buf, false);
_sendToClient(getIdentifier(), server, PlayerLookup.all(server), buf);
}
}
@Override
void sendToClient(MinecraftServer server, ServerPlayer player) {
if (prepareData(false)) {
FriendlyByteBuf buf = PacketByteBufs.create();
serializeData(buf, false);
_sendToClient(getIdentifier(), server, List.of(player), buf);
}
}
public static void _sendToClient(ResourceLocation identifier,
MinecraftServer server,
Collection<ServerPlayer> players,
FriendlyByteBuf buf) {
if (buf.readableBytes() > Chunker.MAX_PACKET_SIZE) {
final PacketChunkSender sender = new PacketChunkSender(buf, identifier);
sender.sendChunks(players);
} else {
for (ServerPlayer player : players) {
ServerPlayNetworking.send(player, identifier, buf);
}
}
}
@Environment(EnvType.CLIENT)
@Override
void sendToServer(Minecraft client) {
if (prepareData(true)) {
FriendlyByteBuf buf = PacketByteBufs.create();
serializeData(buf, true);
ClientPlayNetworking.send(getIdentifier(), buf);
}
}
/**
* A Message that always originates on the Client
*/
public abstract static class FromClient extends BaseDataHandler {
public abstract static class WithoutPayload extends FromClient {
protected WithoutPayload(ResourceLocation identifier) {
super(identifier);
}
@Override
protected boolean prepareDataOnClient() {
return true;
}
@Override
protected void serializeDataOnClient(FriendlyByteBuf buf) {
}
@Override
protected void deserializeIncomingDataOnServer(FriendlyByteBuf buf,
Player player,
PacketSender responseSender) {
}
}
protected FromClient(ResourceLocation identifier) {
super(identifier, false);
}
@Environment(EnvType.CLIENT)
protected boolean prepareDataOnClient() {
return true;
}
@Environment(EnvType.CLIENT)
abstract protected void serializeDataOnClient(FriendlyByteBuf buf);
protected abstract void deserializeIncomingDataOnServer(FriendlyByteBuf buf,
Player player,
PacketSender responseSender);
protected abstract void runOnServerGameThread(MinecraftServer server, Player player);
@Environment(EnvType.CLIENT)
@Override
void receiveFromServer(Minecraft client,
ClientPacketListener handler,
FriendlyByteBuf buf,
PacketSender responseSender) {
BCLib.LOGGER.error("[Internal Error] The message '" + getIdentifier() + "' must originate from the client!");
}
@Override
void receiveFromClient(MinecraftServer server,
ServerPlayer player,
ServerGamePacketListenerImpl handler,
FriendlyByteBuf buf,
PacketSender responseSender) {
super.receiveFromClient(server, player, handler, buf, responseSender);
deserializeIncomingDataOnServer(buf, player, responseSender);
final Runnable runner = () -> runOnServerGameThread(server, player);
if (isBlocking()) server.executeBlocking(runner);
else server.execute(runner);
}
@Override
void sendToClient(MinecraftServer server) {
BCLib.LOGGER.error("[Internal Error] The message '" + getIdentifier() + "' must originate from the client!");
}
@Override
void sendToClient(MinecraftServer server, ServerPlayer player) {
BCLib.LOGGER.error("[Internal Error] The message '" + getIdentifier() + "' must originate from the client!");
}
@Environment(EnvType.CLIENT)
@Override
void sendToServer(Minecraft client) {
if (prepareDataOnClient()) {
FriendlyByteBuf buf = PacketByteBufs.create();
serializeDataOnClient(buf);
ClientPlayNetworking.send(getIdentifier(), buf);
}
}
}
/**
* A Message that always originates on the Server
*/
public abstract static class FromServer extends BaseDataHandler {
public abstract static class WithoutPayload extends FromServer {
protected WithoutPayload(ResourceLocation identifier) {
super(identifier);
}
@Override
protected boolean prepareDataOnServer() {
return true;
}
@Override
protected void serializeDataOnServer(FriendlyByteBuf buf) {
}
@Override
protected void deserializeIncomingDataOnClient(FriendlyByteBuf buf, PacketSender responseSender) {
}
}
protected FromServer(ResourceLocation identifier) {
super(identifier, true);
}
protected boolean prepareDataOnServer() {
return true;
}
abstract protected void serializeDataOnServer(FriendlyByteBuf buf);
@Environment(EnvType.CLIENT)
abstract protected void deserializeIncomingDataOnClient(FriendlyByteBuf buf, PacketSender responseSender);
@Environment(EnvType.CLIENT)
abstract protected void runOnClientGameThread(Minecraft client);
@Environment(EnvType.CLIENT)
@Override
final void receiveFromServer(Minecraft client,
ClientPacketListener handler,
FriendlyByteBuf buf,
PacketSender responseSender) {
deserializeIncomingDataOnClient(buf, responseSender);
final Runnable runner = () -> runOnClientGameThread(client);
if (isBlocking()) client.executeBlocking(runner);
else client.execute(runner);
}
@Override
final void receiveFromClient(MinecraftServer server,
ServerPlayer player,
ServerGamePacketListenerImpl handler,
FriendlyByteBuf buf,
PacketSender responseSender) {
super.receiveFromClient(server, player, handler, buf, responseSender);
BCLib.LOGGER.error("[Internal Error] The message '" + getIdentifier() + "' must originate from the server!");
}
public void receiveFromMemory(FriendlyByteBuf buf) {
receiveFromServer(Minecraft.getInstance(), null, buf, null);
}
@Override
final void sendToClient(MinecraftServer server) {
if (prepareDataOnServer()) {
FriendlyByteBuf buf = PacketByteBufs.create();
serializeDataOnServer(buf);
_sendToClient(getIdentifier(), server, PlayerLookup.all(server), buf);
}
}
@Override
final void sendToClient(MinecraftServer server, ServerPlayer player) {
if (prepareDataOnServer()) {
FriendlyByteBuf buf = PacketByteBufs.create();
serializeDataOnServer(buf);
_sendToClient(getIdentifier(), server, List.of(player), buf);
}
}
@Environment(EnvType.CLIENT)
@Override
final void sendToServer(Minecraft client) {
BCLib.LOGGER.error("[Internal Error] The message '" + getIdentifier() + "' must originate from the server!");
}
}
}

View file

@ -0,0 +1,57 @@
package org.betterx.bclib.api.dataexchange;
import net.minecraft.resources.ResourceLocation;
import java.util.Objects;
import java.util.function.Supplier;
import org.jetbrains.annotations.NotNull;
public class DataHandlerDescriptor {
public DataHandlerDescriptor(@NotNull ResourceLocation identifier, @NotNull Supplier<BaseDataHandler> instancer) {
this(identifier, instancer, instancer, false, false);
}
public DataHandlerDescriptor(@NotNull ResourceLocation identifier,
@NotNull Supplier<BaseDataHandler> instancer,
boolean sendOnJoin,
boolean sendBeforeEnter) {
this(identifier, instancer, instancer, sendOnJoin, sendBeforeEnter);
}
public DataHandlerDescriptor(@NotNull ResourceLocation identifier,
@NotNull Supplier<BaseDataHandler> receiv_instancer,
@NotNull Supplier<BaseDataHandler> join_instancer,
boolean sendOnJoin,
boolean sendBeforeEnter) {
this.INSTANCE = receiv_instancer;
this.JOIN_INSTANCE = join_instancer;
this.IDENTIFIER = identifier;
this.sendOnJoin = sendOnJoin;
this.sendBeforeEnter = sendBeforeEnter;
}
public final boolean sendOnJoin;
public final boolean sendBeforeEnter;
@NotNull
public final ResourceLocation IDENTIFIER;
@NotNull
public final Supplier<BaseDataHandler> INSTANCE;
@NotNull
public final Supplier<BaseDataHandler> JOIN_INSTANCE;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o instanceof ResourceLocation) {
return o.equals(IDENTIFIER);
}
if (!(o instanceof DataHandlerDescriptor that)) return false;
return IDENTIFIER.equals(that.IDENTIFIER);
}
@Override
public int hashCode() {
return Objects.hash(IDENTIFIER);
}
}

View file

@ -0,0 +1,160 @@
package org.betterx.bclib.api.dataexchange;
import net.minecraft.network.FriendlyByteBuf;
import org.betterx.bclib.BCLib;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.Objects;
import org.jetbrains.annotations.NotNull;
public class FileHash {
private static final int ERR_DOES_NOT_EXIST = -10;
private static final int ERR_IO_ERROR = -20;
/**
* The md5-hash of the file
*/
@NotNull
public final byte[] md5;
/**
* The size (in bytes) of the input.
*/
public final int size;
/**
* a value that is directly calculated from defined byte positions.
*/
public final int value;
FileHash(byte[] md5, int size, int value) {
Objects.nonNull(md5);
this.md5 = md5;
this.size = size;
this.value = value;
}
static FileHash createForEmpty(int errCode) {
return new FileHash(new byte[0], 0, errCode);
}
public boolean noFile() {
return md5.length == 0;
}
/**
* Serializes the Object to a buffer
*
* @param buf The buffer to write to
*/
public void serialize(FriendlyByteBuf buf) {
buf.writeInt(size);
buf.writeInt(value);
buf.writeByteArray(md5);
}
/**
* Deserialize a Buffer to a new {@link SyncFileHash}-Object
*
* @param buf Thea buffer to read from
* @return The received String
*/
public static FileHash deserialize(FriendlyByteBuf buf) {
final int size = buf.readInt();
final int value = buf.readInt();
final byte[] md5 = buf.readByteArray();
return new FileHash(md5, size, value);
}
/**
* Convert the md5-hash to a human readable string
*
* @return The converted String
*/
public String getMd5String() {
return toHexString(md5);
}
/**
* Converts a byte-array to a hex-string representation
*
* @param bytes The source array
* @return The resulting string, or an empty String if the input was {@code null}
*/
public static String toHexString(byte[] bytes) {
if (bytes == null) return "";
StringBuilder sb = new StringBuilder();
for (byte b : bytes) {
sb.append(String.format("%02x", b));
}
return sb.toString();
}
/**
* Create a new {@link FileHash}.
*
* @param file The input file
* @return A new Instance. You can compare instances using {@link #equals(Object)} to determine if two files are
* identical. Will return {@code null} when an error occurs or the File does not exist
*/
public static FileHash create(File file) {
if (!file.exists()) return createForEmpty(ERR_DOES_NOT_EXIST);
final Path path = file.toPath();
int size = 0;
byte[] md5 = new byte[0];
int value = 0;
try {
byte[] data = Files.readAllBytes(path);
size = data.length;
value = size > 0 ? (data[size / 3] | (data[size / 2] << 8) | (data[size / 5] << 16)) : -1;
if (size > 20) value |= data[20] << 24;
MessageDigest md = MessageDigest.getInstance("MD5");
md.update(data);
md5 = md.digest();
return new FileHash(md5, size, value);
} catch (IOException e) {
BCLib.LOGGER.error("Failed to read file: " + file);
return null;
} catch (NoSuchAlgorithmException e) {
BCLib.LOGGER.error("Unable to build hash for file: " + file);
}
return createForEmpty(ERR_IO_ERROR);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof FileHash)) return false;
FileHash fileHash = (FileHash) o;
return size == fileHash.size && value == fileHash.value && Arrays.equals(md5, fileHash.md5);
}
@Override
public int hashCode() {
int result = Objects.hash(size, value);
result = 31 * result + Arrays.hashCode(md5);
return result;
}
@Override
public String toString() {
return String.format("%08x", size) + "-" + String.format("%08x", value) + "-" + getMd5String();
}
}

View file

@ -0,0 +1,113 @@
package org.betterx.bclib.api.dataexchange;
import net.minecraft.network.FriendlyByteBuf;
import org.betterx.bclib.api.dataexchange.handler.autosync.AutoSync;
import org.betterx.bclib.api.dataexchange.handler.autosync.AutoSyncID;
import java.io.File;
import java.util.Objects;
/**
* Calculates a hash based on the contents of a File.
* <p>
* A File-Hash contains the md5-sum of the File, as well as its size and byte-values from defined positions
* <p>
* You can compare instances using {@link #equals(Object)} to determine if two files are
* identical.
*/
public class SyncFileHash extends AutoSyncID {
public final FileHash hash;
SyncFileHash(String modID, File file, byte[] md5, int size, int value) {
this(modID, file.getName(), md5, size, value);
}
SyncFileHash(String modID, String uniqueID, byte[] md5, int size, int value) {
this(modID, uniqueID, new FileHash(md5, size, value));
}
SyncFileHash(String modID, File file, FileHash hash) {
this(modID, file.getName(), hash);
}
SyncFileHash(String modID, String uniqueID, FileHash hash) {
super(modID, uniqueID);
this.hash = hash;
}
final static AutoSync.NeedTransferPredicate NEED_TRANSFER = (clientHash, serverHash, content) -> !clientHash.equals(
serverHash);
@Override
public String toString() {
return super.toString() + ": " + hash.toString();
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof SyncFileHash)) return false;
if (!super.equals(o)) return false;
SyncFileHash that = (SyncFileHash) o;
return hash.equals(that.hash);
}
@Override
public int hashCode() {
return Objects.hash(super.hashCode(), hash);
}
/**
* Serializes the Object to a buffer
*
* @param buf The buffer to write to
*/
public void serialize(FriendlyByteBuf buf) {
hash.serialize(buf);
DataHandler.writeString(buf, modID);
DataHandler.writeString(buf, uniqueID);
}
/**
* Deserialize a Buffer to a new {@link SyncFileHash}-Object
*
* @param buf Thea buffer to read from
* @return The received String
*/
public static SyncFileHash deserialize(FriendlyByteBuf buf) {
final FileHash hash = FileHash.deserialize(buf);
final String modID = DataHandler.readString(buf);
final String uniqueID = DataHandler.readString(buf);
return new SyncFileHash(modID, uniqueID, hash);
}
/**
* Create a new {@link SyncFileHash}.
* <p>
* Will call {@link #create(String, File, String)} using the name of the File as {@code uniqueID}.
*
* @param modID ID of the calling Mod
* @param file The input file
* @return A new Instance. You can compare instances using {@link #equals(Object)} to determine if two files are
* identical. Will return {@code null} when an error occurs or the File does not exist
*/
public static SyncFileHash create(String modID, File file) {
return create(modID, file, file.getName());
}
/**
* Create a new {@link SyncFileHash}.
*
* @param modID ID of the calling Mod
* @param file The input file
* @param uniqueID The unique ID that is used for this File (see {@link SyncFileHash#uniqueID} for Details.
* @return A new Instance. You can compare instances using {@link #equals(Object)} to determine if two files are
* identical. Will return {@code null} when an error occurs or the File does not exist
*/
public static SyncFileHash create(String modID, File file, String uniqueID) {
return new SyncFileHash(modID, uniqueID, FileHash.create(file));
}
}

View file

@ -0,0 +1,111 @@
package org.betterx.bclib.api.dataexchange.handler;
import net.minecraft.resources.ResourceLocation;
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 org.betterx.bclib.api.dataexchange.*;
import java.util.HashSet;
import java.util.Set;
abstract public class DataExchange {
private static DataExchangeAPI instance;
protected static DataExchangeAPI getInstance() {
if (instance == null) {
instance = new DataExchangeAPI();
}
return instance;
}
protected ConnectorServerside server;
protected ConnectorClientside client;
protected final Set<DataHandlerDescriptor> descriptors;
private final boolean didLoadSyncFolder = false;
abstract protected ConnectorClientside clientSupplier(DataExchange api);
abstract protected ConnectorServerside serverSupplier(DataExchange api);
protected DataExchange() {
descriptors = new HashSet<>();
}
public Set<DataHandlerDescriptor> getDescriptors() {
return descriptors;
}
public static DataHandlerDescriptor getDescriptor(ResourceLocation identifier) {
return getInstance().descriptors.stream().filter(d -> d.equals(identifier)).findFirst().orElse(null);
}
@Environment(EnvType.CLIENT)
protected void initClientside() {
if (client != null) return;
client = clientSupplier(this);
ClientPlayConnectionEvents.INIT.register(client::onPlayInit);
ClientPlayConnectionEvents.JOIN.register(client::onPlayReady);
ClientPlayConnectionEvents.DISCONNECT.register(client::onPlayDisconnect);
}
protected void initServerSide() {
if (server != null) return;
server = serverSupplier(this);
ServerPlayConnectionEvents.INIT.register(server::onPlayInit);
ServerPlayConnectionEvents.JOIN.register(server::onPlayReady);
ServerPlayConnectionEvents.DISCONNECT.register(server::onPlayDisconnect);
}
/**
* Initializes all datastructures that need to exist in the client component.
* <p>
* This is automatically called by BCLib. You can register {@link DataHandler}-Objects before this Method is called
*/
@Environment(EnvType.CLIENT)
public static void prepareClientside() {
DataExchange api = DataExchange.getInstance();
api.initClientside();
}
/**
* Initializes all datastructures that need to exist in the server component.
* <p>
* This is automatically called by BCLib. You can register {@link DataHandler}-Objects before this Method is called
*/
public static void prepareServerside() {
DataExchange api = DataExchange.getInstance();
api.initServerSide();
}
/**
* Automatically called before the player enters the world.
* <p>
* This is automatically called by BCLib. It will send all {@link DataHandler}-Objects that have {@link DataHandlerDescriptor#sendBeforeEnter} set to*
* {@code true},
*/
@Environment(EnvType.CLIENT)
public static void sendOnEnter() {
getInstance().descriptors.forEach((desc) -> {
if (desc.sendBeforeEnter) {
BaseDataHandler h = desc.JOIN_INSTANCE.get();
if (!h.getOriginatesOnServer()) {
getInstance().client.sendToServer(h);
}
}
});
}
}

View file

@ -0,0 +1,249 @@
package org.betterx.bclib.api.dataexchange.handler.autosync;
import net.minecraft.network.FriendlyByteBuf;
import org.betterx.bclib.BCLib;
import org.betterx.bclib.api.dataexchange.DataHandler;
import org.betterx.bclib.api.dataexchange.SyncFileHash;
import org.betterx.bclib.util.ModUtil;
import org.betterx.bclib.util.ModUtil.ModInfo;
import org.betterx.bclib.util.Pair;
import org.betterx.bclib.util.PathUtil;
import org.betterx.bclib.util.Triple;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
class AutoFileSyncEntry extends AutoSyncID {
static class ForDirectFileRequest extends AutoFileSyncEntry {
final File relFile;
ForDirectFileRequest(String syncID, File relFile, File absFile) {
super(AutoSyncID.ForDirectFileRequest.MOD_ID, syncID, absFile, false, (a, b, c) -> false);
this.relFile = relFile;
}
@Override
public int serializeContent(FriendlyByteBuf buf) {
int res = super.serializeContent(buf);
DataHandler.writeString(buf, relFile.toString());
return res;
}
static AutoFileSyncEntry.ForDirectFileRequest finishDeserializeContent(String syncID, FriendlyByteBuf buf) {
final String relFile = DataHandler.readString(buf);
SyncFolderDescriptor desc = AutoSync.getSyncFolderDescriptor(syncID);
if (desc != null) {
//ensures that the file is not above the base-folder
if (desc.acceptChildElements(desc.mapAbsolute(relFile))) {
return new AutoFileSyncEntry.ForDirectFileRequest(syncID,
new File(relFile),
desc.localFolder.resolve(relFile)
.normalize()
.toFile());
}
}
return null;
}
@Override
public String toString() {
return uniqueID + " - " + relFile;
}
}
static class ForModFileRequest extends AutoFileSyncEntry {
public static File getLocalPathForID(String modID, boolean matchLocalVersion) {
ModInfo mi = ModUtil.getModInfo(modID, matchLocalVersion);
if (mi != null) {
return mi.jarPath.toFile();
}
return null;
}
public final String version;
ForModFileRequest(String modID, boolean matchLocalVersion, String version) {
super(modID,
AutoSyncID.ForModFileRequest.UNIQUE_ID,
getLocalPathForID(modID, matchLocalVersion),
false,
(a, b, c) -> false);
if (this.fileName == null && matchLocalVersion) {
BCLib.LOGGER.error("Unknown mod '" + modID + "'.");
}
if (version == null)
this.version = ModUtil.getModVersion(modID);
else
this.version = version;
}
@Override
public int serializeContent(FriendlyByteBuf buf) {
final int res = super.serializeContent(buf);
buf.writeInt(ModUtil.convertModVersion(version));
return res;
}
static AutoFileSyncEntry.ForModFileRequest finishDeserializeContent(String modID, FriendlyByteBuf buf) {
final String version = ModUtil.convertModVersion(buf.readInt());
return new AutoFileSyncEntry.ForModFileRequest(modID, false, version);
}
@Override
public String toString() {
return "Mod " + modID + " (v" + version + ")";
}
}
public final AutoSync.NeedTransferPredicate needTransfer;
public final File fileName;
public final boolean requestContent;
private SyncFileHash hash;
AutoFileSyncEntry(String modID,
File fileName,
boolean requestContent,
AutoSync.NeedTransferPredicate needTransfer) {
this(modID, fileName.getName(), fileName, requestContent, needTransfer);
}
AutoFileSyncEntry(String modID,
String uniqueID,
File fileName,
boolean requestContent,
AutoSync.NeedTransferPredicate needTransfer) {
super(modID, uniqueID);
this.needTransfer = needTransfer;
this.fileName = fileName;
this.requestContent = requestContent;
}
public SyncFileHash getFileHash() {
if (hash == null) {
hash = SyncFileHash.create(modID, fileName, uniqueID);
}
return hash;
}
public byte[] getContent() {
if (!fileName.exists()) return new byte[0];
final Path path = fileName.toPath();
try {
return Files.readAllBytes(path);
} catch (IOException e) {
}
return new byte[0];
}
public int serializeContent(FriendlyByteBuf buf) {
DataHandler.writeString(buf, modID);
DataHandler.writeString(buf, uniqueID);
return serializeFileContent(buf);
}
public static Triple<AutoFileSyncEntry, byte[], AutoSyncID> deserializeContent(FriendlyByteBuf buf) {
final String modID = DataHandler.readString(buf);
final String uniqueID = DataHandler.readString(buf);
byte[] data = deserializeFileContent(buf);
AutoFileSyncEntry entry;
if (AutoSyncID.ForDirectFileRequest.MOD_ID.equals(modID)) {
entry = AutoFileSyncEntry.ForDirectFileRequest.finishDeserializeContent(uniqueID, buf);
} else if (AutoSyncID.ForModFileRequest.UNIQUE_ID.equals(uniqueID)) {
entry = AutoFileSyncEntry.ForModFileRequest.finishDeserializeContent(modID, buf);
} else {
entry = AutoFileSyncEntry.findMatching(modID, uniqueID);
}
return new Triple<>(entry, data, new AutoSyncID(modID, uniqueID));
}
public void serialize(FriendlyByteBuf buf) {
getFileHash().serialize(buf);
buf.writeBoolean(requestContent);
if (requestContent) {
serializeFileContent(buf);
}
}
public static AutoSync.AutoSyncTriple deserializeAndMatch(FriendlyByteBuf buf) {
Pair<SyncFileHash, byte[]> e = deserialize(buf);
AutoFileSyncEntry match = findMatching(e.first);
return new AutoSync.AutoSyncTriple(e.first, e.second, match);
}
public static Pair<SyncFileHash, byte[]> deserialize(FriendlyByteBuf buf) {
SyncFileHash hash = SyncFileHash.deserialize(buf);
boolean withContent = buf.readBoolean();
byte[] data = null;
if (withContent) {
data = deserializeFileContent(buf);
}
return new Pair(hash, data);
}
private int serializeFileContent(FriendlyByteBuf buf) {
if (!PathUtil.isChildOf(PathUtil.GAME_FOLDER, fileName.toPath())) {
BCLib.LOGGER.error(fileName + " is not within game folder " + PathUtil.GAME_FOLDER + ". Pretending it does not exist.");
buf.writeInt(0);
return 0;
}
byte[] content = getContent();
buf.writeInt(content.length);
buf.writeByteArray(content);
return content.length;
}
private static byte[] deserializeFileContent(FriendlyByteBuf buf) {
byte[] data;
int size = buf.readInt();
data = buf.readByteArray(size);
return data;
}
public static AutoFileSyncEntry findMatching(SyncFileHash hash) {
return findMatching(hash.modID, hash.uniqueID);
}
public static AutoFileSyncEntry findMatching(AutoSyncID aid) {
if (aid instanceof AutoSyncID.ForDirectFileRequest) {
AutoSyncID.ForDirectFileRequest freq = (AutoSyncID.ForDirectFileRequest) aid;
SyncFolderDescriptor desc = AutoSync.getSyncFolderDescriptor(freq.uniqueID);
if (desc != null) {
SyncFolderDescriptor.SubFile subFile = desc.getLocalSubFile(freq.relFile.toString());
if (subFile != null) {
final File absPath = desc.localFolder.resolve(subFile.relPath)
.normalize()
.toFile();
return new AutoFileSyncEntry.ForDirectFileRequest(freq.uniqueID,
new File(subFile.relPath),
absPath);
}
}
return null;
} else if (aid instanceof AutoSyncID.ForModFileRequest) {
AutoSyncID.ForModFileRequest mreq = (AutoSyncID.ForModFileRequest) aid;
return new AutoFileSyncEntry.ForModFileRequest(mreq.modID, true, null);
}
return findMatching(aid.modID, aid.uniqueID);
}
public static AutoFileSyncEntry findMatching(String modID, String uniqueID) {
return AutoSync.getAutoSyncFiles()
.stream()
.filter(asf -> asf.modID.equals(modID) && asf.uniqueID.equals(uniqueID))
.findFirst()
.orElse(null);
}
}

View file

@ -0,0 +1,199 @@
package org.betterx.bclib.api.dataexchange.handler.autosync;
import net.fabricmc.loader.api.FabricLoader;
import org.betterx.bclib.BCLib;
import org.betterx.bclib.api.dataexchange.DataExchangeAPI;
import org.betterx.bclib.api.dataexchange.SyncFileHash;
import org.betterx.bclib.config.Configs;
import org.betterx.bclib.config.ServerConfig;
import org.betterx.bclib.util.PathUtil;
import java.io.File;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.function.BiConsumer;
public class AutoSync {
public static final String SYNC_CATEGORY = "auto_sync";
public final static SyncFolderDescriptor SYNC_FOLDER = new SyncFolderDescriptor("BCLIB-SYNC",
FabricLoader.getInstance()
.getGameDir()
.resolve("bclib-sync")
.normalize()
.toAbsolutePath(),
true);
@FunctionalInterface
public interface NeedTransferPredicate {
boolean test(SyncFileHash clientHash, SyncFileHash serverHash, FileContentWrapper content);
}
final static class AutoSyncTriple {
public final SyncFileHash serverHash;
public final byte[] serverContent;
public final AutoFileSyncEntry localMatch;
public AutoSyncTriple(SyncFileHash serverHash, byte[] serverContent, AutoFileSyncEntry localMatch) {
this.serverHash = serverHash;
this.serverContent = serverContent;
this.localMatch = localMatch;
}
@Override
public String toString() {
return serverHash.modID + "." + serverHash.uniqueID;
}
}
// ##### File Syncing
protected final static List<BiConsumer<AutoSyncID, File>> onWriteCallbacks = new ArrayList<>(2);
/**
* 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>
* 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<AutoSyncID, File> callback) {
onWriteCallbacks.add(callback);
}
private static final List<AutoFileSyncEntry> autoSyncFiles = new ArrayList<>(4);
public static List<AutoFileSyncEntry> getAutoSyncFiles() {
return autoSyncFiles;
}
/**
* Registers a File for automatic client syncing.
*
* @param modID The ID of the calling Mod
* @param needTransfer If the predicate returns true, the file needs to get copied to the server.
* @param fileName The name of the File
* @param requestContent When {@code true} the content of the file is requested for comparison. This will copy the
* entire file from the client to the server.
* <p>
* 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 SyncFileHash}
* for comparison is sufficient.
*/
public static void addAutoSyncFileData(String modID,
File fileName,
boolean requestContent,
NeedTransferPredicate needTransfer) {
if (!PathUtil.isChildOf(PathUtil.GAME_FOLDER, fileName.toPath())) {
BCLib.LOGGER.error(fileName + " is outside of Game Folder " + PathUtil.GAME_FOLDER);
} else {
autoSyncFiles.add(new AutoFileSyncEntry(modID, fileName, requestContent, needTransfer));
}
}
/**
* Registers a File for automatic client syncing.
*
* @param modID The ID of the calling Mod
* @param uniqueID A unique Identifier for the File. (see {@link SyncFileHash#uniqueID} for
* Details
* @param needTransfer If the predicate returns true, the file needs to get copied to the server.
* @param fileName The name of the File
* @param requestContent When {@code true} the content of the file is requested for comparison. This will copy the
* entire file from the client to the server.
* <p>
* 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 SyncFileHash}
* for comparison is sufficient.
*/
public static void addAutoSyncFileData(String modID,
String uniqueID,
File fileName,
boolean requestContent,
NeedTransferPredicate needTransfer) {
if (!PathUtil.isChildOf(PathUtil.GAME_FOLDER, fileName.toPath())) {
BCLib.LOGGER.error(fileName + " is outside of Game Folder " + PathUtil.GAME_FOLDER);
} else {
autoSyncFiles.add(new AutoFileSyncEntry(modID, uniqueID, fileName, requestContent, needTransfer));
}
}
/**
* Called when {@code SendFiles} received a File on the Client and wrote it to the FileSystem.
* <p>
* This is the place where reload Code should go.
*
* @param aid The ID of the received File
* @param file The location of the FIle on the client
*/
static void didReceiveFile(AutoSyncID aid, File file) {
onWriteCallbacks.forEach(fkt -> fkt.accept(aid, file));
}
// ##### Folder Syncing
static final List<SyncFolderDescriptor> syncFolderDescriptions = Arrays.asList(SYNC_FOLDER);
private List<String> syncFolderContent;
protected List<String> getSyncFolderContent() {
if (syncFolderContent == null) {
return new ArrayList<>(0);
}
return syncFolderContent;
}
private static boolean didRegisterAdditionalMods = false;
//we call this from HelloClient on the Server to prepare transfer
protected static void loadSyncFolder() {
if (Configs.SERVER_CONFIG.isOfferingFiles()) {
syncFolderDescriptions.forEach(desc -> desc.loadCache());
}
if (!didRegisterAdditionalMods && Configs.SERVER_CONFIG.isOfferingMods()) {
didRegisterAdditionalMods = true;
List<String> modIDs = Configs.SERVER_CONFIG.get(ServerConfig.ADDITIONAL_MODS);
if (modIDs != null) {
modIDs.stream().forEach(modID -> DataExchangeAPI.registerModDependency(modID));
}
}
}
protected static SyncFolderDescriptor getSyncFolderDescriptor(String folderID) {
return syncFolderDescriptions.stream()
.filter(d -> d.equals(folderID))
.findFirst()
.orElse(null);
}
protected static Path localBasePathForFolderID(String folderID) {
final SyncFolderDescriptor desc = getSyncFolderDescriptor(folderID);
if (desc != null) {
return desc.localFolder;
} else {
BCLib.LOGGER.warning("Unknown Sync-Folder ID '" + folderID + "'");
return null;
}
}
public static void registerSyncFolder(String folderID, Path localBaseFolder, boolean removeAdditionalFiles) {
localBaseFolder = localBaseFolder.normalize();
if (PathUtil.isChildOf(PathUtil.GAME_FOLDER, localBaseFolder)) {
final SyncFolderDescriptor desc = new SyncFolderDescriptor(folderID,
localBaseFolder,
removeAdditionalFiles);
if (syncFolderDescriptions.contains(desc)) {
BCLib.LOGGER.warning("Tried to override Folder Sync '" + folderID + "' again.");
} else {
syncFolderDescriptions.add(desc);
}
} else {
BCLib.LOGGER.error(localBaseFolder + " (from " + folderID + ") is outside the game directory " + PathUtil.GAME_FOLDER + ". Sync is not allowed.");
}
}
}

View file

@ -0,0 +1,144 @@
package org.betterx.bclib.api.dataexchange.handler.autosync;
import net.minecraft.network.FriendlyByteBuf;
import org.betterx.bclib.api.dataexchange.DataHandler;
import org.betterx.bclib.config.Config;
import org.betterx.bclib.util.ModUtil;
import java.io.File;
import java.util.Objects;
import org.jetbrains.annotations.NotNull;
public class AutoSyncID {
static class WithContentOverride extends AutoSyncID {
final FileContentWrapper contentWrapper;
final File localFile;
WithContentOverride(String modID, String uniqueID, FileContentWrapper contentWrapper, File localFile) {
super(modID, uniqueID);
this.contentWrapper = contentWrapper;
this.localFile = localFile;
}
@Override
public String toString() {
return super.toString() + " (Content override)";
}
}
static class ForDirectFileRequest extends AutoSyncID {
public final static String MOD_ID = "bclib::FILE";
final File relFile;
ForDirectFileRequest(String syncID, File relFile) {
super(ForDirectFileRequest.MOD_ID, syncID);
this.relFile = relFile;
}
@Override
void serializeData(FriendlyByteBuf buf) {
super.serializeData(buf);
DataHandler.writeString(buf, relFile.toString());
}
static ForDirectFileRequest finishDeserialize(String modID, String uniqueID, FriendlyByteBuf buf) {
final File fl = new File(DataHandler.readString(buf));
return new ForDirectFileRequest(uniqueID, fl);
}
@Override
public String toString() {
return super.uniqueID + " (" + this.relFile + ")";
}
}
static class ForModFileRequest extends AutoSyncID {
public final static String UNIQUE_ID = "bclib::MOD";
private final String version;
ForModFileRequest(String modID, String version) {
super(modID, ForModFileRequest.UNIQUE_ID);
this.version = version;
}
@Override
void serializeData(FriendlyByteBuf buf) {
super.serializeData(buf);
buf.writeInt(ModUtil.convertModVersion(version));
}
static ForModFileRequest finishDeserialize(String modID, String uniqueID, FriendlyByteBuf buf) {
final String version = ModUtil.convertModVersion(buf.readInt());
return new ForModFileRequest(modID, version);
}
@Override
public String toString() {
return super.modID + " (v" + this.version + ")";
}
}
/**
* A Unique ID for the referenced File.
* <p>
* Files with the same {@link #modID} need to have a unique IDs. Normally the filename from FileHash(String, File, byte[], int, int)
* is used to generated that ID, but you can directly specify one using FileHash(String, String, byte[], int, int).
*/
@NotNull
public final String uniqueID;
/**
* The ID of the Mod that is registering the File
*/
@NotNull
public final String modID;
public AutoSyncID(String modID, String uniqueID) {
Objects.nonNull(modID);
Objects.nonNull(uniqueID);
this.modID = modID;
this.uniqueID = uniqueID;
}
@Override
public String toString() {
return modID + "." + uniqueID;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof AutoSyncID)) return false;
AutoSyncID that = (AutoSyncID) o;
return uniqueID.equals(that.uniqueID) && modID.equals(that.modID);
}
@Override
public int hashCode() {
return Objects.hash(uniqueID, modID);
}
void serializeData(FriendlyByteBuf buf) {
DataHandler.writeString(buf, modID);
DataHandler.writeString(buf, uniqueID);
}
static AutoSyncID deserializeData(FriendlyByteBuf buf) {
String modID = DataHandler.readString(buf);
String uID = DataHandler.readString(buf);
if (ForDirectFileRequest.MOD_ID.equals(modID)) {
return ForDirectFileRequest.finishDeserialize(modID, uID, buf);
} else if (ForModFileRequest.UNIQUE_ID.equals(uID)) {
return ForModFileRequest.finishDeserialize(modID, uID, buf);
} else {
return new AutoSyncID(modID, uID);
}
}
public boolean isConfigFile() {
return this.uniqueID.startsWith(Config.CONFIG_SYNC_PREFIX);
}
}

View file

@ -0,0 +1,276 @@
package org.betterx.bclib.api.dataexchange.handler.autosync;
import net.minecraft.client.Minecraft;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.util.ProgressListener;
import net.fabricmc.fabric.api.networking.v1.PacketByteBufs;
import net.fabricmc.fabric.api.networking.v1.PacketSender;
import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking;
import org.betterx.bclib.BCLib;
import org.betterx.bclib.api.dataexchange.BaseDataHandler;
import org.betterx.bclib.api.dataexchange.DataHandler;
import org.betterx.bclib.api.dataexchange.DataHandlerDescriptor;
import org.betterx.bclib.api.dataexchange.handler.DataExchange;
import java.util.*;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* Used to seperate large data transfers into multiple smaller messages.
* <p>
* {@link DataHandler} will automatically convert larger messages into Chunks on the Server
* and assemble the original message from those chunks on the client.
*/
public class Chunker extends DataHandler.FromServer {
/**
* Responsible for assembling the original ByteBuffer created by {@link PacketChunkSender} on the
* receiving end. Automatically created from the header {@link Chunker}-Message (where the serialNo==-1)
*/
static class PacketChunkReceiver {
@NotNull
public final UUID uuid;
public final int chunkCount;
@NotNull
private final FriendlyByteBuf networkedBuf;
@Nullable
private final DataHandlerDescriptor descriptor;
private static final List<PacketChunkReceiver> active = new ArrayList<>(1);
private static PacketChunkReceiver newReceiver(@NotNull UUID uuid, int chunkCount, ResourceLocation origin) {
DataHandlerDescriptor desc = DataExchange.getDescriptor(origin);
final PacketChunkReceiver r = new PacketChunkReceiver(uuid, chunkCount, desc);
active.add(r);
return r;
}
private static PacketChunkReceiver getOrCreate(@NotNull UUID uuid, int chunkCount, ResourceLocation origin) {
return active.stream()
.filter(r -> r.uuid.equals(uuid))
.findFirst()
.orElse(newReceiver(uuid, chunkCount, origin));
}
public static PacketChunkReceiver get(@NotNull UUID uuid) {
return active.stream().filter(r -> r.uuid.equals(uuid)).findFirst().orElse(null);
}
private PacketChunkReceiver(@NotNull UUID uuid, int chunkCount, @Nullable DataHandlerDescriptor descriptor) {
this.uuid = uuid;
this.chunkCount = chunkCount;
networkedBuf = PacketByteBufs.create();
this.descriptor = descriptor;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof PacketChunkReceiver)) return false;
PacketChunkReceiver that = (PacketChunkReceiver) o;
return uuid.equals(that.uuid);
}
@Override
public int hashCode() {
return Objects.hash(uuid);
}
public boolean testFinished() {
ProgressListener listener = ChunkerProgress.getProgressListener();
if (listener != null) {
listener.progressStagePercentage((100 * receivedCount) / chunkCount);
}
if (incomingBuffer == null) {
return true;
}
if (lastReadSerial >= chunkCount - 1) {
onFinish();
return true;
}
return false;
}
private void addBuffer(FriendlyByteBuf input) {
final int size = input.readableBytes();
final int cap = networkedBuf.capacity() - networkedBuf.writerIndex();
if (cap < size) {
networkedBuf.capacity(networkedBuf.writerIndex() + size);
}
input.readBytes(networkedBuf, size);
input.clear();
}
protected void onFinish() {
incomingBuffer.clear();
incomingBuffer = null;
final BaseDataHandler baseHandler = descriptor.INSTANCE.get();
if (baseHandler instanceof DataHandler.FromServer handler) {
handler.receiveFromMemory(networkedBuf);
}
}
Map<Integer, FriendlyByteBuf> incomingBuffer = new HashMap<>();
int lastReadSerial = -1;
int receivedCount = 0;
public void processReceived(FriendlyByteBuf buf, int serialNo, int size) {
receivedCount++;
if (lastReadSerial == serialNo - 1) {
addBuffer(buf);
lastReadSerial = serialNo;
} else {
//not sure if order is guaranteed by the underlying system!
boolean haveAll = true;
for (int nr = lastReadSerial + 1; nr < serialNo - 1; nr++) {
if (incomingBuffer.get(nr) == null) {
haveAll = false;
break;
}
}
if (haveAll) {
for (int nr = lastReadSerial + 1; nr < serialNo - 1; nr++) {
addBuffer(incomingBuffer.get(nr));
incomingBuffer.put(nr, null);
}
addBuffer(buf);
lastReadSerial = serialNo;
} else {
incomingBuffer.put(serialNo, buf);
}
}
}
}
/**
* Responsible for splitting an outgoing ByteBuffer into several smaller Chunks and
* send them as seperate messages to the {@link Chunker}-Channel
*/
public static class PacketChunkSender {
private final FriendlyByteBuf networkedBuf;
public final UUID uuid;
public final int chunkCount;
public final int size;
public final ResourceLocation origin;
public PacketChunkSender(FriendlyByteBuf buf, ResourceLocation origin) {
networkedBuf = buf;
size = buf.readableBytes();
chunkCount = (int) Math.ceil((double) size / MAX_PAYLOAD_SIZE);
uuid = UUID.randomUUID();
this.origin = origin;
}
public void sendChunks(Collection<ServerPlayer> players) {
BCLib.LOGGER.info("Sending Request in " + chunkCount + " Packet-Chunks");
for (int i = -1; i < chunkCount; i++) {
Chunker c = new Chunker(i, uuid, networkedBuf, chunkCount, origin);
FriendlyByteBuf buf = PacketByteBufs.create();
c.serializeDataOnServer(buf);
for (ServerPlayer player : players) {
ServerPlayNetworking.send(player, DESCRIPTOR.IDENTIFIER, buf);
}
}
}
}
//header = version + UUID + serialNo + size, see serializeDataOnServer
private static final int HEADER_SIZE = 1 + 16 + 4 + 4;
public static final int MAX_PACKET_SIZE = 1024 * 1024;
private static final int MAX_PAYLOAD_SIZE = MAX_PACKET_SIZE - HEADER_SIZE;
public static final DataHandlerDescriptor DESCRIPTOR = new DataHandlerDescriptor(new ResourceLocation(BCLib.MOD_ID,
"chunker"),
Chunker::new,
false,
false);
private int serialNo;
private UUID uuid;
private int chunkCount;
private FriendlyByteBuf networkedBuf;
private ResourceLocation origin;
protected Chunker(int serialNo, UUID uuid, FriendlyByteBuf networkedBuf, int chunkCount, ResourceLocation origin) {
super(DESCRIPTOR.IDENTIFIER);
this.serialNo = serialNo;
this.uuid = uuid;
this.networkedBuf = networkedBuf;
this.chunkCount = chunkCount;
this.origin = origin;
}
protected Chunker() {
super(DESCRIPTOR.IDENTIFIER);
}
@Override
protected void serializeDataOnServer(FriendlyByteBuf buf) {
//Sending Header. Make sure to change HEADER_SIZE if you change this!
buf.writeByte(0);
buf.writeLong(uuid.getMostSignificantBits());
buf.writeLong(uuid.getLeastSignificantBits());
buf.writeInt(serialNo);
//sending Payload
if (serialNo == -1) {
//this is our header-Chunk that transports status information
buf.writeInt(chunkCount);
writeString(buf, origin.getNamespace());
writeString(buf, origin.getPath());
} else {
//this is an actual payload chunk
buf.capacity(MAX_PACKET_SIZE);
final int size = Math.min(MAX_PAYLOAD_SIZE, networkedBuf.readableBytes());
buf.writeInt(size);
networkedBuf.readBytes(buf, size);
}
}
private PacketChunkReceiver receiver;
@Override
protected void deserializeIncomingDataOnClient(FriendlyByteBuf buf, PacketSender responseSender) {
final int version = buf.readByte();
uuid = new UUID(buf.readLong(), buf.readLong());
serialNo = buf.readInt();
if (serialNo == -1) {
chunkCount = buf.readInt();
final String namespace = readString(buf);
final String path = readString(buf);
ResourceLocation ident = new ResourceLocation(namespace, path);
BCLib.LOGGER.info("Receiving " + chunkCount + " + Packet-Chunks for " + ident);
receiver = PacketChunkReceiver.getOrCreate(uuid, chunkCount, ident);
} else {
receiver = PacketChunkReceiver.get(uuid);
if (receiver != null) {
final int size = buf.readInt();
receiver.processReceived(buf, serialNo, size);
} else {
BCLib.LOGGER.error("Unknown Packet-Chunk Transfer for " + uuid);
}
}
}
@Override
protected void runOnClientGameThread(Minecraft client) {
if (receiver != null) {
receiver.testFinished();
}
}
}

View file

@ -0,0 +1,28 @@
package org.betterx.bclib.api.dataexchange.handler.autosync;
import net.minecraft.util.ProgressListener;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
import org.betterx.bclib.gui.screens.ProgressScreen;
@Environment(EnvType.CLIENT)
public class ChunkerProgress {
private static ProgressScreen progressScreen;
@Environment(EnvType.CLIENT)
public static void setProgressScreen(ProgressScreen scr) {
progressScreen = scr;
}
@Environment(EnvType.CLIENT)
public static ProgressScreen getProgressScreen() {
return progressScreen;
}
@Environment(EnvType.CLIENT)
public static ProgressListener getProgressListener() {
return progressScreen;
}
}

View file

@ -0,0 +1,75 @@
package org.betterx.bclib.api.dataexchange.handler.autosync;
import org.betterx.bclib.BCLib;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
public class FileContentWrapper {
private byte[] rawContent;
private ByteArrayOutputStream outputStream;
FileContentWrapper(byte[] content) {
this.rawContent = content;
this.outputStream = null;
}
public byte[] getOriginalContent() {
return rawContent;
}
public byte[] getRawContent() {
if (outputStream != null) {
return outputStream.toByteArray();
}
return rawContent;
}
private void invalidateOutputStream() {
if (this.outputStream != null) {
try {
this.outputStream.close();
} catch (IOException e) {
BCLib.LOGGER.debug(e);
}
}
this.outputStream = null;
}
public void setRawContent(byte[] rawContent) {
this.rawContent = rawContent;
invalidateOutputStream();
}
public void syncWithOutputStream() {
if (outputStream != null) {
try {
outputStream.flush();
} catch (IOException e) {
BCLib.LOGGER.error(e.getMessage());
e.printStackTrace();
}
setRawContent(getRawContent());
invalidateOutputStream();
}
}
public ByteArrayInputStream getInputStream() {
if (rawContent == null) return new ByteArrayInputStream(new byte[0]);
return new ByteArrayInputStream(rawContent);
}
public ByteArrayOutputStream getOrCreateOutputStream() {
if (this.outputStream == null) {
return this.getEmptyOutputStream();
}
return this.outputStream;
}
public ByteArrayOutputStream getEmptyOutputStream() {
invalidateOutputStream();
this.outputStream = new ByteArrayOutputStream(this.rawContent.length);
return this.outputStream;
}
}

View file

@ -0,0 +1,509 @@
package org.betterx.bclib.api.dataexchange.handler.autosync;
import net.minecraft.client.Minecraft;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.network.chat.CommonComponents;
import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceLocation;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
import net.fabricmc.fabric.api.networking.v1.PacketSender;
import net.fabricmc.loader.api.metadata.ModEnvironment;
import org.betterx.bclib.BCLib;
import org.betterx.bclib.api.dataexchange.DataExchangeAPI;
import org.betterx.bclib.api.dataexchange.DataHandler;
import org.betterx.bclib.api.dataexchange.DataHandlerDescriptor;
import org.betterx.bclib.config.Configs;
import org.betterx.bclib.config.ServerConfig;
import org.betterx.bclib.gui.screens.ModListScreen;
import org.betterx.bclib.gui.screens.ProgressScreen;
import org.betterx.bclib.gui.screens.SyncFilesScreen;
import org.betterx.bclib.gui.screens.WarnBCLibVersionMismatch;
import org.betterx.bclib.util.ModUtil;
import org.betterx.bclib.util.ModUtil.ModInfo;
import org.betterx.bclib.util.PathUtil;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.util.*;
import java.util.Map.Entry;
import java.util.stream.Collectors;
/**
* Sent from the Server to the Client.
* <p>
* For Details refer to {@link HelloServer}
*/
public class HelloClient extends DataHandler.FromServer {
public record OfferedModInfo(String version, int size, boolean canDownload) {
}
public interface IServerModMap extends Map<String, OfferedModInfo> {
}
public static class ServerModMap extends HashMap<String, OfferedModInfo> implements IServerModMap {
}
public static final DataHandlerDescriptor DESCRIPTOR = new DataHandlerDescriptor(new ResourceLocation(BCLib.MOD_ID,
"hello_client"),
HelloClient::new,
false,
false);
public HelloClient() {
super(DESCRIPTOR.IDENTIFIER);
}
static String getBCLibVersion() {
return ModUtil.getModVersion(BCLib.MOD_ID);
}
@Override
protected boolean prepareDataOnServer() {
if (!Configs.SERVER_CONFIG.isAllowingAutoSync()) {
BCLib.LOGGER.info("Auto-Sync was disabled on the server.");
return false;
}
AutoSync.loadSyncFolder();
return true;
}
@Override
protected void serializeDataOnServer(FriendlyByteBuf buf) {
final String vbclib = getBCLibVersion();
BCLib.LOGGER.info("Sending Hello to Client. (server=" + vbclib + ")");
//write BCLibVersion (=protocol version)
buf.writeInt(ModUtil.convertModVersion(vbclib));
if (Configs.SERVER_CONFIG.isOfferingMods() || Configs.SERVER_CONFIG.isOfferingInfosForMods()) {
List<String> mods = DataExchangeAPI.registeredMods();
final List<String> inmods = mods;
if (Configs.SERVER_CONFIG.isOfferingAllMods() || Configs.SERVER_CONFIG.isOfferingInfosForMods()) {
mods = new ArrayList<>(inmods.size());
mods.addAll(inmods);
mods.addAll(ModUtil
.getMods()
.entrySet()
.stream()
.filter(entry -> entry.getValue().metadata.getEnvironment() != ModEnvironment.SERVER && !inmods.contains(
entry.getKey()))
.map(entry -> entry.getKey())
.collect(Collectors.toList())
);
}
mods = mods
.stream()
.filter(entry -> !Configs.SERVER_CONFIG.get(ServerConfig.EXCLUDED_MODS).contains(entry))
.collect(Collectors.toList());
//write Plugin Versions
buf.writeInt(mods.size());
for (String modID : mods) {
final String ver = ModUtil.getModVersion(modID);
int size = 0;
final ModInfo mi = ModUtil.getModInfo(modID);
if (mi != null) {
try {
size = (int) Files.size(mi.jarPath);
} catch (IOException e) {
BCLib.LOGGER.error("Unable to get File Size: " + e.getMessage());
}
}
writeString(buf, modID);
buf.writeInt(ModUtil.convertModVersion(ver));
buf.writeInt(size);
final boolean canDownload = size > 0 && Configs.SERVER_CONFIG.isOfferingMods() && (Configs.SERVER_CONFIG.isOfferingAllMods() || inmods.contains(
modID));
buf.writeBoolean(canDownload);
BCLib.LOGGER.info(" - Listing Mod " + modID + " v" + ver + " (size: " + PathUtil.humanReadableFileSize(
size) + ", download=" + canDownload + ")");
}
} else {
BCLib.LOGGER.info("Server will not list Mods.");
buf.writeInt(0);
}
if (Configs.SERVER_CONFIG.isOfferingFiles() || Configs.SERVER_CONFIG.isOfferingConfigs()) {
//do only include files that exist on the server
final List<AutoFileSyncEntry> existingAutoSyncFiles = AutoSync.getAutoSyncFiles()
.stream()
.filter(e -> e.fileName.exists())
.filter(e -> (e.isConfigFile() && Configs.SERVER_CONFIG.isOfferingConfigs()) || (e instanceof AutoFileSyncEntry.ForDirectFileRequest && Configs.SERVER_CONFIG.isOfferingFiles()))
.collect(Collectors.toList());
//send config Data
buf.writeInt(existingAutoSyncFiles.size());
for (AutoFileSyncEntry entry : existingAutoSyncFiles) {
entry.serialize(buf);
BCLib.LOGGER.info(" - Offering " + (entry.isConfigFile() ? "Config " : "File ") + entry);
}
} else {
BCLib.LOGGER.info("Server will neither offer Files nor Configs.");
buf.writeInt(0);
}
if (Configs.SERVER_CONFIG.isOfferingFiles()) {
buf.writeInt(AutoSync.syncFolderDescriptions.size());
AutoSync.syncFolderDescriptions.forEach(desc -> {
BCLib.LOGGER.info(" - Offering Folder " + desc.localFolder + " (allowDelete=" + desc.removeAdditionalFiles + ")");
desc.serialize(buf);
});
} else {
BCLib.LOGGER.info("Server will not offer Sync Folders.");
buf.writeInt(0);
}
buf.writeBoolean(Configs.SERVER_CONFIG.isOfferingInfosForMods());
}
String bclibVersion = "0.0.0";
IServerModMap modVersion = new ServerModMap();
List<AutoSync.AutoSyncTriple> autoSyncedFiles = null;
List<SyncFolderDescriptor> autoSynFolders = null;
boolean serverPublishedModInfo = false;
@Environment(EnvType.CLIENT)
@Override
protected void deserializeIncomingDataOnClient(FriendlyByteBuf buf, PacketSender responseSender) {
//read BCLibVersion (=protocol version)
bclibVersion = ModUtil.convertModVersion(buf.readInt());
//read Plugin Versions
modVersion = new ServerModMap();
int count = buf.readInt();
for (int i = 0; i < count; i++) {
final String id = readString(buf);
final String version = ModUtil.convertModVersion(buf.readInt());
final int size;
final boolean canDownload;
//since v0.4.1 we also send the size of the mod-File
size = buf.readInt();
canDownload = buf.readBoolean();
modVersion.put(id, new OfferedModInfo(version, size, canDownload));
}
//read config Data
count = buf.readInt();
autoSyncedFiles = new ArrayList<>(count);
for (int i = 0; i < count; i++) {
//System.out.println("Deserializing ");
AutoSync.AutoSyncTriple t = AutoFileSyncEntry.deserializeAndMatch(buf);
autoSyncedFiles.add(t);
//System.out.println(t.first);
}
autoSynFolders = new ArrayList<>(1);
//since v0.4.1 we also send the sync folders
final int folderCount = buf.readInt();
for (int i = 0; i < folderCount; i++) {
SyncFolderDescriptor desc = SyncFolderDescriptor.deserialize(buf);
autoSynFolders.add(desc);
}
serverPublishedModInfo = buf.readBoolean();
}
@Environment(EnvType.CLIENT)
private void processAutoSyncFolder(final List<AutoSyncID> filesToRequest,
final List<AutoSyncID.ForDirectFileRequest> filesToRemove) {
if (!Configs.CLIENT_CONFIG.isAcceptingFiles()) {
return;
}
if (autoSynFolders.size() > 0) {
BCLib.LOGGER.info("Folders offered by Server:");
}
autoSynFolders.forEach(desc -> {
//desc contains the fileCache sent from the server, load the local version to get hold of the actual file cache on the client
SyncFolderDescriptor localDescriptor = AutoSync.getSyncFolderDescriptor(desc.folderID);
if (localDescriptor != null) {
BCLib.LOGGER.info(" - " + desc.folderID + " (" + desc.localFolder + ", allowRemove=" + desc.removeAdditionalFiles + ")");
localDescriptor.invalidateCache();
desc.relativeFilesStream()
.filter(desc::discardChildElements)
.forEach(subFile -> {
BCLib.LOGGER.warning(" * " + subFile.relPath + " (REJECTED)");
});
if (desc.removeAdditionalFiles) {
List<AutoSyncID.ForDirectFileRequest> additionalFiles = localDescriptor.relativeFilesStream()
.filter(subFile -> !desc.hasRelativeFile(
subFile))
.map(desc::mapAbsolute)
.filter(desc::acceptChildElements)
.map(absPath -> new AutoSyncID.ForDirectFileRequest(
desc.folderID,
absPath.toFile()))
.collect(Collectors.toList());
additionalFiles.forEach(aid -> BCLib.LOGGER.info(" * " + desc.localFolder.relativize(aid.relFile.toPath()) + " (missing on server)"));
filesToRemove.addAll(additionalFiles);
}
desc.relativeFilesStream()
.filter(desc::acceptChildElements)
.forEach(subFile -> {
SyncFolderDescriptor.SubFile localSubFile = localDescriptor.getLocalSubFile(subFile.relPath);
if (localSubFile != null) {
//the file exists locally, check if the hashes match
if (!localSubFile.hash.equals(subFile.hash)) {
BCLib.LOGGER.info(" * " + subFile.relPath + " (changed)");
filesToRequest.add(new AutoSyncID.ForDirectFileRequest(desc.folderID,
new File(subFile.relPath)));
} else {
BCLib.LOGGER.info(" * " + subFile.relPath);
}
} else {
//the file is missing locally
BCLib.LOGGER.info(" * " + subFile.relPath + " (missing on client)");
filesToRequest.add(new AutoSyncID.ForDirectFileRequest(desc.folderID,
new File(subFile.relPath)));
}
});
//free some memory
localDescriptor.invalidateCache();
} else {
BCLib.LOGGER.info(" - " + desc.folderID + " (Failed to find)");
}
});
}
@Environment(EnvType.CLIENT)
private void processSingleFileSync(final List<AutoSyncID> filesToRequest) {
final boolean debugHashes = Configs.CLIENT_CONFIG.shouldPrintDebugHashes();
if (autoSyncedFiles.size() > 0) {
BCLib.LOGGER.info("Files offered by Server:");
}
//Handle single sync files
//Single files need to be registered for sync on both client and server
//There are no restrictions to the target folder, but the client decides the final
//location.
for (AutoSync.AutoSyncTriple e : autoSyncedFiles) {
String actionString = "";
FileContentWrapper contentWrapper = new FileContentWrapper(e.serverContent);
if (e.localMatch == null) {
actionString = "(unknown source -> omitting)";
//filesToRequest.add(new AutoSyncID(e.serverHash.modID, e.serverHash.uniqueID));
} else if (e.localMatch.needTransfer.test(e.localMatch.getFileHash(), e.serverHash, contentWrapper)) {
actionString = "(prepare update)";
//we did not yet receive the new content
if (contentWrapper.getRawContent() == null) {
filesToRequest.add(new AutoSyncID(e.serverHash.modID, e.serverHash.uniqueID));
} else {
filesToRequest.add(new AutoSyncID.WithContentOverride(e.serverHash.modID,
e.serverHash.uniqueID,
contentWrapper,
e.localMatch.fileName));
}
}
BCLib.LOGGER.info(" - " + e + ": " + actionString);
if (debugHashes) {
BCLib.LOGGER.info(" * " + e.serverHash + " (Server)");
BCLib.LOGGER.info(" * " + e.localMatch.getFileHash() + " (Client)");
BCLib.LOGGER.info(" * local Content " + (contentWrapper.getRawContent() == null));
}
}
}
@Environment(EnvType.CLIENT)
private void processModFileSync(final List<AutoSyncID> filesToRequest, final Set<String> mismatchingMods) {
for (Entry<String, OfferedModInfo> e : modVersion.entrySet()) {
final String localVersion = ModUtil.convertModVersion(ModUtil.convertModVersion(ModUtil.getModVersion(e.getKey())));
final OfferedModInfo serverInfo = e.getValue();
ModInfo nfo = ModUtil.getModInfo(e.getKey());
final boolean clientOnly = nfo != null && nfo.metadata.getEnvironment() == ModEnvironment.CLIENT;
final boolean requestMod = !clientOnly && !serverInfo.version.equals(localVersion) && serverInfo.size > 0 && serverInfo.canDownload;
BCLib.LOGGER.info(" - " + e.getKey() + " (client=" + localVersion + ", server=" + serverInfo.version + ", size=" + PathUtil.humanReadableFileSize(
serverInfo.size) + (requestMod ? ", requesting" : "") + (serverInfo.canDownload
? ""
: ", not offered") + (clientOnly ? ", client only" : "") + ")");
if (requestMod) {
filesToRequest.add(new AutoSyncID.ForModFileRequest(e.getKey(), serverInfo.version));
}
if (!serverInfo.version.equals(localVersion)) {
mismatchingMods.add(e.getKey());
}
}
mismatchingMods.addAll(ModListScreen.localMissing(modVersion));
mismatchingMods.addAll(ModListScreen.serverMissing(modVersion));
}
@Override
protected boolean isBlocking() {
return true;
}
@Environment(EnvType.CLIENT)
@Override
protected void runOnClientGameThread(Minecraft client) {
if (!Configs.CLIENT_CONFIG.isAllowingAutoSync()) {
BCLib.LOGGER.info("Auto-Sync was disabled on the client.");
return;
}
final String localBclibVersion = getBCLibVersion();
BCLib.LOGGER.info("Received Hello from Server. (client=" + localBclibVersion + ", server=" + bclibVersion + ")");
if (ModUtil.convertModVersion(localBclibVersion) != ModUtil.convertModVersion(bclibVersion)) {
showBCLibError(client);
return;
}
final List<AutoSyncID> filesToRequest = new ArrayList<>(2);
final List<AutoSyncID.ForDirectFileRequest> filesToRemove = new ArrayList<>(2);
final Set<String> mismatchingMods = new HashSet<>(2);
processModFileSync(filesToRequest, mismatchingMods);
processSingleFileSync(filesToRequest);
processAutoSyncFolder(filesToRequest, filesToRemove);
//Handle folder sync
//Both client and server need to know about the folder you want to sync
//Files can only get placed within that folder
if ((filesToRequest.size() > 0 || filesToRemove.size() > 0) && (Configs.CLIENT_CONFIG.isAcceptingMods() || Configs.CLIENT_CONFIG.isAcceptingConfigs() || Configs.CLIENT_CONFIG.isAcceptingFiles())) {
showSyncFilesScreen(client, filesToRequest, filesToRemove);
return;
} else if (serverPublishedModInfo && mismatchingMods.size() > 0 && Configs.CLIENT_CONFIG.isShowingModInfo()) {
client.setScreen(new ModListScreen(client.screen,
Component.translatable("title.bclib.modmissmatch"),
Component.translatable("message.bclib.modmissmatch"),
CommonComponents.GUI_PROCEED,
ModUtil.getMods(),
modVersion));
return;
}
}
@Environment(EnvType.CLIENT)
protected void showBCLibError(Minecraft client) {
BCLib.LOGGER.error("BCLib differs on client and server.");
client.setScreen(new WarnBCLibVersionMismatch((download) -> {
if (download) {
requestBCLibDownload();
this.onCloseSyncFilesScreen();
} else {
Minecraft.getInstance()
.setScreen(null);
}
}));
}
@Environment(EnvType.CLIENT)
protected void showSyncFilesScreen(Minecraft client,
List<AutoSyncID> files,
final List<AutoSyncID.ForDirectFileRequest> filesToRemove) {
int configFiles = 0;
int singleFiles = 0;
int folderFiles = 0;
int modFiles = 0;
for (AutoSyncID aid : files) {
if (aid.isConfigFile()) {
configFiles++;
} else if (aid instanceof AutoSyncID.ForModFileRequest) {
modFiles++;
} else if (aid instanceof AutoSyncID.ForDirectFileRequest) {
folderFiles++;
} else {
singleFiles++;
}
}
client.setScreen(new SyncFilesScreen(modFiles,
configFiles,
singleFiles,
folderFiles,
filesToRemove.size(),
modVersion,
(downloadMods, downloadConfigs, downloadFiles, removeFiles) -> {
if (downloadMods || downloadConfigs || downloadFiles) {
BCLib.LOGGER.info("Updating local Files:");
List<AutoSyncID.WithContentOverride> localChanges = new ArrayList<>(
files.toArray().length);
List<AutoSyncID> requestFiles = new ArrayList<>(files.toArray().length);
files.forEach(aid -> {
if (aid.isConfigFile() && downloadConfigs) {
processOfferedFile(requestFiles, aid);
} else if (aid instanceof AutoSyncID.ForModFileRequest && downloadMods) {
processOfferedFile(requestFiles, aid);
} else if (downloadFiles) {
processOfferedFile(requestFiles, aid);
}
});
requestFileDownloads(requestFiles);
}
if (removeFiles) {
filesToRemove.forEach(aid -> {
BCLib.LOGGER.info(" - " + aid.relFile + " (removing)");
aid.relFile.delete();
});
}
this.onCloseSyncFilesScreen();
}));
}
@Environment(EnvType.CLIENT)
private void onCloseSyncFilesScreen() {
Minecraft.getInstance()
.setScreen(ChunkerProgress.getProgressScreen());
}
private void processOfferedFile(List<AutoSyncID> requestFiles, AutoSyncID aid) {
if (aid instanceof AutoSyncID.WithContentOverride) {
final AutoSyncID.WithContentOverride aidc = (AutoSyncID.WithContentOverride) aid;
BCLib.LOGGER.info(" - " + aid + " (updating Content)");
SendFiles.writeSyncedFile(aid, aidc.contentWrapper.getRawContent(), aidc.localFile);
} else {
requestFiles.add(aid);
BCLib.LOGGER.info(" - " + aid + " (requesting)");
}
}
private void requestBCLibDownload() {
BCLib.LOGGER.warning("Starting download of BCLib");
requestFileDownloads(List.of(new AutoSyncID.ForModFileRequest(BCLib.MOD_ID, bclibVersion)));
}
@Environment(EnvType.CLIENT)
private void requestFileDownloads(List<AutoSyncID> files) {
BCLib.LOGGER.info("Starting download of Files:" + files.size());
final ProgressScreen progress = new ProgressScreen(null,
Component.translatable("title.bclib.filesync.progress"),
Component.translatable("message.bclib.filesync.progress"));
progress.progressStart(Component.translatable("message.bclib.filesync.progress.stage.empty"));
ChunkerProgress.setProgressScreen(progress);
DataExchangeAPI.send(new RequestFiles(files));
}
}

View file

@ -0,0 +1,115 @@
package org.betterx.bclib.api.dataexchange.handler.autosync;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.MinecraftServer;
import net.minecraft.world.entity.player.Player;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
import net.fabricmc.fabric.api.networking.v1.PacketSender;
import org.betterx.bclib.BCLib;
import org.betterx.bclib.api.dataexchange.DataExchangeAPI;
import org.betterx.bclib.api.dataexchange.DataHandler;
import org.betterx.bclib.api.dataexchange.DataHandlerDescriptor;
import org.betterx.bclib.config.Configs;
import org.betterx.bclib.util.ModUtil;
import java.io.File;
/**
* This message is sent once a player enters the world. It initiates a sequence of Messages that will sync files between both
* client and server.
* <table>
* <caption>Description</caption>
* <tr>
* <th>Server</th>
* <th></th>
* <th>Client</th>
* <th></th>
* </tr>
* <tr>
* <td colspan="4">Player enters World</td>
* </tr>
* <tr>
* <td></td>
* <td>&lt;--</td>
* <td>{@link HelloServer}</td>
* <td>Sends the current BLib-Version installed on the Client</td>
* </tr>
* <tr>
* <td>{@link HelloClient}</td>
* <td>--&gt;</td>
* <td></td>
* <td>Sends the current BClIb-Version, the Version of all Plugins and data for all AutpoSync-Files
* ({@link DataExchangeAPI#addAutoSyncFile(String, File)} on the Server</td>
* </tr>
* <tr>
* <td></td>
* <td>&lt;--</td>
* <td>{@link RequestFiles}</td>
* <td>Request missing or out of sync Files from the Server</td>
* </tr>
* <tr>
* <td>{@link SendFiles}</td>
* <td>--&gt;</td>
* <td></td>
* <td>Send Files from the Server to the Client</td>
* </tr>
* </table>
*/
public class HelloServer extends DataHandler.FromClient {
public static final DataHandlerDescriptor DESCRIPTOR = new DataHandlerDescriptor(new ResourceLocation(BCLib.MOD_ID,
"hello_server"),
HelloServer::new,
true,
false);
protected String bclibVersion = "0.0.0";
public HelloServer() {
super(DESCRIPTOR.IDENTIFIER);
}
@Environment(EnvType.CLIENT)
@Override
protected boolean prepareDataOnClient() {
if (!Configs.CLIENT_CONFIG.isAllowingAutoSync()) {
BCLib.LOGGER.info("Auto-Sync was disabled on the client.");
return false;
}
return true;
}
@Environment(EnvType.CLIENT)
@Override
protected void serializeDataOnClient(FriendlyByteBuf buf) {
BCLib.LOGGER.info("Sending hello to server.");
buf.writeInt(ModUtil.convertModVersion(HelloClient.getBCLibVersion()));
}
@Override
protected void deserializeIncomingDataOnServer(FriendlyByteBuf buf, Player player, PacketSender responseSender) {
bclibVersion = ModUtil.convertModVersion(buf.readInt());
}
@Override
protected void runOnServerGameThread(MinecraftServer server, Player player) {
if (!Configs.SERVER_CONFIG.isAllowingAutoSync()) {
BCLib.LOGGER.info("Auto-Sync was disabled on the server.");
return;
}
String localBclibVersion = HelloClient.getBCLibVersion();
BCLib.LOGGER.info("Received Hello from Client. (server=" + localBclibVersion + ", client=" + bclibVersion + ")");
if (!server.isPublished()) {
BCLib.LOGGER.info("Auto-Sync is disabled for Singleplayer worlds.");
return;
}
reply(new HelloClient(), server);
}
}

View file

@ -0,0 +1,105 @@
package org.betterx.bclib.api.dataexchange.handler.autosync;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.MinecraftServer;
import net.minecraft.world.entity.player.Player;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
import net.fabricmc.fabric.api.networking.v1.PacketSender;
import org.betterx.bclib.BCLib;
import org.betterx.bclib.api.dataexchange.DataHandler;
import org.betterx.bclib.api.dataexchange.DataHandlerDescriptor;
import org.betterx.bclib.config.Configs;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
public class RequestFiles extends DataHandler.FromClient {
public static final DataHandlerDescriptor DESCRIPTOR = new DataHandlerDescriptor(new ResourceLocation(BCLib.MOD_ID,
"request_files"),
RequestFiles::new,
false,
false);
static String currentToken = "";
protected List<AutoSyncID> files;
private RequestFiles() {
this(null);
}
public RequestFiles(List<AutoSyncID> files) {
super(DESCRIPTOR.IDENTIFIER);
this.files = files;
}
@Environment(EnvType.CLIENT)
@Override
protected boolean prepareDataOnClient() {
if (!Configs.CLIENT_CONFIG.isAllowingAutoSync()) {
BCLib.LOGGER.info("Auto-Sync was disabled on the client.");
return false;
}
return true;
}
@Environment(EnvType.CLIENT)
@Override
protected void serializeDataOnClient(FriendlyByteBuf buf) {
newToken();
writeString(buf, currentToken);
buf.writeInt(files.size());
for (AutoSyncID a : files) {
a.serializeData(buf);
}
}
String receivedToken = "";
@Override
protected void deserializeIncomingDataOnServer(FriendlyByteBuf buf, Player player, PacketSender responseSender) {
receivedToken = readString(buf);
int size = buf.readInt();
files = new ArrayList<>(size);
BCLib.LOGGER.info("Client requested " + size + " Files:");
for (int i = 0; i < size; i++) {
AutoSyncID asid = AutoSyncID.deserializeData(buf);
files.add(asid);
BCLib.LOGGER.info(" - " + asid);
}
}
@Override
protected void runOnServerGameThread(MinecraftServer server, Player player) {
if (!Configs.SERVER_CONFIG.isAllowingAutoSync()) {
BCLib.LOGGER.info("Auto-Sync was disabled on the server.");
return;
}
List<AutoFileSyncEntry> syncEntries = files.stream()
.map(asid -> AutoFileSyncEntry.findMatching(asid))
.filter(e -> e != null)
.collect(Collectors.toList());
reply(new SendFiles(syncEntries, receivedToken), server);
}
public static void newToken() {
currentToken = UUID.randomUUID()
.toString();
}
static {
newToken();
}
}

View file

@ -0,0 +1,219 @@
package org.betterx.bclib.api.dataexchange.handler.autosync;
import net.minecraft.client.Minecraft;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.resources.ResourceLocation;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
import net.fabricmc.fabric.api.networking.v1.PacketSender;
import org.betterx.bclib.BCLib;
import org.betterx.bclib.api.dataexchange.DataHandler;
import org.betterx.bclib.api.dataexchange.DataHandlerDescriptor;
import org.betterx.bclib.config.Configs;
import org.betterx.bclib.gui.screens.ConfirmRestartScreen;
import org.betterx.bclib.util.Pair;
import org.betterx.bclib.util.PathUtil;
import org.betterx.bclib.util.Triple;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
public class SendFiles extends DataHandler.FromServer {
public static final DataHandlerDescriptor DESCRIPTOR = new DataHandlerDescriptor(new ResourceLocation(BCLib.MOD_ID,
"send_files"),
SendFiles::new,
false,
false);
protected List<AutoFileSyncEntry> files;
private String token;
public SendFiles() {
this(null, "");
}
public SendFiles(List<AutoFileSyncEntry> files, String token) {
super(DESCRIPTOR.IDENTIFIER);
this.files = files;
this.token = token;
}
@Override
protected boolean prepareDataOnServer() {
if (!Configs.SERVER_CONFIG.isAllowingAutoSync()) {
BCLib.LOGGER.info("Auto-Sync was disabled on the server.");
return false;
}
return true;
}
@Override
protected void serializeDataOnServer(FriendlyByteBuf buf) {
List<AutoFileSyncEntry> existingFiles = files.stream()
.filter(e -> e != null && e.fileName != null && e.fileName.exists())
.collect(Collectors.toList());
/*
//this will try to send a file that was not registered or requested by the client
existingFiles.add(new AutoFileSyncEntry("none", new File("D:\\MinecraftPlugins\\BetterNether\\run\\server.properties"),true,(a, b, content) -> {
System.out.println("Got Content:" + content.length);
return true;
}));*/
/*//this will try to send a folder-file that was not registered or requested by the client
existingFiles.add(new AutoFileSyncEntry.ForDirectFileRequest(DataExchange.SYNC_FOLDER.folderID, new File("test.json"), DataExchange.SYNC_FOLDER.mapAbsolute("test.json").toFile()));*/
/*//this will try to send a folder-file that was not registered or requested by the client and is outside the base-folder
existingFiles.add(new AutoFileSyncEntry.ForDirectFileRequest(DataExchange.SYNC_FOLDER.folderID, new File("../breakout.json"), DataExchange.SYNC_FOLDER.mapAbsolute("../breakout.json").toFile()));*/
writeString(buf, token);
buf.writeInt(existingFiles.size());
BCLib.LOGGER.info("Sending " + existingFiles.size() + " Files to Client:");
for (AutoFileSyncEntry entry : existingFiles) {
int length = entry.serializeContent(buf);
BCLib.LOGGER.info(" - " + entry + " (" + PathUtil.humanReadableFileSize(length) + ")");
}
}
private List<Pair<AutoFileSyncEntry, byte[]>> receivedFiles;
@Environment(EnvType.CLIENT)
@Override
protected void deserializeIncomingDataOnClient(FriendlyByteBuf buf, PacketSender responseSender) {
if (Configs.CLIENT_CONFIG.isAcceptingConfigs() || Configs.CLIENT_CONFIG.isAcceptingFiles() || Configs.CLIENT_CONFIG.isAcceptingMods()) {
token = readString(buf);
if (!token.equals(RequestFiles.currentToken)) {
RequestFiles.newToken();
BCLib.LOGGER.error("Unrequested File Transfer!");
receivedFiles = new ArrayList<>(0);
return;
}
RequestFiles.newToken();
int size = buf.readInt();
receivedFiles = new ArrayList<>(size);
BCLib.LOGGER.info("Server sent " + size + " Files:");
for (int i = 0; i < size; i++) {
Triple<AutoFileSyncEntry, byte[], AutoSyncID> p = AutoFileSyncEntry.deserializeContent(buf);
if (p.first != null) {
final String type;
if (p.first.isConfigFile() && Configs.CLIENT_CONFIG.isAcceptingConfigs()) {
receivedFiles.add(p);
type = "Accepted Config ";
} else if (p.first instanceof AutoFileSyncEntry.ForModFileRequest && Configs.CLIENT_CONFIG.isAcceptingMods()) {
receivedFiles.add(p);
type = "Accepted Mod ";
} else if (Configs.CLIENT_CONFIG.isAcceptingFiles()) {
receivedFiles.add(p);
type = "Accepted File ";
} else {
type = "Ignoring ";
}
BCLib.LOGGER.info(" - " + type + p.first + " (" + PathUtil.humanReadableFileSize(p.second.length) + ")");
} else {
BCLib.LOGGER.error(" - Failed to receive File " + p.third + ", possibly sent from a Mod that is not installed on the client.");
}
}
}
}
@Environment(EnvType.CLIENT)
@Override
protected void runOnClientGameThread(Minecraft client) {
if (Configs.CLIENT_CONFIG.isAcceptingConfigs() || Configs.CLIENT_CONFIG.isAcceptingFiles() || Configs.CLIENT_CONFIG.isAcceptingMods()) {
BCLib.LOGGER.info("Writing Files:");
for (Pair<AutoFileSyncEntry, byte[]> entry : receivedFiles) {
final AutoFileSyncEntry e = entry.first;
final byte[] data = entry.second;
writeSyncedFile(e, data, e.fileName);
}
showConfirmRestart(client);
}
}
@Environment(EnvType.CLIENT)
static void writeSyncedFile(AutoSyncID e, byte[] data, File fileName) {
if (fileName != null && !PathUtil.isChildOf(PathUtil.GAME_FOLDER, fileName.toPath())) {
BCLib.LOGGER.error(fileName + " is not within game folder " + PathUtil.GAME_FOLDER);
return;
}
if (!PathUtil.MOD_BAK_FOLDER.toFile().exists()) {
PathUtil.MOD_BAK_FOLDER.toFile().mkdirs();
}
Path path = fileName != null ? fileName.toPath() : null;
Path removeAfter = null;
if (e instanceof AutoFileSyncEntry.ForModFileRequest mase) {
removeAfter = path;
int count = 0;
final String prefix = "_bclib_synced_";
String name = prefix + mase.modID + "_" + mase.version.replace(".", "_") + ".jar";
do {
if (path != null) {
//move to the same directory as the existing Mod
path = path.getParent()
.resolve(name);
} else {
//move to the default mode location
path = PathUtil.MOD_FOLDER.resolve(name);
}
count++;
name = prefix + mase.modID + "_" + mase.version.replace(".", "_") + "__" + String.format("%03d",
count) + ".jar";
} while (path.toFile().exists());
}
BCLib.LOGGER.info(" - Writing " + path + " (" + PathUtil.humanReadableFileSize(data.length) + ")");
try {
final File parentFile = path.getParent()
.toFile();
if (!parentFile.exists()) {
parentFile.mkdirs();
}
Files.write(path, data);
if (removeAfter != null) {
final String bakFileName = removeAfter.toFile().getName();
String collisionFreeName = bakFileName;
Path targetPath;
int count = 0;
do {
targetPath = PathUtil.MOD_BAK_FOLDER.resolve(collisionFreeName);
count++;
collisionFreeName = String.format("%03d", count) + "_" + bakFileName;
} while (targetPath.toFile().exists());
BCLib.LOGGER.info(" - Moving " + removeAfter + " to " + targetPath);
removeAfter.toFile().renameTo(targetPath.toFile());
}
AutoSync.didReceiveFile(e, fileName);
} catch (IOException ioException) {
BCLib.LOGGER.error(" --> Writing " + fileName + " failed: " + ioException);
}
}
@Environment(EnvType.CLIENT)
protected void showConfirmRestart(Minecraft client) {
client.setScreen(new ConfirmRestartScreen(() -> {
Minecraft.getInstance()
.setScreen(null);
client.stop();
}));
}
}

View file

@ -0,0 +1,214 @@
package org.betterx.bclib.api.dataexchange.handler.autosync;
import net.minecraft.network.FriendlyByteBuf;
import org.betterx.bclib.BCLib;
import org.betterx.bclib.api.dataexchange.DataHandler;
import org.betterx.bclib.api.dataexchange.FileHash;
import org.betterx.bclib.api.dataexchange.handler.autosync.AutoSyncID.ForDirectFileRequest;
import org.betterx.bclib.config.Configs;
import org.betterx.bclib.util.PathUtil;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Stream;
import org.jetbrains.annotations.NotNull;
public class SyncFolderDescriptor {
static class SubFile {
public final String relPath;
public final FileHash hash;
SubFile(String relPath, FileHash hash) {
this.relPath = relPath;
this.hash = hash;
}
@Override
public String toString() {
return relPath;
}
public void serialize(FriendlyByteBuf buf) {
DataHandler.writeString(buf, relPath);
hash.serialize(buf);
}
public static SubFile deserialize(FriendlyByteBuf buf) {
final String relPath = DataHandler.readString(buf);
FileHash hash = FileHash.deserialize(buf);
return new SubFile(relPath, hash);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o instanceof String) return relPath.equals(o);
if (!(o instanceof SubFile)) return false;
SubFile subFile = (SubFile) o;
return relPath.equals(subFile.relPath);
}
@Override
public int hashCode() {
return relPath.hashCode();
}
}
@NotNull
public final String folderID;
public final boolean removeAdditionalFiles;
@NotNull
public final Path localFolder;
private List<SubFile> fileCache;
public SyncFolderDescriptor(String folderID, Path localFolder, boolean removeAdditionalFiles) {
this.removeAdditionalFiles = removeAdditionalFiles;
this.folderID = folderID;
this.localFolder = localFolder;
fileCache = null;
}
@Override
public String toString() {
return "SyncFolderDescriptor{" + "folderID='" + folderID + '\'' + ", removeAdditionalFiles=" + removeAdditionalFiles + ", localFolder=" + localFolder + ", files=" + (
fileCache == null
? "?"
: fileCache.size()) + "}";
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o instanceof String) {
return folderID.equals(o);
}
if (o instanceof ForDirectFileRequest) {
return folderID.equals(((ForDirectFileRequest) o).uniqueID);
}
if (!(o instanceof SyncFolderDescriptor)) return false;
SyncFolderDescriptor that = (SyncFolderDescriptor) o;
return folderID.equals(that.folderID);
}
@Override
public int hashCode() {
return folderID.hashCode();
}
public int fileCount() {
return fileCache == null ? 0 : fileCache.size();
}
public void invalidateCache() {
fileCache = null;
}
public void loadCache() {
if (fileCache == null) {
fileCache = new ArrayList<>(8);
PathUtil.fileWalker(localFolder.toFile(), p -> fileCache.add(new SubFile(localFolder.relativize(p)
.toString(),
FileHash.create(p.toFile()))));
/*//this tests if we can trick the system to load files that are not beneath the base-folder
if (!BCLib.isClient()) {
fileCache.add(new SubFile("../breakout.json", FileHash.create(mapAbsolute("../breakout.json").toFile())));
}*/
}
}
public void serialize(FriendlyByteBuf buf) {
final boolean debugHashes = Configs.CLIENT_CONFIG.getBoolean(AutoSync.SYNC_CATEGORY, "debugHashes", false);
loadCache();
DataHandler.writeString(buf, folderID);
buf.writeBoolean(removeAdditionalFiles);
buf.writeInt(fileCache.size());
fileCache.forEach(fl -> {
BCLib.LOGGER.info(" - " + fl.relPath);
if (debugHashes) {
BCLib.LOGGER.info(" " + fl.hash);
}
fl.serialize(buf);
});
}
public static SyncFolderDescriptor deserialize(FriendlyByteBuf buf) {
final String folderID = DataHandler.readString(buf);
final boolean remAddFiles = buf.readBoolean();
final int count = buf.readInt();
SyncFolderDescriptor localDescriptor = AutoSync.getSyncFolderDescriptor(folderID);
final SyncFolderDescriptor desc;
if (localDescriptor != null) {
desc = new SyncFolderDescriptor(folderID,
localDescriptor.localFolder,
localDescriptor.removeAdditionalFiles && remAddFiles);
desc.fileCache = new ArrayList<>(count);
} else {
BCLib.LOGGER.warning(BCLib.isClient()
? "Client"
: "Server" + " does not know Sync-Folder ID '" + folderID + "'");
desc = null;
}
for (int i = 0; i < count; i++) {
SubFile relPath = SubFile.deserialize(buf);
if (desc != null) desc.fileCache.add(relPath);
}
return desc;
}
//Note: make sure loadCache was called before using this
boolean hasRelativeFile(String relFile) {
return fileCache.stream()
.filter(sf -> sf.equals(relFile))
.findFirst()
.isPresent();
}
//Note: make sure loadCache was called before using this
boolean hasRelativeFile(SubFile subFile) {
return hasRelativeFile(subFile.relPath);
}
//Note: make sure loadCache was called before using this
SubFile getLocalSubFile(String relPath) {
return fileCache.stream()
.filter(sf -> sf.relPath.equals(relPath))
.findFirst()
.orElse(null);
}
Stream<SubFile> relativeFilesStream() {
loadCache();
return fileCache.stream();
}
public Path mapAbsolute(String relPath) {
return this.localFolder.resolve(relPath)
.normalize();
}
public Path mapAbsolute(SubFile subFile) {
return this.localFolder.resolve(subFile.relPath)
.normalize();
}
public boolean acceptChildElements(Path absPath) {
return PathUtil.isChildOf(this.localFolder, absPath);
}
public boolean acceptChildElements(SubFile subFile) {
return acceptChildElements(mapAbsolute(subFile));
}
public boolean discardChildElements(SubFile subFile) {
return !acceptChildElements(subFile);
}
}