Fix cannot load world after uninstalling dimension mod/datapack without breaking world presets. ()

* Fix the issue that cannot load world after uninstalling a dimension mod/datapack, by making deserialization fail-soft, instead of removing non-vanilla dimensions.

* Fix style.

* Fix license.

* Small changes to FailSoftMapCodec.

* Make FailSoftMapCodec final.

* Revert "Make FailSoftMapCodec final."

This reverts commit 0c0642a1c4.

* Use ModifyVariable and change comments.

* Remove unnecessary `equals`
This commit is contained in:
qouteall 2023-02-13 17:30:31 +08:00 committed by GitHub
parent e63306e015
commit 00a2eb1095
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 157 additions and 84 deletions
fabric-dimensions-v1/src/main

View file

@ -0,0 +1,94 @@
/*
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.fabricmc.fabric.impl.dimension;
import java.util.Map;
import com.google.common.collect.ImmutableMap;
import com.mojang.datafixers.util.Pair;
import com.mojang.serialization.Codec;
import com.mojang.serialization.DataResult;
import com.mojang.serialization.DynamicOps;
import com.mojang.serialization.Lifecycle;
import com.mojang.serialization.MapLike;
import com.mojang.serialization.codecs.BaseMapCodec;
import com.mojang.serialization.codecs.UnboundedMapCodec;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Has the same functionality as {@link UnboundedMapCodec}.
* But it will fail-soft when an entry cannot be deserialized.
*/
public record FailSoftMapCodec<K, V>(Codec<K> keyCodec, Codec<V> elementCodec) implements BaseMapCodec<K, V>, Codec<Map<K, V>> {
private static final Logger LOGGER = LoggerFactory.getLogger("FailSoftMapCodec");
@Override
public <T> DataResult<Pair<Map<K, V>, T>> decode(final DynamicOps<T> ops, final T input) {
return ops.getMap(input).setLifecycle(Lifecycle.stable()).flatMap(map -> decode(ops, map)).map(r -> Pair.of(r, input));
}
@Override
public <T> DataResult<T> encode(final Map<K, V> input, final DynamicOps<T> ops, final T prefix) {
return encode(input, ops, ops.mapBuilder()).build(prefix);
}
/**
* In {@link BaseMapCodec#decode(DynamicOps, MapLike)},
* the whole deserialization will fail if one element fails.
* `apply2stable` will return fail when any of the two elements is failed.
* In this implementation, if one deserialization fails, it will log and ignore.
* The result will always be success.
* It will not output partial result when some entries fail deserialization because
* currently (MC 1.19.3) the dimension data deserialization rejects partial result.
*/
@Override
public <T> DataResult<Map<K, V>> decode(final DynamicOps<T> ops, final MapLike<T> input) {
final ImmutableMap.Builder<K, V> builder = ImmutableMap.builder();
input.entries().forEach(pair -> {
try {
final DataResult<K> k = keyCodec().parse(ops, pair.getFirst());
final DataResult<V> v = elementCodec().parse(ops, pair.getSecond());
k.get().ifRight(kPartialResult -> {
LOGGER.error("Failed to decode key {} from {} {}", k, pair, kPartialResult);
});
v.get().ifRight(vPartialResult -> {
LOGGER.error("Failed to decode value {} from {} {}", v, pair, vPartialResult);
});
if (k.get().left().isPresent() && v.get().left().isPresent()) {
builder.put(k.get().left().get(), v.get().left().get());
} else {
// ignore failure
}
} catch (Throwable e) {
LOGGER.error("Decoding {}", pair, e);
}
});
final Map<K, V> elements = builder.build();
return DataResult.success(elements);
}
@Override
public String toString() {
return "FailSoftMapCodec[" + keyCodec + " -> " + elementCodec + ']';
}
}

View file

@ -1,83 +0,0 @@
/*
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.fabricmc.fabric.mixin.dimension;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import com.mojang.datafixers.DataFixer;
import com.mojang.datafixers.util.Pair;
import com.mojang.serialization.Dynamic;
import com.mojang.serialization.Lifecycle;
import net.minecraft.nbt.NbtCompound;
import net.minecraft.nbt.NbtElement;
import net.minecraft.world.gen.GeneratorOptions;
import net.minecraft.world.level.storage.LevelStorage;
/**
* After removing a dimension mod or a dimension datapack, Minecraft may fail to enter
* the world, because it fails to deserialize the chunk generator of the custom dimensions in file {@code level.dat}
* This mixin will remove the custom dimensions from the nbt tag, so the deserializer and DFU cannot see custom
* dimensions and won't cause errors.
* The custom dimensions will be re-added later.
*
* <p>This Mixin changes a vanilla behavior that is deemed as a bug (MC-197860). In vanilla, the custom dimension
* is not removed after uninstalling the dimension datapack.
* This makes custom dimensions non-removable. Most players don't want this behavior.
* With this Mixin, custom dimensions will be removed when its datapack is removed.
*/
@Mixin(LevelStorage.class)
public class LevelStorageMixin {
@SuppressWarnings("unchecked")
@Inject(method = "readGeneratorProperties", at = @At("HEAD"))
private static <T> void onReadGeneratorProperties(
Dynamic<T> nbt, DataFixer dataFixer, int version,
CallbackInfoReturnable<Pair<GeneratorOptions, Lifecycle>> cir
) {
NbtElement nbtTag = ((Dynamic<NbtElement>) nbt).getValue();
NbtCompound worldGenSettings = ((NbtCompound) nbtTag).getCompound("WorldGenSettings");
removeNonVanillaDimensionsFromWorldGenSettingsTag(worldGenSettings);
}
/**
* Removes all non-vanilla dimensions from the tag. The custom dimensions will be re-added later from the datapacks.
*/
@Unique
private static void removeNonVanillaDimensionsFromWorldGenSettingsTag(NbtCompound worldGenSettings) {
String[] vanillaDimensionIds =
new String[]{"minecraft:overworld", "minecraft:the_nether", "minecraft:the_end"};
NbtCompound dimensions = worldGenSettings.getCompound("dimensions");
if (dimensions.getSize() > vanillaDimensionIds.length) {
NbtCompound newDimensions = new NbtCompound();
for (String dimId : vanillaDimensionIds) {
if (dimensions.contains(dimId)) {
newDimensions.put(dimId, dimensions.getCompound(dimId));
}
}
worldGenSettings.put("dimensions", newDimensions);
}
}
}

View file

@ -0,0 +1,62 @@
/*
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.fabricmc.fabric.mixin.dimension;
import java.util.Map;
import com.mojang.serialization.Codec;
import com.mojang.serialization.Lifecycle;
import com.mojang.serialization.codecs.UnboundedMapCodec;
import org.apache.commons.lang3.Validate;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.ModifyVariable;
import net.minecraft.registry.Registry;
import net.minecraft.registry.RegistryCodecs;
import net.minecraft.registry.RegistryKey;
import net.fabricmc.fabric.impl.dimension.FailSoftMapCodec;
@Mixin(RegistryCodecs.class)
public class RegistryCodecsMixin {
/**
* Fix the issue that cannot load world after uninstalling a dimension mod/datapack.
* After uninstalling a dimension mod/datapack, the dimension config in `level.dat` file cannot be deserialized.
* The solution is to make it fail-soft.
* Currently (1.19.3), `createKeyedRegistryCodec` is only used in dimension codec.
*/
@ModifyVariable(
method = "createKeyedRegistryCodec",
at = @At(
value = "INVOKE_ASSIGN",
target = "Lcom/mojang/serialization/Codec;unboundedMap(Lcom/mojang/serialization/Codec;Lcom/mojang/serialization/Codec;)Lcom/mojang/serialization/codecs/UnboundedMapCodec;",
remap = false
),
ordinal = 1 // there are two local variables of `Codec` type. Modify the second.
)
private static <E> Codec<Map<RegistryKey<E>, E>> modifyCodecLocalVariable(
Codec<Map<RegistryKey<E>, E>> originalVariable,
RegistryKey<? extends Registry<E>> registryRef,
Lifecycle lifecycle, Codec<E> elementCodec
) {
// make sure that it's not modifying the wrong variable
Validate.isTrue(originalVariable instanceof UnboundedMapCodec<?, ?>);
return new FailSoftMapCodec<>(RegistryKey.createCodec(registryRef), elementCodec);
}
}

View file

@ -4,7 +4,7 @@
"compatibilityLevel": "JAVA_16",
"mixins": [
"EntityMixin",
"LevelStorageMixin"
"RegistryCodecsMixin"
],
"injectors": {
"defaultRequire": 1