Merge pull request #26 from quiqueck/feature/networking

Auto File Sync
This commit is contained in:
paulevsGitch 2021-08-07 01:38:43 +03:00 committed by GitHub
commit b2484cfd96
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
37 changed files with 1666 additions and 333 deletions

View file

@ -7,7 +7,11 @@ import net.minecraft.resources.ResourceLocation;
import ru.bclib.api.TagAPI;
import ru.bclib.api.WorldDataAPI;
import ru.bclib.api.dataexchange.DataExchangeAPI;
import ru.bclib.api.dataexchange.DataHandler;
import ru.bclib.api.dataexchange.handler.HelloClient;
import ru.bclib.api.dataexchange.handler.HelloServer;
import ru.bclib.api.dataexchange.handler.RequestFiles;
import ru.bclib.api.dataexchange.handler.SendFiles;
import ru.bclib.config.Configs;
import ru.bclib.recipes.CraftingRecipes;
import ru.bclib.registry.BaseBlockEntities;
@ -15,6 +19,9 @@ import ru.bclib.registry.BaseRegistry;
import ru.bclib.util.Logger;
import ru.bclib.world.surface.BCLSurfaceBuilders;
import java.util.ArrayList;
import java.util.List;
public class BCLib implements ModInitializer {
public static final String MOD_ID = "bclib";
public static final Logger LOGGER = new Logger(MOD_ID);
@ -28,7 +35,13 @@ public class BCLib implements ModInitializer {
CraftingRecipes.init();
WorldDataAPI.registerModCache(MOD_ID);
Configs.save();
DataExchangeAPI.registerDescriptor(HelloServer.DESCRIPTOR);
DataExchangeAPI.registerDescriptors(List.of(
HelloClient.DESCRIPTOR,
HelloServer.DESCRIPTOR,
RequestFiles.DESCRIPTOR,
SendFiles.DESCRIPTOR
));
}
public static boolean isDevEnvironment() {

View file

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

View file

@ -8,11 +8,15 @@ import net.minecraft.client.Minecraft;
import net.minecraft.client.multiplayer.ClientPacketListener;
import net.minecraft.network.FriendlyByteBuf;
import ru.bclib.BCLib;
import ru.bclib.api.dataexchange.handler.DataExchange;
/**
* This is an internal class that handles a Clienetside players Connection to a Server
*/
@Environment(EnvType.CLIENT)
class ConnectorClientside extends Connector {
public class ConnectorClientside extends Connector {
private Minecraft client;
ConnectorClientside(DataExchangeAPI api) {
ConnectorClientside(DataExchange api) {
super(api);
this.client = null;
}
@ -23,7 +27,7 @@ class ConnectorClientside extends Connector {
return true;
}
protected void onPlayInit(ClientPacketListener handler, Minecraft client){
public void onPlayInit(ClientPacketListener handler, Minecraft client){
if (this.client!=null && this.client != client){
BCLib.LOGGER.warning("Client changed!");
}
@ -34,8 +38,8 @@ class ConnectorClientside extends Connector {
});
}
}
void onPlayReady(ClientPacketListener handler, PacketSender sender, Minecraft client){
public void onPlayReady(ClientPacketListener handler, PacketSender sender, Minecraft client){
for(DataHandlerDescriptor desc : getDescriptors()){
if (desc.sendOnJoin){
DataHandler h = desc.JOIN_INSTANCE.get();
@ -45,8 +49,8 @@ class ConnectorClientside extends Connector {
}
}
}
void onPlayDisconnect(ClientPacketListener handler, Minecraft client){
public void onPlayDisconnect(ClientPacketListener handler, Minecraft client){
for(DataHandlerDescriptor desc : getDescriptors()) {
ClientPlayNetworking.unregisterReceiver(desc.IDENTIFIER);
}
@ -57,7 +61,7 @@ class ConnectorClientside extends Connector {
h.receiveFromServer(client, handler, buf, responseSender);
}
void sendToServer(DataHandler h){
public void sendToServer(DataHandler h){
if (client==null){
throw new RuntimeException("[internal error] Client not initialized yet!");
}

View file

@ -7,10 +7,14 @@ import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.server.network.ServerGamePacketListenerImpl;
import ru.bclib.BCLib;
import ru.bclib.api.dataexchange.handler.DataExchange;
class ConnectorServerside extends Connector {
/**
* This is an internal class that handles a Serverside Connection to a Client-Player
*/
public class ConnectorServerside extends Connector {
private MinecraftServer server;
ConnectorServerside(DataExchangeAPI api) {
ConnectorServerside(DataExchange api) {
super(api);
server = null;
}
@ -20,7 +24,7 @@ class ConnectorServerside extends Connector {
return false;
}
protected void onPlayInit(ServerGamePacketListenerImpl handler, MinecraftServer server){
public void onPlayInit(ServerGamePacketListenerImpl handler, MinecraftServer server){
if (this.server!=null && this.server != server){
BCLib.LOGGER.warning("Server changed!");
}
@ -31,8 +35,8 @@ class ConnectorServerside extends Connector {
});
}
}
void onPlayReady(ServerGamePacketListenerImpl handler, PacketSender sender, MinecraftServer server){
public void onPlayReady(ServerGamePacketListenerImpl handler, PacketSender sender, MinecraftServer server){
for(DataHandlerDescriptor desc : getDescriptors()){
if (desc.sendOnJoin){
DataHandler h = desc.JOIN_INSTANCE.get();
@ -42,8 +46,8 @@ class ConnectorServerside extends Connector {
}
}
}
void onPlayDisconnect(ServerGamePacketListenerImpl handler, MinecraftServer server){
public void onPlayDisconnect(ServerGamePacketListenerImpl handler, MinecraftServer server){
for(DataHandlerDescriptor desc : getDescriptors()){
ServerPlayNetworking.unregisterReceiver(handler, desc.IDENTIFIER);
}
@ -54,7 +58,7 @@ class ConnectorServerside extends Connector {
h.receiveFromClient(server, player, handler, buf, responseSender);
}
void sendToClient(DataHandler h){
public void sendToClient(DataHandler h){
if (server==null){
throw new RuntimeException("[internal error] Server not initialized yet!");
}

View file

@ -3,119 +3,161 @@ package ru.bclib.api.dataexchange;
import com.google.common.collect.Lists;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
import net.fabricmc.fabric.api.client.networking.v1.ClientLoginConnectionEvents;
import net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents;
import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents;
import net.minecraft.network.FriendlyByteBuf;
import ru.bclib.BCLib;
import ru.bclib.api.dataexchange.handler.DataExchange;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.function.Function;
import java.util.function.Predicate;
public class DataExchangeAPI {
private final static List<String> MODS = Lists.newArrayList();
private static DataExchangeAPI instance;
private ConnectorServerside server;
private ConnectorClientside client;
protected final Set<DataHandlerDescriptor> descriptors;
private DataExchangeAPI(){
descriptors = new HashSet<>();
}
static DataExchangeAPI getInstance(){
if (instance==null){
instance = new DataExchangeAPI();
}
return instance;
}
@Environment(EnvType.CLIENT)
private void initClientside(){
if (client!=null) return;
client = new ConnectorClientside(this);
ClientPlayConnectionEvents.INIT.register(client::onPlayInit);
ClientPlayConnectionEvents.JOIN.register(client::onPlayReady);
ClientPlayConnectionEvents.DISCONNECT.register(client::onPlayDisconnect);
}
private void initServerSide(){
if (server!=null) return;
server = new ConnectorServerside(this);
ServerPlayConnectionEvents.INIT.register(server::onPlayInit);
ServerPlayConnectionEvents.JOIN.register(server::onPlayReady);
ServerPlayConnectionEvents.DISCONNECT.register(server::onPlayDisconnect);
}
/**
* Register a mod to participate in the DataExchange.
*
* @param modID - {@link String} modID.
*/
public static void registerMod(String modID) {
MODS.add(modID);
}
/**
* Returns the IDs of all registered Mods.
* @return List of modIDs
*/
public static List<String> registeredMods(){
return MODS;
}
/**
* Add a new Descriptor for a DataHandler.
* @param desc The Descriptor you want to add.
*/
public static void registerDescriptor(DataHandlerDescriptor desc){
DataExchangeAPI api = DataExchangeAPI.getInstance();
api.descriptors.add(desc);
}
/**
* 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(){
DataExchangeAPI api = DataExchangeAPI.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(){
DataExchangeAPI api = DataExchangeAPI.getInstance();
api.initServerSide();
}
/**
* 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)} is called just before the data is sent. You should
* use this method to add the Data you need to the communication.
* @param h The Data that you want to send
*/
public static void send(DataHandler h){
if (h.getOriginatesOnServer()){
DataExchangeAPI.getInstance().server.sendToClient(h);
} else {
DataExchangeAPI.getInstance().client.sendToServer(h);
}
}
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) {
MODS.add(modID);
}
/**
* 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)} is called just before the data is sent. You should
* use this method to add the Data you need to the communication.
*
* @param h The Data that you want to send
*/
public static void send(DataHandler h) {
if (h.getOriginatesOnServer()) {
DataExchangeAPI.getInstance().server.sendToClient(h);
} else {
DataExchangeAPI.getInstance().client.sendToServer(h);
}
}
/**
* Registers a File for automatic client syncing.
*
* @param modID The ID of the calling Mod
* @param fileName The name of the File
*/
public static void addAutoSyncFile(String modID, File fileName) {
getInstance().addAutoSyncFileData(modID, fileName, false, FileHash.NEED_TRANSFER);
}
/**
* Registers a File for automatic client syncing.
* <p>
* The file is synced of the {@link FileHash} on client and server are not equal. This method will not copy the
* configs content from the client to the server.
*
* @param modID The ID of the calling Mod
* @param uniqueID A unique Identifier for the File. (see {@link ru.bclib.api.dataexchange.FileHash#uniqueID} for
* Details
* @param fileName The name of the File
*/
public static void addAutoSyncFile(String modID, String uniqueID, File fileName) {
getInstance().addAutoSyncFileData(modID, uniqueID, fileName, false, FileHash.NEED_TRANSFER);
}
/**
* Registers a File for automatic client syncing.
* <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 ru.bclib.api.dataexchange.FileHash}
* for comparison is sufficient.
*
* @param modID The ID of the calling Mod
* @param fileName The name of the File
* @param needTransfer If the predicate returns true, the file needs to get copied to the server.
*/
public static void addAutoSyncFile(String modID, File fileName, NeedTransferPredicate needTransfer) {
getInstance().addAutoSyncFileData(modID, fileName, true, needTransfer);
}
/**
* Registers a File for automatic client syncing.
* <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 ru.bclib.api.dataexchange.FileHash}
* for comparison is sufficient.
*
* @param modID The ID of the calling Mod
* @param uniqueID A unique Identifier for the File. (see {@link ru.bclib.api.dataexchange.FileHash#uniqueID} for
* Details
* @param fileName The name of the File
* @param needTransfer If the predicate returns true, the file needs to get copied to the server.
*/
public static void addAutoSyncFile(String modID, String uniqueID, File fileName, NeedTransferPredicate needTransfer) {
getInstance().addAutoSyncFileData(modID, uniqueID, fileName, true, needTransfer);
}
}

View file

@ -38,29 +38,30 @@ public abstract class DataHandler {
@Environment(EnvType.CLIENT)
void receiveFromServer(Minecraft client, ClientPacketListener handler, FriendlyByteBuf buf, PacketSender responseSender){
deserializeFromIncomingData(buf, responseSender, false);
client.execute(() -> runOnClient(client));
}
void receiveFromClient(MinecraftServer server, ServerPlayer player, ServerGamePacketListenerImpl handler, FriendlyByteBuf buf, PacketSender responseSender){
deserializeFromIncomingData(buf, responseSender, true);
server.execute(() -> runOnServer(server));
client.execute(() -> runOnGameThread(client, null, true));
}
protected void deserializeFromIncomingData(FriendlyByteBuf buf, PacketSender responseSender, boolean fromClient){
private ServerPlayer lastMessageSender;
void receiveFromClient(MinecraftServer server, ServerPlayer player, ServerGamePacketListenerImpl handler, FriendlyByteBuf buf, PacketSender responseSender){
lastMessageSender = player;
deserializeFromIncomingData(buf, responseSender, false);
server.execute(() -> runOnGameThread(null, server, false));
}
@Environment(EnvType.CLIENT)
protected void runOnClient(Minecraft client){
}
protected void runOnServer(MinecraftServer server){
}
protected void serializeData(FriendlyByteBuf buf) {
}
protected void deserializeFromIncomingData(FriendlyByteBuf buf, PacketSender responseSender, boolean isClient){
}
protected void runOnGameThread(Minecraft client, MinecraftServer server, boolean isClient){
}
final protected boolean reply(DataHandler message, MinecraftServer server){
if (lastMessageSender==null) return false;
message.sendToClient(server, lastMessageSender);
return true;
}
void sendToClient(MinecraftServer server){

View file

@ -1,26 +1,32 @@
package ru.bclib.api.dataexchange;
import net.minecraft.resources.ResourceLocation;
import ru.bclib.api.dataexchange.handler.HelloClient;
import ru.bclib.api.dataexchange.handler.HelloServer;
import ru.bclib.api.dataexchange.handler.RequestFiles;
import ru.bclib.api.dataexchange.handler.SendFiles;
import java.util.function.Supplier;
public class DataHandlerDescriptor {
public DataHandlerDescriptor(ResourceLocation identifier, Supplier<DataHandler> instancer){
this(identifier, instancer, instancer, false);
this(identifier, instancer, instancer, false, false);
}
public DataHandlerDescriptor(ResourceLocation identifier, Supplier<DataHandler> instancer, boolean sendOnJoin){
this(identifier, instancer, instancer, sendOnJoin);
public DataHandlerDescriptor(ResourceLocation identifier, Supplier<DataHandler> instancer, boolean sendOnJoin, boolean sendBeforeEnter){
this(identifier, instancer, instancer, sendOnJoin, sendBeforeEnter);
}
public DataHandlerDescriptor(ResourceLocation identifier, Supplier<DataHandler> receiv_instancer, Supplier<DataHandler> join_instancer, boolean sendOnJoin){
public DataHandlerDescriptor(ResourceLocation identifier, Supplier<DataHandler> receiv_instancer, Supplier<DataHandler> 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;
public final ResourceLocation IDENTIFIER;
public final Supplier<DataHandler> INSTANCE;
public final Supplier<DataHandler> JOIN_INSTANCE;

View file

@ -0,0 +1,196 @@
package ru.bclib.api.dataexchange;
import net.minecraft.network.FriendlyByteBuf;
import org.jetbrains.annotations.NotNull;
import ru.bclib.BCLib;
import ru.bclib.api.dataexchange.handler.DataExchange;
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 java.util.function.Predicate;
/**
* 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 FileHash extends DataExchange.AutoSyncID {
/**
* 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(String modID, File file, byte[] md5, int size, int value) {
this(modID, file.getName(), md5, size, value);
}
FileHash(String modID, String uniqueID, byte[] md5, int size, int value) {
super(modID, uniqueID);
Objects.nonNull(md5);
this.md5 = md5;
this.size = size;
this.value = value;
}
private static int ERR_DOES_NOT_EXIST = -10;
private static int ERR_IO_ERROR = -20;
static FileHash createForEmpty(String modID, String uniqueID, int errCode){
return new FileHash(modID, uniqueID, new byte[0], 0, errCode);
}
final static DataExchange.NeedTransferPredicate NEED_TRANSFER = (clientHash, serverHash, content)-> !clientHash.equals(serverHash);
public boolean noFile() {
return md5.length == 0;
}
@Override
public String toString() {
return super.toString()+": "+String.format("%08x", size)
+ "-"
+ String.format("%08x", value)
+ "-"
+ getMd5String();
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
FileHash fileHash = (FileHash) o;
return size == fileHash.size && value == fileHash.value && Arrays.equals(md5, fileHash.md5) && uniqueID.equals(fileHash.uniqueID) && modID.equals(fileHash.modID);
}
@Override
public int hashCode() {
int result = Objects.hash(size, value, uniqueID, modID);
result = 31 * result + Arrays.hashCode(md5);
return result;
}
/**
* Convert the md5-hash to a human readable string
* @return The converted String
*/
public String getMd5String(){
return toHexString(md5);
}
/**
* 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);
DataHandler.writeString(buf, modID);
DataHandler.writeString(buf, uniqueID);
}
/**
*Deserialize a Buffer to a new {@link FileHash}-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();
final String modID = DataHandler.readString(buf);
final String uniqueID = DataHandler.readString(buf);
return new FileHash(modID, uniqueID, md5, size, value);
}
/**
* 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}.
* <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 FileHash create(String modID, File file){
return create(modID, file, file.getName());
}
/**
* Create a new {@link FileHash}.
* @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 FileHash#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 FileHash create(String modID, File file, String uniqueID){
if (!file.exists()) return createForEmpty(modID, uniqueID, 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(modID, uniqueID, 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(modID, uniqueID, ERR_IO_ERROR);
}
}

View file

@ -0,0 +1,124 @@
package ru.bclib.api.dataexchange.handler;
import net.minecraft.network.FriendlyByteBuf;
import ru.bclib.api.dataexchange.DataHandler;
import ru.bclib.api.dataexchange.FileHash;
import ru.bclib.util.Pair;
import ru.bclib.util.Triple;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
class AutoFileSyncEntry extends DataExchange.AutoSyncID {
public final DataExchange.NeedTransferPredicate needTransfer;
public final File fileName;
public final boolean requestContent;
private FileHash hash;
AutoFileSyncEntry(String modID, File fileName, boolean requestContent, DataExchange.NeedTransferPredicate needTransfer) {
this(modID, fileName.getName(), fileName, requestContent, needTransfer);
}
AutoFileSyncEntry(String modID, String uniqueID, File fileName, boolean requestContent, DataExchange.NeedTransferPredicate needTransfer) {
super(modID, uniqueID);
this.needTransfer = needTransfer;
this.fileName = fileName;
this.requestContent = requestContent;
}
public FileHash getFileHash() {
if (hash == null)
{
hash = FileHash.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[], DataExchange.AutoSyncID> deserializeContent(FriendlyByteBuf buf) {
final String modID = DataHandler.readString(buf);
final String uniqueID = DataHandler.readString(buf);
byte[] data = deserializeFileContent(buf);
AutoFileSyncEntry entry = AutoFileSyncEntry.findMatching(modID, uniqueID);
return new Triple<>(entry, data, new DataExchange.AutoSyncID(modID, uniqueID));
}
public void serialize(FriendlyByteBuf buf) {
getFileHash().serialize(buf);
buf.writeBoolean(requestContent);
if (requestContent) {
serializeFileContent(buf);
}
}
public static DataExchange.AutoSyncTriple deserializeAndMatch(FriendlyByteBuf buf) {
Pair<FileHash, byte[]> e = deserialize(buf);
AutoFileSyncEntry match = findMatching(e.first);
return new DataExchange.AutoSyncTriple(e.first, e.second, match);
}
public static Pair<FileHash, byte[]> deserialize(FriendlyByteBuf buf) {
FileHash hash = FileHash.deserialize(buf);
boolean withContent = buf.readBoolean();
byte[] data = null;
if (withContent) {
data = deserializeFileContent(buf);
}
return new Pair(hash, data);
}
private int serializeFileContent(FriendlyByteBuf buf) {
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(FileHash hash) {
return findMatching(hash.modID, hash.uniqueID);
}
public static AutoFileSyncEntry findMatching(DataExchange.AutoSyncID aid) {
return findMatching(aid.modID, aid.uniqueID);
}
public static AutoFileSyncEntry findMatching(String modID, String uniqueID) {
return DataExchange
.getInstance()
.autoSyncFiles
.stream()
.filter(asf -> asf.modID.equals(modID) && asf.uniqueID.equals(uniqueID))
.findFirst()
.orElse(null);
}
}

View file

@ -0,0 +1,208 @@
package ru.bclib.api.dataexchange.handler;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
import net.fabricmc.fabric.api.client.networking.v1.ClientLoginConnectionEvents;
import net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents;
import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents;
import org.jetbrains.annotations.NotNull;
import ru.bclib.api.dataexchange.ConnectorClientside;
import ru.bclib.api.dataexchange.ConnectorServerside;
import ru.bclib.api.dataexchange.DataExchangeAPI;
import ru.bclib.api.dataexchange.DataHandler;
import ru.bclib.api.dataexchange.DataHandlerDescriptor;
import ru.bclib.api.dataexchange.FileHash;
import ru.bclib.util.Pair;
import ru.bclib.util.Triple;
import java.io.File;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
abstract public class DataExchange {
@FunctionalInterface
public interface NeedTransferPredicate {
public boolean test(FileHash clientHash, FileHash serverHash, byte[] content);
}
public static class AutoSyncID {
/**
* A Unique ID for the referenced File.
* <p>
* Files with the same {@link #modID} need to have a unique IDs. Normally the filename from {@link #FileHash(String, File, byte[], int, int)}
* is used to generated that ID, but you can directly specify one using {@link #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 == null || getClass() != o.getClass()) return false;
AutoSyncID that = (AutoSyncID) o;
return uniqueID.equals(that.uniqueID) && modID.equals(that.modID);
}
@Override
public int hashCode() {
return Objects.hash(uniqueID, modID);
}
}
final static class AutoSyncTriple extends Triple<FileHash, byte[], AutoFileSyncEntry>{
public AutoSyncTriple(FileHash first, byte[] second, AutoFileSyncEntry third) {
super(first, second, third);
}
@Override
public String toString() {
return first.modID+"."+first.uniqueID;
}
}
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;
protected final List<AutoFileSyncEntry> autoSyncFiles = new ArrayList<>(4);
abstract protected ConnectorClientside clientSupplier(DataExchange api);
abstract protected ConnectorServerside serverSupplier(DataExchange api);
protected DataExchange(){
descriptors = new HashSet<>();
}
public Set<DataHandlerDescriptor> getDescriptors() { return descriptors; }
@Environment(EnvType.CLIENT)
protected void initClientside(){
if (client!=null) return;
client = clientSupplier(this);
ClientLoginConnectionEvents.INIT.register((a, b) ->{
System.out.println("INIT");
});
ClientLoginConnectionEvents.QUERY_START.register((a, b) ->{
System.out.println("INIT");
});
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){
DataHandler h = desc.JOIN_INSTANCE.get();
if (!h.getOriginatesOnServer()) {
getInstance().client.sendToServer(h);
}
}
});
}
/**
* 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 ru.bclib.api.dataexchange.FileHash}
* for comparison is sufficient.
*/
protected void addAutoSyncFileData(String modID, File fileName, boolean requestContent, NeedTransferPredicate needTransfer){
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 ru.bclib.api.dataexchange.FileHash#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 ru.bclib.api.dataexchange.FileHash}
* for comparison is sufficient.
*/
protected void addAutoSyncFileData(String modID, String uniqueID, File fileName, boolean requestContent, NeedTransferPredicate needTransfer){
autoSyncFiles.add(new AutoFileSyncEntry(modID, uniqueID, fileName, requestContent, needTransfer));
}
}

View file

@ -0,0 +1,182 @@
package ru.bclib.api.dataexchange.handler;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
import net.fabricmc.fabric.api.networking.v1.PacketSender;
import net.fabricmc.loader.api.FabricLoader;
import net.fabricmc.loader.api.ModContainer;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.screens.Screen;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.MinecraftServer;
import ru.bclib.BCLib;
import ru.bclib.api.dataexchange.DataExchangeAPI;
import ru.bclib.api.dataexchange.DataHandler;
import ru.bclib.api.dataexchange.DataHandlerDescriptor;
import ru.bclib.api.dataexchange.handler.DataExchange.AutoSyncID;
import ru.bclib.api.datafixer.DataFixerAPI;
import ru.bclib.gui.screens.SyncFilesScreen;
import ru.bclib.gui.screens.WarnBCLibVersionMismatch;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.function.Consumer;
/**
* Sent from the Server to the Client.
* <p>
* For Details refer to {@link HelloServer}
*/
public class HelloClient extends DataHandler {
public static DataHandlerDescriptor DESCRIPTOR = new DataHandlerDescriptor(new ResourceLocation(BCLib.MOD_ID, "hello_client"), HelloClient::new, false, false);
public HelloClient() {
super(DESCRIPTOR.IDENTIFIER, true);
}
public static String getModVersion(String modID){
Optional<ModContainer> optional = FabricLoader.getInstance().getModContainer(modID);
if (optional.isPresent()) {
ModContainer modContainer = optional.get();
return modContainer.getMetadata().getVersion().toString();
}
return "0.0.0";
}
static String getBCLibVersion(){
return getModVersion(BCLib.MOD_ID);
}
@Override
protected void serializeData(FriendlyByteBuf buf) {
final String vbclib = getBCLibVersion();
BCLib.LOGGER.info("Sending Hello to Client. (server="+vbclib+")");
final List<String> mods = DataExchangeAPI.registeredMods();
//write BCLibVersion (=protocol version)
buf.writeInt(DataFixerAPI.getModVersion(vbclib));
//write Plugin Versions
buf.writeInt(mods.size());
for (String modID : mods) {
writeString(buf, modID);
final String ver = getModVersion(modID);
buf.writeInt(DataFixerAPI.getModVersion(ver));
BCLib.LOGGER.info(" - Listing Mod " + modID + " v" + ver);
}
//send config Data
final List<AutoFileSyncEntry> autoSyncFiles = DataExchange.getInstance().autoSyncFiles;
buf.writeInt(autoSyncFiles.size());
for (AutoFileSyncEntry entry : autoSyncFiles) {
//System.out.println("Serializing " + entry.getFileHash());
entry.serialize(buf);
BCLib.LOGGER.info(" - Offering File " + entry);
}
}
String bclibVersion ="0.0.0";
Map<String, String> modVersion = new HashMap<>();
List<DataExchange.AutoSyncTriple> autoSyncedFiles = null;
@Override
protected void deserializeFromIncomingData(FriendlyByteBuf buf, PacketSender responseSender, boolean fromClient) {
//read BCLibVersion (=protocol version)
bclibVersion = DataFixerAPI.getModVersion(buf.readInt());
//read Plugin Versions
modVersion = new HashMap<>();
int count = buf.readInt();
for (int i=0; i< count; i++) {
String id = readString(buf);
String version = DataFixerAPI.getModVersion(buf.readInt());
modVersion.put(id, version);
}
//read config Data
count = buf.readInt();
autoSyncedFiles = new ArrayList<>(count);
for (int i=0; i< count; i++) {
//System.out.println("Deserializing ");
DataExchange.AutoSyncTriple t = AutoFileSyncEntry.deserializeAndMatch(buf);
autoSyncedFiles.add(t);
//System.out.println(t.first);
}
}
@Override
protected void runOnGameThread(Minecraft client, MinecraftServer server, boolean isClient) {
String localBclibVersion = getBCLibVersion();
BCLib.LOGGER.info("Received Hello from Server. (client="+localBclibVersion+", server="+bclibVersion+")");
if (DataFixerAPI.getModVersion(localBclibVersion) != DataFixerAPI.getModVersion(bclibVersion)){
showBCLibError(client);
return;
}
List<AutoSyncID> filesToRequest = new ArrayList<>(4);
for (Entry<String, String> e : modVersion.entrySet()){
String ver = getModVersion(e.getKey());
BCLib.LOGGER.info(" - " + e.getKey() + " (client="+ver+", server="+ver+")");
}
BCLib.LOGGER.info("Server offered Files to sync.");
for (DataExchange.AutoSyncTriple e : autoSyncedFiles) {
boolean willRequest = false;
if (e.third == null) {
filesToRequest.add(new AutoSyncID(e.first.modID, e.first.uniqueID));
willRequest = true;
BCLib.LOGGER.info(" - File " + e + ": Does not exist on client.");
} else if (e.third.needTransfer.test(e.third.getFileHash(), e.first, e.second)) {
willRequest = true;
filesToRequest.add(new AutoSyncID(e.first.modID, e.first.uniqueID));
BCLib.LOGGER.info(" - File " + e + ": Needs Transfer");
}
BCLib.LOGGER.info(" - " + e + ": " + (willRequest ? " (requesting)":""));
}
if (filesToRequest.size()>0) {
showDonwloadConfigs(client, filesToRequest);
return;
}
}
@Environment(EnvType.CLIENT)
protected void showBCLibError(Minecraft client){
BCLib.LOGGER.error("BCLib differs on client and server.");
client.setScreen(new WarnBCLibVersionMismatch((download) -> {
Minecraft.getInstance().setScreen((Screen)null);
if (download){
requestBCLibDownload((hadErrors)->{
client.stop();
});
}
}));
}
@Environment(EnvType.CLIENT)
protected void showDonwloadConfigs(Minecraft client, List<AutoSyncID> files){
client.setScreen(new SyncFilesScreen((download) -> {
Minecraft.getInstance().setScreen((Screen)null);
if (download){
requestFileDownloads(files);
}
}));
}
private void requestBCLibDownload(Consumer<Boolean> whenFinished){
BCLib.LOGGER.warning("Starting download of BCLib");
whenFinished.accept(true);
}
private void requestFileDownloads(List<AutoSyncID> files){
BCLib.LOGGER.info("Starting download of Files:" + files.size());
DataExchangeAPI.send(new RequestFiles(files));
}
}

View file

@ -1,8 +1,7 @@
package ru.bclib.api.dataexchange.handler;
import net.fabricmc.fabric.api.networking.v1.PacketSender;
import net.fabricmc.loader.api.FabricLoader;
import net.fabricmc.loader.api.ModContainer;
import net.minecraft.client.Minecraft;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.MinecraftServer;
@ -12,67 +11,71 @@ import ru.bclib.api.dataexchange.DataHandler;
import ru.bclib.api.dataexchange.DataHandlerDescriptor;
import ru.bclib.api.datafixer.DataFixerAPI;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
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.
* <p>
* <tab<le>
* <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 {
public static DataHandlerDescriptor DESCRIPTOR = new DataHandlerDescriptor(new ResourceLocation(BCLib.MOD_ID, "hello_server"), HelloServer::new, true);
public static DataHandlerDescriptor DESCRIPTOR = new DataHandlerDescriptor(new ResourceLocation(BCLib.MOD_ID, "hello_server"), HelloServer::new, false, true);
protected String bclibVersion ="0.0.0";
public HelloServer() {
super(DESCRIPTOR.IDENTIFIER, false);
}
public static String getModVersion(String modID){
Optional<ModContainer> optional = FabricLoader.getInstance().getModContainer(modID);
if (optional.isPresent()) {
ModContainer modContainer = optional.get();
return modContainer.getMetadata().getVersion().toString();
}
return "0.0.0";
@Override
protected void serializeData(FriendlyByteBuf buf) {
buf.writeInt(DataFixerAPI.getModVersion(HelloClient.getBCLibVersion()));
}
protected static String getBCLibVersion(){
return getModVersion(BCLib.MOD_ID);
}
String bclibVersion ="0.0.0";
Map<String, String> modVersion = new HashMap<>();
@Override
protected void deserializeFromIncomingData(FriendlyByteBuf buf, PacketSender responseSender, boolean fromClient) {
bclibVersion = DataFixerAPI.getModVersion(buf.readInt());
modVersion = new HashMap<>();
int count = buf.readInt();
for (int i=0; i< count; i++){
String id = readString(buf);
String version = DataFixerAPI.getModVersion(buf.readInt());
modVersion.put(id, version);
}
}
@Override
protected void runOnServer(MinecraftServer server) {
String localBclibVersion = getBCLibVersion();
BCLib.LOGGER.info("Hello Server received from BCLib. (server="+localBclibVersion+", client="+bclibVersion+")");
for (Entry<String, String> e : modVersion.entrySet()){
String ver = getModVersion(e.getKey());
BCLib.LOGGER.info(" - " + e.getKey() + " (server="+ver+", client="+ver+")");
}
}
@Override
protected void serializeData(FriendlyByteBuf buf) {
final List<String> mods = DataExchangeAPI.registeredMods();
buf.writeInt(DataFixerAPI.getModVersion(getBCLibVersion()));
buf.writeInt(mods.size());
for (String modID : mods) {
writeString(buf, modID);
buf.writeInt(DataFixerAPI.getModVersion(getModVersion(modID)));
}
protected void runOnGameThread(Minecraft client, MinecraftServer server, boolean isClient) {
String localBclibVersion = HelloClient.getBCLibVersion();
BCLib.LOGGER.info("Received Hello from Client. (server="+localBclibVersion+", client="+bclibVersion+")");
reply(new HelloClient(), server);
}
}

View file

@ -0,0 +1,81 @@
package ru.bclib.api.dataexchange.handler;
import net.fabricmc.fabric.api.networking.v1.PacketSender;
import net.minecraft.client.Minecraft;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.MinecraftServer;
import ru.bclib.BCLib;
import ru.bclib.api.dataexchange.DataHandler;
import ru.bclib.api.dataexchange.DataHandlerDescriptor;
import ru.bclib.api.dataexchange.handler.DataExchange.AutoSyncID;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
public class RequestFiles extends DataHandler {
public static 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, false);
this.files = files;
}
@Override
protected void serializeData(FriendlyByteBuf buf) {
newToken();
writeString(buf, currentToken);
buf.writeInt(files.size());
for (AutoSyncID a : files){
writeString(buf, a.modID);
writeString(buf, a.uniqueID);
}
}
String receivedToken = "";
@Override
protected void deserializeFromIncomingData(FriendlyByteBuf buf, PacketSender responseSender, boolean fromClient) {
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++){
String modID = readString(buf);
String uID = readString(buf);
AutoSyncID asid = new AutoSyncID(modID, uID);
files.add(asid);
BCLib.LOGGER.info(" - " + asid);
}
}
@Override
protected void runOnGameThread(Minecraft client, MinecraftServer server, boolean isClient) {
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,111 @@
package ru.bclib.api.dataexchange.handler;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
import net.fabricmc.fabric.api.networking.v1.PacketSender;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.screens.Screen;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.MinecraftServer;
import ru.bclib.BCLib;
import ru.bclib.api.dataexchange.DataHandler;
import ru.bclib.api.dataexchange.DataHandlerDescriptor;
import ru.bclib.gui.screens.ConfirmRestartScreen;
import ru.bclib.util.Pair;
import ru.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 {
public static 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, true);
this.files = files;
this.token = token;
}
@Override
protected void serializeData(FriendlyByteBuf buf) {
List<AutoFileSyncEntry> existingFiles = files.stream().filter(e -> 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;
}));*/
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 + " (" + length + " Bytes)");
}
}
private List<Pair<AutoFileSyncEntry, byte[]>> receivedFiles;
@Override
protected void deserializeFromIncomingData(FriendlyByteBuf buf, PacketSender responseSender, boolean fromClient) {
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[], DataExchange.AutoSyncID> p = AutoFileSyncEntry.deserializeContent(buf);
if (p.first != null) {
receivedFiles.add(p);
BCLib.LOGGER.info(" - " + p.first + " (" + p.second.length + " Bytes)");
} else {
BCLib.LOGGER.error(" - Failed to receive File " +p.third+ ", possibly sent from a Mod that is not installed on the client.");
}
}
}
@Override
protected void runOnGameThread(Minecraft client, MinecraftServer server, boolean isClient) {
BCLib.LOGGER.info("Writing Files:");
for (Pair<AutoFileSyncEntry, byte[]> entry : receivedFiles) {
final AutoFileSyncEntry e = entry.first;
final byte[] data = entry.second;
Path path = e.fileName.toPath();
BCLib.LOGGER.info(" - Writing " + path + " (" + data.length + " Bytes)");
try {
Files.write(path, data);
} catch (IOException ioException) {
BCLib.LOGGER.error(" --> Writing "+e.fileName+" failed: " + ioException);
}
}
showConfirmRestart(client);
}
@Environment(EnvType.CLIENT)
protected void showConfirmRestart(Minecraft client){
client.setScreen(new ConfirmRestartScreen(() -> {
Minecraft.getInstance().setScreen((Screen)null);
client.stop();
}));
}
}

View file

@ -105,9 +105,9 @@ public class DataFixerAPI {
*/
public static boolean fixData(LevelStorageSource.LevelStorageAccess levelStorageAccess, boolean showUI, Consumer<Boolean> onResume){
File levelPath = levelStorageAccess.getLevelPath(LevelResource.ROOT).toFile();
File levelDat = levelStorageAccess.getLevelPath(LevelResource.LEVEL_DATA_FILE).toFile();
boolean newWorld = false;
if (!levelPath.exists()) {
if (!levelDat.exists()) {
BCLib.LOGGER.info("Creating a new World, no fixes needed");
newWorld = true;
}
@ -227,9 +227,6 @@ public class DataFixerAPI {
@Environment(EnvType.CLIENT)
static void showBackupWarning(String levelID, Consumer<Boolean> whenFinished){
TranslatableComponent promptText = new TranslatableComponent("bclib.datafixer.backupWarning.prompt");
TranslatableComponent buttonTitle = new TranslatableComponent("bclib.datafixer.backupWarning.button");
Minecraft.getInstance().setScreen(new ConfirmFixScreen((Screen) null, (createBackup, applyFixes) -> {
if (createBackup) {
EditWorldScreen.makeBackupAndShowToast(Minecraft.getInstance().getLevelSource(), levelID);
@ -238,7 +235,6 @@ public class DataFixerAPI {
Minecraft.getInstance().setScreen((Screen)null);
whenFinished.accept(applyFixes);
}));
}
private static void runDataFixes(File dir, MigrationProfile profile, ProgressListener progress) {
@ -258,6 +254,13 @@ public class DataFixerAPI {
players.parallelStream().forEach((file) -> fixPlayer(profile, state, file));
fixLevel(profile, state, new File(dir, "level.dat"));
try {
profile.patchWorldData();
} catch (PatchDidiFailException e){
state.didFail = true;
BCLib.LOGGER.error(e.getMessage());
}
if (!state.didFail) {
profile.markApplied();

View file

@ -2,6 +2,7 @@ package ru.bclib.api.datafixer;
import net.minecraft.nbt.CompoundTag;
import org.jetbrains.annotations.NotNull;
import ru.bclib.api.WorldDataAPI;
import java.util.Collections;
import java.util.HashMap;
@ -11,10 +12,11 @@ import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
class MigrationProfile {
public class MigrationProfile {
final Set<String> mods;
final Map<String, String> idReplacements;
final List<PatchFunction<CompoundTag, Boolean>> levelPatchers;
final List<Patch> worldDataPatchers;
private final CompoundTag config;
@ -28,6 +30,7 @@ class MigrationProfile {
HashMap<String, String> replacements = new HashMap<String, String>();
List<PatchFunction<CompoundTag, Boolean>> levelPatches = new LinkedList<>();
List<Patch> worldDataPatches = new LinkedList<>();
for (String modID : mods) {
Patch.getALL()
.stream()
@ -37,6 +40,8 @@ class MigrationProfile {
replacements.putAll(patch.getIDReplacements());
if (patch.getLevelDatPatcher()!=null)
levelPatches.add(patch.getLevelDatPatcher());
if (patch.getWorldDataPatcher()!=null)
worldDataPatches.add(patch);
DataFixerAPI.LOGGER.info("Applying " + patch);
}
else {
@ -47,6 +52,7 @@ class MigrationProfile {
this.idReplacements = Collections.unmodifiableMap(replacements);
this.levelPatchers = Collections.unmodifiableList(levelPatches);
this.worldDataPatchers = Collections.unmodifiableList(worldDataPatches);
}
final public void markApplied() {
@ -66,7 +72,7 @@ class MigrationProfile {
}
public boolean hasAnyFixes() {
return idReplacements.size() > 0 || levelPatchers.size() > 0;
return idReplacements.size() > 0 || levelPatchers.size() > 0 || worldDataPatchers.size() > 0;
}
public boolean replaceStringFromIDs(@NotNull CompoundTag tag, @NotNull String key) {
@ -87,8 +93,18 @@ class MigrationProfile {
public boolean patchLevelDat(@NotNull CompoundTag level) throws PatchDidiFailException {
boolean changed = false;
for (PatchFunction<CompoundTag, Boolean> f : levelPatchers) {
changed |= f.apply(level);
changed |= f.apply(level, this);
}
return changed;
}
public void patchWorldData() throws PatchDidiFailException {
for (Patch patch : worldDataPatchers) {
CompoundTag root = WorldDataAPI.getRootTag(patch.modID);
boolean changed = patch.getWorldDataPatcher().apply(root, this);
if (changed) {
WorldDataAPI.saveFile(patch.modID);
}
}
}
}

View file

@ -124,14 +124,25 @@ public abstract class Patch {
* Return a {@link PatchFunction} that is called with the content of <i>level.dat</i>.
* <p>
* The function needs to return {@code true}, if changes were made to the data.
* If an error occurs, the method shoudl throw a {@link PatchDidiFailException}
* If an error occurs, the method should throw a {@link PatchDidiFailException}
*
* The default implementation of this method returns null.
*
* @return The returned function is called a {@code CompoundTag} that contains the
* contents of <i>level.dat</i>
* @return {@code true} if changes were applied and we need to save the data
*/
public PatchFunction<CompoundTag, Boolean> getLevelDatPatcher() { return null; }
/**
* Return a {@link PatchFunction} that is called with the content from the
* {@link ru.bclib.api.WorldDataAPI} for this Mod.
* The function needs to return {@code true}, if changes were made to the data.
* If an error occurs, the method should throw a {@link PatchDidiFailException}
*
* The default implementation of this method returns null.
*
* @return {@code true} if changes were applied and we need to save the data
*/
public PatchFunction<CompoundTag, Boolean> getWorldDataPatcher() { return null; }
/**
* Generates ready to use data for all currently registered patches. The list of

View file

@ -2,5 +2,5 @@ package ru.bclib.api.datafixer;
@FunctionalInterface
public interface PatchFunction<T, R> {
R apply(T t) throws PatchDidiFailException;
R apply(T t, MigrationProfile profile) throws PatchDidiFailException;
}

View file

@ -2,6 +2,7 @@ package ru.bclib.config;
import org.jetbrains.annotations.Nullable;
import ru.bclib.BCLib;
import ru.bclib.api.dataexchange.DataExchangeAPI;
import ru.bclib.config.ConfigKeeper.BooleanEntry;
import ru.bclib.config.ConfigKeeper.Entry;
import ru.bclib.config.ConfigKeeper.FloatEntry;
@ -13,16 +14,19 @@ import java.io.File;
public abstract class Config {
protected final ConfigKeeper keeper;
protected final boolean autoSync;
protected abstract void registerEntries();
public Config(String modID, String group) {
this(modID, group, null);
protected Config(String modID, String group) {
this(modID, group, true);
}
protected Config(String modID, String group, File path) {
this.keeper = new ConfigKeeper(modID, group, path);
protected Config(String modID, String group, boolean autoSync) {
this.keeper = new ConfigKeeper(modID, group);
this.registerEntries();
this.autoSync = autoSync;
DataExchangeAPI.addAutoSyncFile(BCLib.MOD_ID, "CONFIG_"+modID+"_"+group, keeper.getConfigFile());
}
public void saveChanges() {

View file

@ -22,13 +22,13 @@ public final class ConfigKeeper {
private boolean changed = false;
public ConfigKeeper(String modID, String group) {
this(modID, group, null);
}
protected ConfigKeeper(String modID, String group, File path) {
this.writer = new ConfigWriter(modID, group, path);
this.writer = new ConfigWriter(modID, group);
this.configObject = writer.load();
}
File getConfigFile(){
return this.writer.getConfigFile();
}
public void save() {
if (!changed) return;

View file

@ -15,20 +15,17 @@ public class ConfigWriter {
private JsonObject configObject;
public ConfigWriter(String modID, String configFile) {
this(modID, configFile, null);
}
public ConfigWriter(String modID, String configFile, File configFolder) {
this.configFile = new File((configFolder == null ? GAME_CONFIG_DIR.resolve(modID).toFile() : new File(
configFolder,
modID
)), configFile + ".json");
this.configFile = new File(GAME_CONFIG_DIR.resolve(modID).toFile() , configFile + ".json");
File parent = this.configFile.getParentFile();
if (!parent.exists()) {
parent.mkdirs();
}
this.load();
}
File getConfigFile(){
return this.configFile;
}
public JsonObject getConfig() {
return configObject;

View file

@ -8,13 +8,13 @@ import ru.bclib.config.ConfigKeeper.IntegerRange;
import java.io.File;
public class PathConfig extends Config {
public PathConfig(String modID, String group) {
this(modID, group, null);
public PathConfig(String modID, String group, boolean autoSync) {
super(modID, group, autoSync);
}
protected PathConfig(String modID, String group, File path) {
super(modID, group, path);
public PathConfig(String modID, String group) {
super(modID, group);
}
@Override

View file

@ -1,25 +0,0 @@
package ru.bclib.config;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.storage.LevelStorageSource;
import ru.bclib.BCLib;
import java.io.File;
public class SessionConfig extends PathConfig {
private static File getWorldFolder(LevelStorageSource.LevelStorageAccess session, ServerLevel world) {
File dir = session.getDimensionPath(world.dimension());
if (!new File(dir, "level.dat").exists()) {
dir = dir.getParentFile();
}
return dir;
}
public final File levelFolder;
public SessionConfig(String modID, String group, LevelStorageSource.LevelStorageAccess session, ServerLevel world) {
super(modID, group, new File(getWorldFolder(session, world), BCLib.MOD_ID+"-config"));
this.levelFolder = new File(getWorldFolder(session, world), BCLib.MOD_ID+"-config");
}
}

View file

@ -0,0 +1,154 @@
package ru.bclib.gui;
import com.mojang.blaze3d.vertex.PoseStack;
import net.minecraft.client.gui.Font;
import net.minecraft.client.gui.components.Button;
import net.minecraft.client.gui.components.Button.OnPress;
import net.minecraft.client.gui.components.MultiLineLabel;
import net.minecraft.network.chat.Component;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.function.Consumer;
public class GridLayout {
class LablePos {
final MultiLineLabel label;
final int top;
LablePos(MultiLineLabel label, int top){
this.label = label;
this.top = top;
}
}
class ButtonPos {
final int top;
final int height;
final int width;
final float alpha;
final Component component;
final Button.OnPress onPress;
ButtonPos(float alpha, int top, int width, int height, Component component, OnPress onPress) {
this.height = height;
this.width = width;
this.top = top;
this.alpha = alpha;
this.component = component;
this.onPress = onPress;
}
}
public final int width;
public final int height;
@NotNull
private final Font font;
private final Consumer<Button> addButtonFunction;
private final int topStart;
private int topOffset;
private int top;
private int currentRowHeight = 0;
private int currentRowMargin = 6;
private int lastRowMargin = 0;
final private List<LablePos> labels;
final private List<List<ButtonPos>> buttons;
private List<ButtonPos> currentButtonRow;
public GridLayout(int topStart, int width, int height, Font font, Consumer<Button> addButtonFunction){
Objects.requireNonNull(font);
this.topStart = topStart;
top = topStart + 20;
this.topOffset = 0;
this.width = width;
this.height = height;
this.font = font;
this.addButtonFunction = addButtonFunction;
labels = new ArrayList<>(4);
buttons = new ArrayList<>(4);
}
public int getTopStart(){
return topStart + topOffset;
}
public void addMessageRow(Component text, int padding){
addMessageRow(MultiLineLabel.create(this.font, text, this.width - 2*padding));
}
public void addMessageRow(MultiLineLabel lb){
labels.add(new LablePos(lb, top));
int promptLines = lb.getLineCount() + 1;
int height = promptLines * 9;
currentRowMargin = 12;
currentRowHeight = height;
}
public void startRow(){
this.endRow();
this.currentButtonRow = new ArrayList<>(8);
this.buttons.add(this.currentButtonRow);
}
public void endRow(){
lastRowMargin = currentRowMargin;
top += currentRowHeight + currentRowMargin;
currentRowHeight = 0;
currentRowMargin = 0;
}
public void recenterVertically(){
int hg = (top - lastRowMargin) - topStart;
int targetTop = (height - hg)/2;
topOffset = targetTop - topStart;
}
void finalizeLayout(){
final int BUTTON_SPACING = 10;
for (List<ButtonPos> row : this.buttons) {
int count = row.size();
int rowWidth = row.stream()
.map(b -> b.width)
.reduce(0, (p, c) -> p + c) + (count - 1) * BUTTON_SPACING;
int left = (width - rowWidth) / 2;
for (ButtonPos bp : row) {
Button customButton = new Button(left, bp.top+topOffset, bp.width, bp.height, bp.component, bp.onPress);
customButton.setAlpha(bp.alpha);
addButtonFunction.accept(customButton);
left += BUTTON_SPACING + bp.width;
}
}
}
public void addButton(int width, int height, Component component, Button.OnPress onPress){
addButton(1.0f, width, height, component, onPress);
}
public void addButton(int height, Component component, Button.OnPress onPress){
addButton(1.0f, height, component, onPress);
}
public void addButton(float alpha, int height, Component component, Button.OnPress onPress){
final int BUTTON_PADDING = 12;
int width = font.width(component.getVisualOrderText()) + 2*BUTTON_PADDING;
addButton(alpha, width, height, component, onPress);
}
public void addButton(float alpha, int width, int height, Component component, Button.OnPress onPress){
currentRowHeight = Math.max(currentRowHeight, height);
currentRowMargin = 6;
currentButtonRow.add(new ButtonPos(alpha, top, width, height, component, onPress));
}
public void render(PoseStack poseStack){
labels.forEach(lp -> {
lp.label.renderCentered(poseStack, this.width / 2, lp.top + topOffset);
});
}
}

View file

@ -0,0 +1,34 @@
package ru.bclib.gui;
import com.mojang.blaze3d.vertex.PoseStack;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
import net.minecraft.client.gui.screens.Screen;
import net.minecraft.network.chat.Component;
@Environment(EnvType.CLIENT)
public abstract class GridScreen extends Screen {
protected GridLayout grid = null;
public final int topStart;
public GridScreen(int topStart, Component title) {
super(title);
this.topStart = topStart;
}
final protected void init() {
super.init();
this.grid = new GridLayout(topStart, this.width, this.height, this.font, this::addRenderableWidget);
initLayout();
grid.finalizeLayout();
}
protected abstract void initLayout();
public void render(PoseStack poseStack, int i, int j, float f) {
this.renderBackground(poseStack);
drawCenteredString(poseStack, this.font, this.title, grid.width / 2, grid.getTopStart(), 16777215);
if (grid!=null) grid.render(poseStack);
super.render(poseStack, i, j, f);
}
}

View file

@ -1,115 +1,63 @@
package ru.bclib.gui.screens;
import com.mojang.blaze3d.systems.RenderSystem;
import com.mojang.blaze3d.vertex.PoseStack;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.Font;
import net.minecraft.client.gui.components.Button;
import net.minecraft.client.gui.components.MultiLineLabel;
import net.minecraft.client.gui.screens.BackupConfirmScreen;
import net.minecraft.client.gui.screens.Screen;
import net.minecraft.client.renderer.GameRenderer;
import net.minecraft.network.chat.CommonComponents;
import net.minecraft.network.chat.Component;
import net.minecraft.network.chat.TranslatableComponent;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.util.Mth;
import org.jetbrains.annotations.Nullable;
import ru.bclib.BCLib;
import java.util.Objects;
class ColoredButton extends Button {
private final float r;
private final float g;
private final float b;
public ColoredButton(int x, int y, int width, int height, Component component, OnPress onPress, float r, float g, float b) {
super(x, y, width, height, component, onPress);
this.r = r;
this.g = g;
this.b = b;
}
@Override
public void renderButton(PoseStack poseStack, int i, int j, float f) {
Minecraft minecraft = Minecraft.getInstance();
Font font = minecraft.font;
RenderSystem.setShader(GameRenderer::getPositionTexShader);
RenderSystem.setShaderTexture(0, WIDGETS_LOCATION);
RenderSystem.setShaderColor(r, g, b, this.alpha);
int k = this.getYImage(this.isHovered());
RenderSystem.enableBlend();
RenderSystem.defaultBlendFunc();
RenderSystem.enableDepthTest();
this.blit(poseStack, this.x, this.y, 0, 46 + k * 20, this.width / 2, this.height);
this.blit(poseStack, this.x + this.width / 2, this.y, 200 - this.width / 2, 46 + k * 20, this.width / 2, this.height);
this.renderBg(poseStack, minecraft, i, j);
int l = this.active ? 16777215 : 10526880;
drawCenteredString(poseStack, font, this.getMessage(), this.x + this.width / 2, this.y + (this.height - 8) / 2, l | Mth.ceil(this.alpha * 255.0F) << 24);
}
}
import ru.bclib.gui.GridLayout;
import ru.bclib.gui.GridScreen;
@Environment(EnvType.CLIENT)
public class ConfirmFixScreen extends Screen {
public class ConfirmFixScreen extends GridScreen {
static final ResourceLocation BCLIB_LOGO_LOCATION = new ResourceLocation(BCLib.MOD_ID,
"icon.png");
@Nullable
private final Screen lastScreen;
protected final BackupConfirmScreen.Listener listener;
protected final ConfirmFixScreen.Listener listener;
private final Component description;
private MultiLineLabel message;
protected int id;
public ConfirmFixScreen(@Nullable Screen screen, BackupConfirmScreen.Listener listener) {
super(new TranslatableComponent("bclib.datafixer.backupWarning.title"));
this.message = MultiLineLabel.EMPTY;
public ConfirmFixScreen(@Nullable Screen screen, ConfirmFixScreen.Listener listener) {
super(30, new TranslatableComponent("bclib.datafixer.backupWarning.title"));
this.lastScreen = screen;
this.listener = listener;
this.description = new TranslatableComponent("bclib.datafixer.backupWarning.message");
}
protected void init() {
super.init();
this.message = MultiLineLabel.create(this.font, this.description, this.width - 50);
int promptLines = this.message.getLineCount() + 1;
Objects.requireNonNull(this.font);
int height = promptLines * 9;
final int BUTTON_WIDTH = 150;
final int BUTTON_SPACE = 10;
protected void initLayout() {
final int BUTTON_HEIGHT = 20;
Button customButton = new Button((this.width -BUTTON_WIDTH)/2, 100 + height, BUTTON_WIDTH, BUTTON_HEIGHT, new TranslatableComponent("bclib.datafixer.backupWarning.backup"), (button) -> {
grid.addMessageRow(this.description, 25);
grid.startRow();
grid.addButton( BUTTON_HEIGHT, new TranslatableComponent("bclib.datafixer.backupWarning.backup"), (button) -> {
this.listener.proceed(true, true);
});
this.addRenderableWidget(customButton);
customButton = new Button((this.width - 2*BUTTON_WIDTH - BUTTON_SPACE)/ 2 , 124 + height, BUTTON_WIDTH, BUTTON_HEIGHT, CommonComponents.GUI_CANCEL, (button) -> {
grid.startRow();
grid.addButton( BUTTON_HEIGHT, CommonComponents.GUI_CANCEL, (button) -> {
this.minecraft.setScreen(this.lastScreen);
});
this.addRenderableWidget(customButton);
customButton = new Button((this.width - 2*BUTTON_WIDTH - BUTTON_SPACE)/ 2 + BUTTON_WIDTH+BUTTON_SPACE, 124 + height, BUTTON_WIDTH, BUTTON_HEIGHT, new TranslatableComponent("bclib.datafixer.backupWarning.continue"), (button) -> {
grid.addButton(0.5f, BUTTON_HEIGHT, new TranslatableComponent("bclib.datafixer.backupWarning.continue"), (button) -> {
this.listener.proceed(false, true);
});
customButton.setAlpha(0.5f);
this.addRenderableWidget(customButton);
customButton = new Button((this.width - BUTTON_WIDTH)/ 2, 148 + height, BUTTON_WIDTH, BUTTON_HEIGHT, new TranslatableComponent("bclib.datafixer.backupWarning.nofixes"), (button) -> {
grid.startRow();
grid.addButton(0.5f, BUTTON_HEIGHT, new TranslatableComponent("bclib.datafixer.backupWarning.nofixes"), (button) -> {
this.listener.proceed(false, false);
});
customButton.setAlpha(0.5f);
this.addRenderableWidget(customButton);
}
public void render(PoseStack poseStack, int i, int j, float f) {
this.renderBackground(poseStack);
drawCenteredString(poseStack, this.font, this.title, this.width / 2, 50, 16777215);
this.message.renderCentered(poseStack, this.width / 2, 70);
super.render(poseStack, i, j, f);
grid.endRow();
}
public boolean shouldCloseOnEsc() {
@ -127,6 +75,6 @@ public class ConfirmFixScreen extends Screen {
@Environment(EnvType.CLIENT)
public interface Listener {
void proceed(boolean bl, boolean bl2);
void proceed(boolean createBackup, boolean applyPatches);
}
}

View file

@ -0,0 +1,49 @@
package ru.bclib.gui.screens;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
import net.minecraft.network.chat.CommonComponents;
import net.minecraft.network.chat.Component;
import net.minecraft.network.chat.TranslatableComponent;
import ru.bclib.gui.GridScreen;
@Environment(EnvType.CLIENT)
public class ConfirmRestartScreen extends GridScreen {
private final Component description;
private final ConfirmRestartScreen.Listener listener;
public ConfirmRestartScreen(ConfirmRestartScreen.Listener listener) {
this(listener, null);
}
public ConfirmRestartScreen(ConfirmRestartScreen.Listener listener, Component message) {
super(30, new TranslatableComponent("bclib.datafixer.confirmrestart.title"));
this.description = message==null?new TranslatableComponent("bclib.datafixer.confirmrestart.message"):message;
this.listener = listener;
}
protected void initLayout() {
final int BUTTON_HEIGHT = 20;
grid.addMessageRow(this.description, 25);
grid.startRow();
grid.addButton( BUTTON_HEIGHT, CommonComponents.GUI_PROCEED, (button) -> {
listener.proceed();
});
grid.endRow();
grid.recenterVertically();
}
public boolean shouldCloseOnEsc() {
return false;
}
@Environment(EnvType.CLIENT)
public interface Listener {
void proceed();
}
}

View file

@ -0,0 +1,46 @@
package ru.bclib.gui.screens;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
import net.minecraft.network.chat.CommonComponents;
import net.minecraft.network.chat.Component;
import net.minecraft.network.chat.TranslatableComponent;
import ru.bclib.gui.GridScreen;
@Environment(EnvType.CLIENT)
public class SyncFilesScreen extends GridScreen {
private final Component description;
private final SyncFilesScreen.Listener listener;
public SyncFilesScreen(SyncFilesScreen.Listener listener) {
super(30, new TranslatableComponent("bclib.datafixer.syncfiles.title"));
this.description = new TranslatableComponent("bclib.datafixer.syncfiles.message");
this.listener = listener;
}
protected void initLayout() {
final int BUTTON_HEIGHT = 20;
grid.addMessageRow(this.description, 25);
grid.startRow();
grid.addButton( BUTTON_HEIGHT, CommonComponents.GUI_NO, (button) -> {
listener.proceed(false);
});
grid.addButton( BUTTON_HEIGHT, CommonComponents.GUI_YES, (button) -> {
listener.proceed(true);
});
grid.endRow();
grid.recenterVertically();
}
public boolean shouldCloseOnEsc() {
return false;
}
@Environment(EnvType.CLIENT)
public interface Listener {
void proceed(boolean download);
}
}

View file

@ -0,0 +1,46 @@
package ru.bclib.gui.screens;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
import net.minecraft.network.chat.CommonComponents;
import net.minecraft.network.chat.Component;
import net.minecraft.network.chat.TranslatableComponent;
import ru.bclib.gui.GridScreen;
@Environment(EnvType.CLIENT)
public class WarnBCLibVersionMismatch extends GridScreen {
private final Component description;
private final Listener listener;
public WarnBCLibVersionMismatch(Listener listener) {
super(30, new TranslatableComponent("bclib.datafixer.bclibmissmatch.title"));
this.description = new TranslatableComponent("bclib.datafixer.bclibmissmatch.message");
this.listener = listener;
}
protected void initLayout() {
final int BUTTON_HEIGHT = 20;
grid.addMessageRow(this.description, 25);
grid.startRow();
grid.addButton( BUTTON_HEIGHT, CommonComponents.GUI_NO, (button) -> {
listener.proceed(false);
});
grid.addButton( BUTTON_HEIGHT, CommonComponents.GUI_YES, (button) -> {
listener.proceed(true);
});
grid.endRow();
grid.recenterVertically();
}
public boolean shouldCloseOnEsc() {
return false;
}
@Environment(EnvType.CLIENT)
public interface Listener {
void proceed(boolean download);
}
}

View file

@ -0,0 +1,2 @@
package ru.bclib.mixin.client;public class ClientLevelMixin {
}

View file

@ -0,0 +1,20 @@
package ru.bclib.mixin.client;
import net.minecraft.client.Minecraft;
import net.minecraft.network.protocol.game.ClientGamePacketListener;
import net.minecraft.network.protocol.game.ClientboundLoginPacket;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import ru.bclib.api.dataexchange.DataExchangeAPI;
@Mixin(ClientboundLoginPacket.class)
public abstract class ClientboundLoginPacketMixin {
@Inject(method = "handle", cancellable = true, at=@At("HEAD"))
public void bclib_handle(ClientGamePacketListener clientGamePacketListener, CallbackInfo ci){
//cLevel.setBCLibDidSendHello();
// DataExchangeAPI.sendOnEnter();
// ci.cancel();
}
}

View file

@ -0,0 +1,18 @@
package ru.bclib.mixin.client;
import net.minecraft.client.Game;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import ru.bclib.api.dataexchange.DataExchangeAPI;
import ru.bclib.api.dataexchange.handler.HelloServer;
@Mixin(Game.class)
public class GameMixin {
@Inject(method="onStartGameSession", at=@At("TAIL"))
public void bcliv_onStart(CallbackInfo ci){
DataExchangeAPI.sendOnEnter();
}
}

View file

@ -3,7 +3,9 @@ package ru.bclib.mixin.client;
import net.minecraft.client.Minecraft;
import net.minecraft.client.color.block.BlockColors;
import net.minecraft.client.color.item.ItemColors;
import net.minecraft.client.gui.screens.Screen;
import net.minecraft.client.main.GameConfig;
import net.minecraft.client.multiplayer.ClientLevel;
import net.minecraft.core.Registry;
import net.minecraft.core.RegistryAccess.RegistryHolder;
import net.minecraft.world.level.LevelSettings;
@ -16,6 +18,7 @@ import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.ModifyArg;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import ru.bclib.api.dataexchange.DataExchangeAPI;
import ru.bclib.api.datafixer.DataFixerAPI;
import ru.bclib.interfaces.CustomColorProvider;
@ -51,6 +54,8 @@ public abstract class MinecraftMixin {
if (levelID.startsWith(BCLIB_RECURSION)) {
levelID = levelID.substring(BCLIB_RECURSION.length());
recursiveCall = true;
} else {
DataExchangeAPI.prepareServerside();
}
final String recursiveLevelID = BCLIB_RECURSION + levelID;

View file

@ -0,0 +1,11 @@
package ru.bclib.util;
public class Pair <A, B>{
public final A first;
public final B second;
public Pair(A first, B second) {
this.first = first;
this.second = second;
}
}

View file

@ -0,0 +1,10 @@
package ru.bclib.util;
public class Triple <A, B, C> extends Pair<A, B>{
public final C third;
public Triple(A first, B second, C third) {
super(first, second);
this.third = third;
}
}

View file

@ -4,5 +4,11 @@
"bclib.datafixer.backupWarning.message": "The Guardian detected, that the internals of the following Mods did change since this world was last played.\n\nWe can automatically change the world for you. If you continue without applying the changes the world may not load correct. Before you continue, you should create a Backup.",
"bclib.datafixer.backupWarning.backup": "Create Backup and Continue",
"bclib.datafixer.backupWarning.nofixes": "Continue Without Fixes",
"bclib.datafixer.backupWarning.continue": "Continue Without Backup"
"bclib.datafixer.backupWarning.continue": "Continue Without Backup",
"bclib.datafixer.bclibmissmatch.title": "Version Mismatch",
"bclib.datafixer.bclibmissmatch.message": "The Version of BCLib on the server and this client do not match. This will cause problems when playing.\n\nDo you want to automatically download the BCLib-Version from the server. You will need to delete the old version from your Mods Directory and restart the game.",
"bclib.datafixer.syncfiles.title": "Mismatching (Config-)Files",
"bclib.datafixer.syncfiles.message": "Some Files on the Server doe not match the versions on the client.\n\nDo you want to replace the local versions with the ones from the server?",
"bclib.datafixer.confirmrestart.title": "Restart Required",
"bclib.datafixer.confirmrestart.message": "The requested files were processed. You need o restart Minecraft now."
}

View file

@ -7,6 +7,7 @@
"SimpleReloadableResourceManagerMixin",
"EnchantingTableBlockMixin",
"BackgroundRendererMixin",
"GameMixin",
"TextureAtlasMixin",
"ModelBakeryMixin",
"MinecraftMixin"