Added Screen to confirm Patches

This commit is contained in:
Frank Bauer 2021-07-26 18:36:20 +02:00
parent c93c271bd4
commit a247b17e7f
8 changed files with 384 additions and 26 deletions

View file

@ -1,59 +1,215 @@
package ru.bclib.api.datafixer;
import net.minecraft.Util;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.components.toasts.SystemToast;
import net.minecraft.client.gui.screens.Screen;
import net.minecraft.client.gui.screens.worldselection.EditWorldScreen;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.ListTag;
import net.minecraft.nbt.NbtIo;
import net.minecraft.network.chat.Component;
import net.minecraft.network.chat.TranslatableComponent;
import net.minecraft.util.ProgressListener;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.chunk.storage.RegionFile;
import net.minecraft.world.level.storage.LevelResource;
import net.minecraft.world.level.storage.LevelStorageSource;
import ru.bclib.BCLib;
import ru.bclib.api.WorldDataAPI;
import ru.bclib.config.Configs;
import ru.bclib.gui.screens.ConfirmFixScreen;
import ru.bclib.util.Logger;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.function.Consumer;
import java.util.function.Supplier;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* API to manage Patches that need to get applied to a world
*/
public class DataFixerAPI {
static final Logger LOGGER = new Logger("DataFixerAPI");
static class State {
public boolean didFail = false;
}
public static void fixData(File dir) {
@FunctionalInterface
public static interface Callback {
public void call();
}
/**
* Will apply necessary Patches to the world.
*
* @param levelSource The SourceStorage for this Minecraft instance, You can get this using
* {@code Minecraft.getInstance().getLevelSource()}
* @param levelID The ID of the Level you want to patch
* @param showUI {@code true}, if you want to present the user with a Screen that offers to backup the world
* before applying the patches
* @param onResume When this method retursn {@code true}, this function will be called when the world is ready
* @return {@code true} if the UI was displayed. The UI is only displayed if {@code showUI} was {@code true} and
* patches were enabled in the config and the Guardian did find any patches that need to be applied to the world.
*
*/
public static boolean fixData(LevelStorageSource levelSource, String levelID, boolean showUI, Consumer<Boolean> onResume) {
LevelStorageSource.LevelStorageAccess levelStorageAccess;
try {
levelStorageAccess = levelSource.createAccess(levelID);
} catch (IOException e) {
BCLib.LOGGER.warning((String)"Failed to read level {} data", levelID, e);
SystemToast.onWorldAccessFailure(Minecraft.getInstance(), levelID);
Minecraft.getInstance().setScreen((Screen)null);
return true;
}
boolean cancelRun = fixData(levelStorageAccess, showUI, onResume);
try {
levelStorageAccess.close();
} catch (IOException e) {
BCLib.LOGGER.warning((String)"Failed to unlock access to level {}", levelID, e);
}
return cancelRun;
}
/**
* Will apply necessary Patches to the world.
*
* @param levelStorageAccess The access class of the level you want to patch
* @param showUI {@code true}, if you want to present the user with a Screen that offers to backup the world
* before applying the patches
* @param onResume When this method retursn {@code true}, this function will be called when the world is ready
* @return {@code true} if the UI was displayed. The UI is only displayed if {@code showUI} was {@code true} and
* patches were enabled in the config and the Guardian did find any patches that need to be applied to the world.
*
*/
public static boolean fixData(LevelStorageSource.LevelStorageAccess levelStorageAccess, boolean showUI, Consumer<Boolean> onResume){
File levelPath = levelStorageAccess.getLevelPath(LevelResource.ROOT).toFile();
WorldDataAPI.load(new File(levelPath, "data"));
return fixData(levelPath, levelStorageAccess.getLevelId(), showUI, onResume);
}
private static boolean fixData(File dir, String levelID, boolean showUI, Consumer<Boolean> onResume) {
MigrationProfile profile = loadProfile(dir, levelID);
Consumer<Boolean> runFixes = (applyFixes) -> {
if (applyFixes) {
runDataFixes(dir, profile, new ProgressListener() {
private long timeStamp = Util.getMillis();
public void progressStartNoAbort(Component component) {
}
public void progressStart(Component component) {
}
public void progressStagePercentage(int i) {
if (Util.getMillis() - this.timeStamp >= 1000L) {
this.timeStamp = Util.getMillis();
BCLib.LOGGER.info((String) "Patching... {}%", (Object) i);
}
}
public void stop() {
}
public void progressStage(Component component) {
}
});
} else {
//System.out.println("NO FIXES");
}
//UI is asynchronous, so we need to send the callback now
if (profile != null && showUI) {
onResume.accept(applyFixes);
}
};
//we have some migrations
if (profile != null) {
//display the confirm UI.
if (showUI){
showBackupWarning(levelID, runFixes);
return true;
} else {
BCLib.LOGGER.warning("Applying Fixes on Level");
runFixes.accept(true);
}
}
return false;
}
static MigrationProfile loadProfile(File dir, String levelID){
if (!Configs.MAIN_CONFIG.getBoolean(Configs.MAIN_PATCH_CATEGORY, "applyPatches", true)) {
LOGGER.info("World Patches are disabled");
return;
return null;
}
final CompoundTag patchConfig = WorldDataAPI.getCompoundTag(BCLib.MOD_ID, Configs.MAIN_PATCH_CATEGORY);
MigrationProfile data = Patch.createMigrationData(patchConfig);
if (!data.hasAnyFixes()) {
MigrationProfile profile = Patch.createMigrationData(patchConfig);
if (!profile.hasAnyFixes()) {
LOGGER.info("Everything up to date");
return;
}
return null;
}
return profile;
}
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);
}
Minecraft.getInstance().setScreen((Screen)null);
whenFinished.accept(applyFixes);
}));
}
private static void runDataFixes(File dir, MigrationProfile profile, ProgressListener progress) {
System.out.println("RUNNING fixes");
if (dir!= null) return;
State state = new State();
List<File> regions = getAllRegions(dir, null);
regions.parallelStream().forEach((file) -> fixRegion(data, state, file));
progress.progressStagePercentage(0);
int[] count = {0};
regions.parallelStream().forEach((file) -> {
fixRegion(profile, state, file);
count[0]++;
progress.progressStagePercentage((100 * count[0])/regions.size());
});
progress.stop();
List<File> players = getAllPlayers(dir);
players.parallelStream().forEach((file) -> fixPlayer(data, state, file));
players.parallelStream().forEach((file) -> fixPlayer(profile, state, file));
fixLevel(data, state, new File(dir, "level.dat"));
fixLevel(profile, state, new File(dir, "level.dat"));
if (!state.didFail) {
data.markApplied();
profile.markApplied();
WorldDataAPI.saveFile(BCLib.MOD_ID);
}
}
private static void fixLevel(MigrationProfile data, State state, File file) {
try {
LOGGER.info("Inspecting " + file);
@ -121,6 +277,7 @@ public class DataFixerAPI {
LOGGER.info("Inspecting " + file);
boolean[] changed = new boolean[1];
RegionFile region = new RegionFile(file, file.getParentFile(), true);
for (int x = 0; x < 32; x++) {
for (int z = 0; z < 32; z++) {
ChunkPos pos = new ChunkPos(x, z);

View file

@ -36,8 +36,8 @@ public abstract class ComplexMaterial {
private final Map<String, Block> blocks = Maps.newHashMap();
private final Map<String, Item> items = Maps.newHashMap();
private final String baseName;
private final String modID;
protected final String baseName;
protected final String modID;
public ComplexMaterial(String modID, String baseName) {
this.baseName = baseName;

View file

@ -41,6 +41,7 @@ import ru.bclib.complexmaterials.entry.RecipeEntry;
import ru.bclib.config.PathConfig;
import ru.bclib.recipes.GridRecipe;
import java.util.Arrays;
import java.util.List;
public class WoodenComplexMaterial extends ComplexMaterial {
@ -96,8 +97,14 @@ public class WoodenComplexMaterial extends ComplexMaterial {
@Override
protected void initDefault(FabricBlockSettings blockSettings, FabricItemSettings itemSettings) {
initDefault(blockSettings, itemSettings, new String[0]);
}
final protected void initDefault(FabricBlockSettings blockSettings, FabricItemSettings itemSettings, String[] excludedSuffixes) {
Tag.Named<Block> tagBlockLog = getBlockTag(TAG_LOGS);
Tag.Named<Item> tagItemLog = getItemTag(TAG_LOGS);
List<String> excl = Arrays.asList(excludedSuffixes);
addBlockEntry(
new BlockEntry(BLOCK_STRIPPED_LOG, (complexMaterial, settings) -> {
@ -173,12 +180,18 @@ public class WoodenComplexMaterial extends ComplexMaterial {
addBlockEntry(new BlockEntry(BLOCK_BARREL, (complexMaterial, settings) -> {
return new BaseBarrelBlock(getBlock(BLOCK_PLANKS));
}));
addBlockEntry(new BlockEntry(BLOCK_BOOKSHELF, (complexMaterial, settings) -> {
return new BaseBookshelfBlock(getBlock(BLOCK_PLANKS));
}).setBlockTags(TagAPI.BLOCK_BOOKSHELVES));
addBlockEntry(new BlockEntry(BLOCK_COMPOSTER, (complexMaterial, settings) -> {
return new BaseComposterBlock(getBlock(BLOCK_PLANKS));
}));
if (!excl.contains(BLOCK_BOOKSHELF)) {
addBlockEntry(new BlockEntry(BLOCK_BOOKSHELF, (complexMaterial, settings) -> {
return new BaseBookshelfBlock(getBlock(BLOCK_PLANKS));
}).setBlockTags(TagAPI.BLOCK_BOOKSHELVES));
}
if (!excl.contains(BLOCK_COMPOSTER)) {
addBlockEntry(new BlockEntry(BLOCK_COMPOSTER, (complexMaterial, settings) -> {
return new BaseComposterBlock(getBlock(BLOCK_PLANKS));
}));
}
}
@Override

View file

@ -0,0 +1,132 @@
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);
}
}
@Environment(EnvType.CLIENT)
public class ConfirmFixScreen extends Screen {
static final ResourceLocation BCLIB_LOGO_LOCATION = new ResourceLocation(BCLib.MOD_ID,
"icon.png");
@Nullable
private final Screen lastScreen;
protected final BackupConfirmScreen.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;
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;
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) -> {
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) -> {
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) -> {
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) -> {
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);
}
public boolean shouldCloseOnEsc() {
return false;
}
public boolean keyPressed(int i, int j, int k) {
if (i == 256) {
this.minecraft.setScreen(this.lastScreen);
return true;
} else {
return super.keyPressed(i, j, k);
}
}
@Environment(EnvType.CLIENT)
public interface Listener {
void proceed(boolean bl, boolean bl2);
}
}

View file

@ -5,16 +5,19 @@ import net.minecraft.client.color.block.BlockColors;
import net.minecraft.client.color.item.ItemColors;
import net.minecraft.client.main.GameConfig;
import net.minecraft.core.Registry;
import net.minecraft.world.level.storage.LevelStorageSource;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
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.datafixer.DataFixerAPI;
import ru.bclib.interfaces.CustomColorProvider;
@Mixin(Minecraft.class)
public class MinecraftMixin {
public abstract class MinecraftMixin {
@Final
@Shadow
private BlockColors blockColors;
@ -33,4 +36,51 @@ public class MinecraftMixin {
}
});
}
@Shadow @Final private LevelStorageSource levelSource;
@Shadow public abstract void loadLevel(String string);
private final String BCLIB_RECURSION = "$@BCLIB:";
@Inject(method="loadLevel", cancellable = true, at=@At("HEAD"))
private void bclib_callFixerOnLoad(String levelID, CallbackInfo ci){
boolean recursiveCall = false;
if (levelID.startsWith(BCLIB_RECURSION)) {
levelID = levelID.substring(BCLIB_RECURSION.length());
recursiveCall = true;
}
final String recursiveLevelID = BCLIB_RECURSION + levelID;
if (!recursiveCall && DataFixerAPI.fixData(this.levelSource, levelID, true, (appliedFixes)->{
this.loadLevel(recursiveLevelID);
})){
ci.cancel();
}
}
@ModifyArg(method="loadLevel", at=@At(value="INVOKE", target="Lnet/minecraft/client/Minecraft;doLoadLevel(Ljava/lang/String;Lnet/minecraft/core/RegistryAccess$RegistryHolder;Ljava/util/function/Function;Lcom/mojang/datafixers/util/Function4;ZLnet/minecraft/client/Minecraft$ExperimentalDialogType;)V"))
private String bclib_correctLevelID(String levelID){
if (levelID.startsWith(BCLIB_RECURSION)) {
levelID = levelID.substring(BCLIB_RECURSION.length());
}
return levelID;
}
// @Inject(method="doLoadLevel", cancellable = true, locals = LocalCapture.CAPTURE_FAILHARD, at=@At(value="INVOKE", target="Lnet/minecraft/client/Minecraft;makeServerStem(Lnet/minecraft/core/RegistryAccess$RegistryHolder;Ljava/util/function/Function;Lcom/mojang/datafixers/util/Function4;ZLnet/minecraft/world/level/storage/LevelStorageSource$LevelStorageAccess;)Lnet/minecraft/client/Minecraft$ServerStem;"))
// private void bclib_onCallFixer(
// String string,
// RegistryHolder registryHolder,
// Function<LevelStorageAccess, DataPackConfig> function,
// Function4<LevelStorageAccess, RegistryHolder, ResourceManager, DataPackConfig, WorldData> function4,
// boolean bl,
// ExperimentalDialogType experimentalDialogType,
// CallbackInfo ci,
// LevelStorageSource.LevelStorageAccess levelStorageAccess) {
//
// DataFixerAPI.fixData(levelStorageAccess);
// ci.cancel();
//
// }
}

View file

@ -6,7 +6,6 @@ import net.minecraft.server.MinecraftServer;
import net.minecraft.server.ServerResources;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.storage.LevelResource;
import net.minecraft.world.level.storage.LevelStorageSource;
import net.minecraft.world.level.storage.WorldData;
import org.spongepowered.asm.mixin.Final;
@ -17,11 +16,8 @@ import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import ru.bclib.api.BiomeAPI;
import ru.bclib.api.WorldDataAPI;
import ru.bclib.api.datafixer.DataFixerAPI;
import ru.bclib.recipes.BCLRecipeManager;
import java.io.File;
import java.util.Collection;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
@ -38,12 +34,14 @@ public class MinecraftServerMixin {
@Final
@Shadow
protected WorldData worldData;
@Inject(method="convertFromRegionFormatIfNeeded", at = @At("HEAD"))
private static void bclib_applyPatches(LevelStorageSource.LevelStorageAccess session, CallbackInfo ci){
File levelPath = session.getLevelPath(LevelResource.ROOT).toFile();
/*File levelPath = session.getLevelPath(LevelResource.ROOT).toFile();
WorldDataAPI.load(new File(levelPath, "data"));
DataFixerAPI.fixData(levelPath);
DataFixerAPI.fixData(levelPath, session.getLevelId());*/
}

View file

@ -0,0 +1,3 @@
{
"message.bclib.anvil_damage": "§cSchaden"
}

View file

@ -1,3 +1,8 @@
{
"message.bclib.anvil_damage": "§cDamage"
"message.bclib.anvil_damage": "§cDamage",
"bclib.datafixer.backupWarning.title": "Guardian detected an incompatible World",
"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"
}