mirror of
https://github.com/FabricMC/fabric.git
synced 2025-04-21 03:10:54 -04:00
Fix cannot load world after uninstalling dimension mod/datapack without breaking world presets. (#2856)
* 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:
parent
e63306e015
commit
00a2eb1095
4 changed files with 157 additions and 84 deletions
fabric-dimensions-v1/src/main
java/net/fabricmc/fabric
resources
|
@ -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 + ']';
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -4,7 +4,7 @@
|
|||
"compatibilityLevel": "JAVA_16",
|
||||
"mixins": [
|
||||
"EntityMixin",
|
||||
"LevelStorageMixin"
|
||||
"RegistryCodecsMixin"
|
||||
],
|
||||
"injectors": {
|
||||
"defaultRequire": 1
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue