Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions fabric-serialization-api-v1/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,5 @@ version = getSubprojectVersion(project)
loom {
accessWidenerPath = file('src/main/resources/fabric-serialization-api-v1.classtweaker')
}

moduleDependencies(project, ['fabric-api-base'])
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package net.fabricmc.fabric.api.serialization.v1.filefix;

import com.mojang.datafixers.schemas.Schema;

import net.fabricmc.fabric.impl.serialization.filefix.CombinedFileFixOperation;
import net.fabricmc.fabric.impl.serialization.filefix.FileFixHelpersImpl;

import net.fabricmc.fabric.mixin.serialization.filefix.FileFixerUpperAccessor;

import net.minecraft.resources.Identifier;
import net.minecraft.util.filefix.FileFix;
import net.minecraft.util.filefix.access.FileRelation;
import net.minecraft.util.filefix.operations.FileFixOperation;
import net.minecraft.util.filefix.operations.FileFixOperations;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.function.Function;

public interface FileFixHelpers {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this an interface

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No docs


static FileFixOperation createDimensionDataMoveOperation(String oldSaveId, Identifier newSaveId) {
return createDimensionDataMoveOperation(Map.of(oldSaveId, newSaveId));
}

static FileFixOperation createDimensionDataMoveOperation(Map<String, Identifier> saveIdMap) {
List<FileFixOperation> operations = new ArrayList<>();
operations.addAll(FileFixHelpersImpl.createDimensionMoveOperations(saveIdMap, "", "dimensions/minecraft/overworld"));
operations.addAll(FileFixHelpersImpl.createDimensionMoveOperations(saveIdMap, "DIM-1", "dimensions/minecraft/the_nether"));
operations.addAll(FileFixHelpersImpl.createDimensionMoveOperations(saveIdMap, "DIM1", "dimensions/minecraft/the_end"));
Comment on lines +29 to +31
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should not hard code vanilla's own operations here.

operations.add(FileFixHelpersImpl.createCustomDimensionDataMoveOperation(saveIdMap));
return new CombinedFileFixOperation(operations);
}

static Function<Schema, FileFix> createDimensionDataMoveFileFix(String oldSaveId, Identifier newSaveId) {
return createDimensionDataMoveFileFix(Map.of(oldSaveId, newSaveId));
}

static Function<Schema, FileFix> createDimensionDataMoveFileFix(Map<String, Identifier> saveIdMap) {
return schema -> new FileFix(schema) {
@Override
public void makeFixer() {
addFileFixOperation(createDimensionDataMoveOperation(saveIdMap));
}
};
}

static void registerDimensionDataMoveFileFix(String oldSaveId, Identifier newSaveId) {
registerDimensionDataMoveFileFix(Map.of(oldSaveId, newSaveId));
}

static void registerDimensionDataMoveFileFix(Map<String, Identifier> saveIdMap) {
FileFixSchemaRegisterCallback.registerFileFixes(FileFixerUpperAccessor.getFileFixerIntroductionVersion(), createDimensionDataMoveFileFix(saveIdMap));
}

static FileFixOperation createGlobalDataMoveOperation(String oldSaveId, Identifier newSaveId) {
return createGlobalDataMoveOperation(Map.of(oldSaveId, newSaveId));
}

static FileFixOperation createGlobalDataMoveOperation(Map<String, Identifier> saveIdMap) {
return FileFixOperations.applyInFolders(
FileRelation.DATA,
saveIdMap.entrySet().stream()
.map(entry -> FileFixHelpersImpl.createNamespacedDataMoveOperation(entry.getKey(), entry.getValue(), "", ""))
.toList()
);
}

static Function<Schema, FileFix> createGlobalDataMoveFileFix(String oldSaveId, Identifier newSaveId) {
return createGlobalDataMoveFileFix(Map.of(oldSaveId, newSaveId));
}

static Function<Schema, FileFix> createGlobalDataMoveFileFix(Map<String, Identifier> saveIdMap) {
return schema -> new FileFix(schema) {
@Override
public void makeFixer() {
addFileFixOperation(createGlobalDataMoveOperation(saveIdMap));
}
};
}

static void registerGlobalDataMoveFileFix(String oldSaveId, Identifier newSaveId) {
registerGlobalDataMoveFileFix(Map.of(oldSaveId, newSaveId));
}

static void registerGlobalDataMoveFileFix(Map<String, Identifier> saveIdMap) {
FileFixSchemaRegisterCallback.registerFileFixes(FileFixerUpperAccessor.getFileFixerIntroductionVersion(), createGlobalDataMoveFileFix(saveIdMap));
}
Comment on lines +21 to +89
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd argue most of this file is out of scope, a mod can easily create its own file fix as required. Lets just keep the api simple.

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package net.fabricmc.fabric.api.serialization.v1.filefix;

import com.mojang.datafixers.schemas.Schema;

import net.fabricmc.fabric.api.event.Event;
import net.fabricmc.fabric.api.event.EventFactory;

import net.minecraft.util.filefix.FileFix;
import net.minecraft.util.filefix.FileFixerUpper;

import java.util.function.Function;

/**
* Event for registering file fixes. This event is called for every {@link Schema} added to
* Minecraft's {@link FileFixerUpper}, before any of vanilla's fixes are added to it.
*
* <p>Please note that this event is called early during the game launch, before {@link net.fabricmc.api.ModInitializer}s
* are run. Therefore, make sure to register callbacks early during the mod loading process, using
* a {@link net.fabricmc.loader.api.entrypoint.PreLaunchEntrypoint}.</p>
*/
@FunctionalInterface
public interface FileFixSchemaRegisterCallback {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No docs

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have now written documentation here


Event<FileFixSchemaRegisterCallback> EVENT = EventFactory.createArrayBacked(FileFixSchemaRegisterCallback.class,
listeners -> (fileFixerUpper, schema, version) -> {
for (FileFixSchemaRegisterCallback callback : listeners) {
callback.schemaRegistered(fileFixerUpper, schema, version);
}
});

void schemaRegistered(FileFixerUpper.Builder fileFixerUpper, Schema schema, int version);

/**
* Can be used to easily register file fixes for a data version. This method registers a callback
* at {@link FileFixSchemaRegisterCallback#EVENT}, which adds the given {@code fixes} to the
* game's {@link FileFixerUpper} at the given data version.
*
* <p>Please note that this method does not throw for invalid data versions, and that
* fixes can only be registered for a data version that has file fixes in vanilla, because
* there won't be a schema registered for those versions.</p>
*
* @param version the data version to register the file fixes for.
* @param fixes the file fixes to register.
*/
@SafeVarargs
static void registerFileFixes(int version, Function<Schema, FileFix>... fixes) {
EVENT.register((fileFixerUpper, schema, schemaVersion) -> {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the point in this function, is it not cleaner for a mod to just use the event?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mods will usually want to register file fixes for only one data version at a time. This method functions as a shortcut for that. In my opinion, it is cleaner to write:

FileFixSchemaRegisterCallback.registerFileFixes(4772, FileFixA::new, FileFixB::new);
FileFixSchemaRegisterCallback.registerFileFixes(4773, FileFixC::new);

Rather than:

FileFixSchemaRegisterCallback.EVENT.register((fileFixerUpper, schema, version) -> {
	if (version == 4772) {
		fileFixerUpper.addFixer(new FileFixA(schema));
		fileFixerUpper.addFixer(new FileFixB(schema));
	} else if (version == 4773) {
		fileFixerUpper.addFixer(new FileFixC(schema));
	}
});

Although I do suppose the latter would lead to less callbacks being registered at the event.

if (schemaVersion == version) {
for (Function<Schema, FileFix> fix : fixes) {
fileFixerUpper.addFixer(fix.apply(schema));
}
}
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package net.fabricmc.fabric.impl.serialization.filefix;

import net.minecraft.util.filefix.operations.FileFixOperation;
import net.minecraft.util.worldupdate.UpgradeProgress;

import java.io.IOException;
import java.nio.file.Path;
import java.util.List;

public record CombinedFileFixOperation(List<FileFixOperation> operations) implements FileFixOperation {

@Override
public void fix(Path baseDirectory, UpgradeProgress upgradeProgress) throws IOException {
for (FileFixOperation operation : operations) {
operation.fix(baseDirectory, upgradeProgress);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package net.fabricmc.fabric.impl.serialization.filefix;

import net.minecraft.resources.Identifier;
import net.minecraft.util.filefix.access.FileRelation;
import net.minecraft.util.filefix.operations.FileFixOperation;
import net.minecraft.util.filefix.operations.FileFixOperations;

import java.util.List;
import java.util.Map;

public interface FileFixHelpersImpl {

static List<FileFixOperation> createDimensionMoveOperations(Map<String, Identifier> saveIdMap,
String oldDimensionPath,
String newDimensionPath) {
return saveIdMap.entrySet().stream()
.map(entry -> createNamespacedDataMoveOperation(entry.getKey(), entry.getValue(),
oldDimensionPath + "/data/", newDimensionPath + "/data/"))
.toList();
}

static FileFixOperation createCustomDimensionDataMoveOperation(Map<String, Identifier> saveIdMap) {
return FileFixOperations.applyInFolders(
FileRelation.DIMENSIONS_DATA,
saveIdMap.entrySet().stream()
.map(entry -> createNamespacedDataMoveOperation(entry.getKey(), entry.getValue(), "", ""))
.toList()
);
}

static FileFixOperation createNamespacedDataMoveOperation(String oldId, Identifier newId, String oldPrefix, String newPrefix) {
return FileFixOperations.move(oldPrefix + oldId + ".dat", newPrefix + newId.getNamespace() + "/" + newId.getPath() + ".dat");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package net.fabricmc.fabric.mixin.serialization.filefix;

import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
import com.mojang.datafixers.DataFixerBuilder;
import com.mojang.datafixers.schemas.Schema;

import net.fabricmc.fabric.api.serialization.v1.filefix.FileFixSchemaRegisterCallback;

import net.minecraft.util.datafix.DataFixers;

import net.minecraft.util.filefix.FileFixerUpper;

import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;

import java.util.function.BiFunction;

@Mixin(DataFixers.class)
public abstract class DataFixersMixin {

@WrapOperation(method = "addFixers", at = @At(value = "INVOKE", target = "Lnet/minecraft/util/filefix/FileFixerUpper$Builder;addSchema(Lcom/mojang/datafixers/DataFixerBuilder;ILjava/util/function/BiFunction;)Lcom/mojang/datafixers/schemas/Schema;"))
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will this not target every addSchema in the function? Why can it not just be a simple @Inject at the end of the function.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It will, and that is intentional. Fixes added to a FileFixerUpper are added to the last schema registered at it. Placing an inject at the of the method would limit mods to only be able to add file fixes to the very last schema registered at the FileFixerUpper, which is not how it is intended to be used.

private static Schema runSchemaRegisterCallback(FileFixerUpper.Builder instance, DataFixerBuilder fixerUpper, int version, BiFunction<Integer, Schema, Schema> factory, Operation<Schema> original) {
Schema schema = original.call(instance, fixerUpper, version, factory);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whats the schema used for?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The schema is passed to the FileFixSchemaRegisterCallback event. Mods will need the schema to create file fix instances.

FileFixSchemaRegisterCallback.EVENT.invoker().schemaRegistered(instance, schema, version);
return schema;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package net.fabricmc.fabric.mixin.serialization.filefix;

import net.minecraft.util.filefix.FileFixerUpper;

import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.gen.Accessor;

@Mixin(FileFixerUpper.class)
public interface FileFixerUpperAccessor {

@Accessor("FILE_FIXER_INTRODUCTION_VERSION")
static int getFileFixerIntroductionVersion() {
throw new AssertionError();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
@NullMarked
package net.fabricmc.fabric.mixin.serialization.filefix;

import org.jspecify.annotations.NullMarked;
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
"TagValueInputMixin",
"TagValueOutputMixin",
"ValueInputMixin",
"ValueOutputMixin"
"ValueOutputMixin",
"filefix.DataFixersMixin",
"filefix.FileFixerUpperAccessor"
],
"injectors": {
"defaultRequire": 1
Expand Down