Skip to content

Commit

Permalink
Port FabricMC/fabric#2856 and attempt to add a confirmation screen.
Browse files Browse the repository at this point in the history
  • Loading branch information
LambdAurora committed Jan 30, 2023
1 parent a781ebc commit b821808
Show file tree
Hide file tree
Showing 11 changed files with 435 additions and 12 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"quilt.error.id.malformed": "The identifier \"%s\" is malformed.",
"quilt.error.registry.get_element_failure": "Could not find element \"%s\" in the registry."
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"quilt.error.id.malformed": "L'identifiant \"%s\" est malformé.",
"quilt.error.registry.get_element_failure": "L'élément \"%s\" n'a pas pu être trouvé dans le registre."
}
7 changes: 6 additions & 1 deletion library/worldgen/dimension/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,16 @@ qslModule {
moduleDependencies {
core {
api("qsl_base")
api("resource_loader")
testmodOnly("lifecycle_events")
testmodOnly("resource_loader")
}
management {
testmodOnly("command")
}
}
entrypoints {
client_init {
values = ["org.quiltmc.qsl.worldgen.dimension.impl.client.ClientQuiltDimensionsMod"]
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* Copyright 2023 QuiltMC
*
* 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 org.quiltmc.qsl.worldgen.dimension.impl;

import java.util.List;

import org.jetbrains.annotations.ApiStatus;

@ApiStatus.Internal
public interface DimensionDeserializationFailHandler {
boolean shouldContinueLoad(List<FailSoftMapCodec.DecodeError> errors);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
/*
* Copyright 2016, 2017, 2018, 2019 FabricMC
* Copyright 2023 QuiltMC
*
* 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 org.quiltmc.qsl.worldgen.dimension.impl;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;

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.jetbrains.annotations.ApiStatus;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import net.minecraft.text.HoverEvent;
import net.minecraft.text.Text;

/**
* Has the same functionality as {@link UnboundedMapCodec} but it will fail-soft when an entry cannot be deserialized.
*/
@ApiStatus.Internal
public class FailSoftMapCodec<K, V> implements BaseMapCodec<K, V>, Codec<Map<K, V>> {
public static final Logger LOGGER = LoggerFactory.getLogger("FailSoftMapCodec");

private final Codec<K> keyCodec;
private final Codec<V> elementCodec;
private final List<DecodeError> errors = new ArrayList<>();

public FailSoftMapCodec(final Codec<K> keyCodec, final Codec<V> elementCodec) {
this.keyCodec = keyCodec;
this.elementCodec = elementCodec;
}

@Override
public Codec<K> keyCodec() {
return this.keyCodec;
}

@Override
public Codec<V> elementCodec() {
return this.elementCodec;
}

public List<DecodeError> getErrors() {
return this.errors;
}

@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 this.encode(input, ops, ops.mapBuilder()).build(prefix);
}

/**
* In {@link BaseMapCodec#decode(DynamicOps, MapLike)}, the whole deserialization will fail if one element fails.
* {@code apply2stable} will return fail when any of the two elements is failed.
* In this implementation, if one deserialization fails, it will log and ignore.
*/
@Override
public <T> DataResult<Map<K, V>> decode(final DynamicOps<T> ops, final MapLike<T> input) {
final ImmutableMap.Builder<K, V> builder = ImmutableMap.builder();
this.errors.clear();

input.entries().forEach(pair -> {
try {
final DataResult<K> k = this.keyCodec().parse(ops, pair.getFirst());
final DataResult<V> v = this.elementCodec().parse(ops, pair.getSecond());

k.get().ifRight(partial -> {
LOGGER.error("Failed to decode key {} from {} {}", k, pair, partial);

this.errors.add(new DecodeError(Kind.KEY, partial.message()));
});
v.get().ifRight(partial -> {
LOGGER.error("Failed to decode value {} from {} {}", v, pair, partial);

this.errors.add(new DecodeError(Kind.VALUE, partial.message()));
});

if (k.get().left().isPresent() && v.get().left().isPresent()) {
builder.put(k.get().left().get(), v.get().left().get());
} // ignore failure
} catch (Throwable e) {
LOGGER.error("Decoding {}", pair, e);
}
});

final Map<K, V> elements = builder.build();

return DataResult.success(elements);
}

@Override
public boolean equals(final Object o) {
if (this == o) {
return true;
}

if (o == null || getClass() != o.getClass()) {
return false;
}

final var that = (FailSoftMapCodec<?, ?>) o;
return Objects.equals(this.keyCodec, that.keyCodec) && Objects.equals(this.elementCodec, that.elementCodec);
}

@Override
public int hashCode() {
return Objects.hash(this.keyCodec, this.elementCodec);
}

@Override
public String toString() {
return "FailSoftMapCodec[" + this.keyCodec + " -> " + this.elementCodec + ']';
}

public enum Kind {
KEY {
private static final String NOT_A_VALID_ID = "Not a valid resource location: ";

@Override
Text getFancyMessage(String originalMessage) {
if (originalMessage.startsWith(NOT_A_VALID_ID)) {
var substr = originalMessage.substring(NOT_A_VALID_ID.length());

return Text.translatable("quilt.error.id.malformed",
substr.substring(0, substr.indexOf(" "))
)
.styled(style -> style.withHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, Text.literal(originalMessage))));
}

return Text.literal(originalMessage);
}
},
VALUE {
private static final String FAILED_TO_GET_ELEMENT = "Failed to get element ";

@Override
Text getFancyMessage(String originalMessage) {
if (originalMessage.startsWith(FAILED_TO_GET_ELEMENT)) {
return Text.translatable("quilt.error.registry.get_element_failure",
originalMessage.substring(FAILED_TO_GET_ELEMENT.length())
)
.styled(style -> style.withHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, Text.literal(originalMessage))));
}

return Text.literal(originalMessage);
}
};

abstract Text getFancyMessage(String originalMessage);
}

public record DecodeError(Kind kind, String message) {
public Text getFancyMessage() {
return this.kind.getFancyMessage(this.message);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/*
* Copyright 2016, 2017, 2018, 2019 FabricMC
* Copyright 2022 QuiltMC
* Copyright 2022-2023 QuiltMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -19,16 +19,39 @@

import com.google.common.base.Preconditions;
import org.jetbrains.annotations.ApiStatus;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import net.minecraft.entity.Entity;
import net.minecraft.registry.RegistryKey;
import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.server.world.ServerWorld;
import net.minecraft.world.TeleportTarget;
import net.minecraft.world.dimension.DimensionOptions;

import org.quiltmc.qsl.base.api.util.TriState;

@ApiStatus.Internal
public class QuiltDimensionsImpl {
private static final Logger LOGGER = LoggerFactory.getLogger("QuiltDimensions");
private static final String IGNORE_FAIL_KEY = "quilt.dimension.ignore_failed_deserialization";
public static final boolean IGNORE_FAIL = TriState.fromProperty(IGNORE_FAIL_KEY).toBooleanOrElse(false);
private static DimensionDeserializationFailHandler failHandler = errors -> {
if (!IGNORE_FAIL) {
LOGGER.error("""
Failed to deserialize dimensions from NBT due to missing elements.
No confirmation interface could be displayed, so the loading of the world will be cancelled.
If you wish to ignore this deserialization issue and force-load the world, please specify "-D{}=true" in JVM arguments.""",
IGNORE_FAIL_KEY);
}

return IGNORE_FAIL;
};
private static FailSoftMapCodec<RegistryKey<DimensionOptions>, DimensionOptions> dimensionFailSoftMapCodec;

// Static only-class, no instantiation necessary!
private QuiltDimensionsImpl() {
throw new UnsupportedOperationException("QuiltDimensionsImpl only contains static definitions.");
}

@SuppressWarnings("unchecked")
Expand Down Expand Up @@ -61,4 +84,24 @@ public static <E extends Entity> E teleport(Entity entity, ServerWorld destinati
access.setTeleportTarget(null);
}
}

public static void setDimensionFailSoftMapCodec(FailSoftMapCodec<RegistryKey<DimensionOptions>, DimensionOptions> codec) {
dimensionFailSoftMapCodec = codec;
}

public static void setDimensionFailHandler(DimensionDeserializationFailHandler failHandler) {
QuiltDimensionsImpl.failHandler = failHandler;
}

public static boolean canContinueWorldLoad() {
boolean result = true;

if (!dimensionFailSoftMapCodec.getErrors().isEmpty()) {
result = failHandler.shouldContinueLoad(dimensionFailSoftMapCodec.getErrors());
}

dimensionFailSoftMapCodec.getErrors().clear();

return result;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/*
* Copyright 2023 QuiltMC
*
* 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 org.quiltmc.qsl.worldgen.dimension.impl.client;

import org.jetbrains.annotations.ApiStatus;

import net.minecraft.client.MinecraftClient;
import net.minecraft.client.gui.screen.ConfirmScreen;
import net.minecraft.text.Text;

import org.quiltmc.loader.api.ModContainer;
import org.quiltmc.qsl.base.api.entrypoint.client.ClientModInitializer;
import org.quiltmc.qsl.worldgen.dimension.impl.FailSoftMapCodec;
import org.quiltmc.qsl.worldgen.dimension.impl.QuiltDimensionsImpl;
import org.quiltmc.qsl.worldgen.dimension.mixin.client.MinecraftClientAccessor;

@ApiStatus.Internal
public class ClientQuiltDimensionsMod implements ClientModInitializer {
@Override
public void onInitializeClient(ModContainer mod) {
QuiltDimensionsImpl.setDimensionFailHandler(errors -> {
if (QuiltDimensionsImpl.IGNORE_FAIL) return true;

MinecraftClient client = MinecraftClient.getInstance();
var oldScreen = client.currentScreen;
client.currentScreen = null;
final var result = new boolean[2];
result[1] = true;

var errorMessage = errors.stream()
.map(FailSoftMapCodec.DecodeError::getFancyMessage)
.reduce(Text.literal("Errors:\n"), (mutableText, text) -> mutableText.copy().append("\n - ").append(text));

var confirmScreen = new ConfirmScreen(res -> {
result[0] = res;
result[1] = false;
}, Text.translatable("quilt.error.dimension.deserialization"), errorMessage);

client.setScreen(confirmScreen);

// This is quite bad but there's not much choice since this will run on the render thread.
while (result[1]) {
((MinecraftClientAccessor) client).invokeRender(true);
}

client.setScreen(oldScreen);

return result[0];
});
}
}
Loading

1 comment on commit b821808

@LambdAurora
Copy link
Owner Author

Choose a reason for hiding this comment

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

Note to whoever sees this: this is WIP.

Please sign in to comment.