Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[1.20.4/5] Improved ModMismatchDisconnectedScreen and clientside handling of channel mismatches in general #672

Merged
merged 14 commits into from
May 28, 2024
Merged
Show file tree
Hide file tree
Changes from 2 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
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

public ClientConfigurationPacketListenerImpl(Minecraft p_295262_, Connection p_296339_, CommonListenerCookie p_294706_) {
super(p_295262_, p_296339_, p_294706_);
@@ -45,7 +_,41 @@
@@ -45,7 +_,43 @@
}

@Override
Expand Down Expand Up @@ -38,6 +38,8 @@
+ }
+ if (p_295411_ instanceof net.neoforged.neoforge.network.payload.ModdedNetworkSetupFailedPayload setupFailedPayload) {
+ failureReasons = setupFailedPayload.failureReasons();
+ failureReasons.forEach((r, c) -> LOGGER.warn("Channel [{}] failed to connect: {}", r, c.getString()));
+ return;
+ }
+ if (!this.connectionType.isNeoForge() && p_295411_ instanceof net.minecraft.network.protocol.common.custom.BrandPayload) {
+ this.initializedConnection = true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
Expand All @@ -19,6 +21,7 @@
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.GuiGraphics;
import net.minecraft.client.gui.components.Button;
import net.minecraft.client.gui.components.MultiLineLabel;
import net.minecraft.client.gui.narration.NarrationElementOutput;
import net.minecraft.client.gui.screens.Screen;
import net.minecraft.network.chat.ClickEvent;
Expand All @@ -30,22 +33,26 @@
import net.minecraft.network.chat.TextColor;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.util.FormattedCharSequence;
import net.neoforged.fml.ModContainer;
import net.neoforged.fml.ModList;
import net.neoforged.fml.loading.FMLPaths;
import net.neoforged.neoforge.client.gui.widget.ScrollPanel;
import net.neoforged.neoforge.common.I18nExtension;
import org.apache.commons.lang3.tuple.Pair;

public class ModMismatchDisconnectedScreen extends Screen {
private final Component reason;
private MultiLineLabel message = MultiLineLabel.EMPTY;
private final Screen parent;
private int textHeight;
private final Path modsDir;
private final Path logFile;
private final int listHeight = 140;
private final Map<ResourceLocation, Component> mismatchedChannelData;

public ModMismatchDisconnectedScreen(Screen parentScreen, Component title, Map<ResourceLocation, Component> mismatchedChannelData) {
super(title);
public ModMismatchDisconnectedScreen(Screen parentScreen, Component reason, Map<ResourceLocation, Component> mismatchedChannelData) {
super(Component.translatable("disconnect.lost"));
this.reason = reason;
this.parent = parentScreen;
this.modsDir = FMLPaths.MODSDIR.get();
this.logFile = FMLPaths.GAMEDIR.get().resolve(Paths.get("logs", "latest.log"));
Expand All @@ -56,8 +63,12 @@ public ModMismatchDisconnectedScreen(Screen parentScreen, Component title, Map<R
protected void init() {
int listLeft = Math.max(8, this.width / 2 - 220);
int listWidth = Math.min(440, this.width - 16);
int upperButtonHeight = Math.min((this.height + this.listHeight + this.textHeight) / 2 + 10, this.height - 50);
int lowerButtonHeight = Math.min((this.height + this.listHeight + this.textHeight) / 2 + 35, this.height - 25);

this.message = MultiLineLabel.create(this.font, this.reason, this.width - 50);
this.textHeight = this.message.getLineCount() * 9;

int upperButtonHeight = Math.min((this.height + this.listHeight) / 2 + 25, this.height - 50);
int lowerButtonHeight = Math.min((this.height + this.listHeight) / 2 + 50, this.height - 25);
this.addRenderableWidget(new MismatchInfoPanel(minecraft, listWidth, listHeight, (this.height - this.listHeight) / 2, listLeft));

int buttonWidth = Math.min(210, this.width / 2 - 20);
Expand All @@ -75,8 +86,8 @@ protected void init() {
@Override
public void render(GuiGraphics guiGraphics, int mouseX, int mouseY, float partialTicks) {
this.renderBackground(guiGraphics, mouseX, mouseY, partialTicks);
int textYOffset = 18;
guiGraphics.drawCenteredString(this.font, this.title, this.width / 2, (this.height - this.listHeight - this.textHeight) / 2 - textYOffset - 9 * 2, 0xAAAAAA);
guiGraphics.drawCenteredString(this.font, this.title, this.width / 2, (this.height - this.listHeight) / 2 - this.textHeight - 9 * 4, 0xAAAAAA);
this.message.renderCentered(guiGraphics, this.width / 2, (this.height - this.listHeight) / 2 - this.textHeight - 9 * 2);
super.render(guiGraphics, mouseX, mouseY, partialTicks);
}

Expand All @@ -85,24 +96,25 @@ class MismatchInfoPanel extends ScrollPanel {
private final int contentSize;
private final int nameIndent = 10;
private final int tableWidth = width - border * 2 - 6 - nameIndent;
private final int nameWidth = tableWidth * 3 / 5;
private final int versionWidth = (tableWidth - nameWidth) / 2;
private final int nameWidth = tableWidth / 2;
private final int versionWidth = tableWidth - nameWidth;

public MismatchInfoPanel(Minecraft client, int width, int height, int top, int left) {
super(client, width, height, top, left);

//The raw list of the strings in a table row, the components may still be too long for the final table and will be split up later. The first row element may have a style assigned to it that will be used for the whole content row.
Map<ResourceLocation, Pair<Integer, Component>> collapsedChannelData = collapseChannelData(mismatchedChannelData);
record Row(MutableComponent name, MutableComponent reason) {}
//The raw list of the strings in a table row, the components may still be too long for the final table and will be split up later. The first row element may have a style assigned to it that will be used for the whole content row.
List<Row> rows = new ArrayList<>();
if (!mismatchedChannelData.isEmpty()) {
//This table section contains the mod name and both mod versions of each mod that has a mismatching client and server version
if (!collapsedChannelData.isEmpty()) {
//Each table row contains the mod name and the reason for the corresponding channel mismatch.
rows.add(new Row(Component.translatable("fml.modmismatchscreen.table.channelname"), Component.translatable("fml.modmismatchscreen.table.reason")));
int i = 0;
for (Map.Entry<ResourceLocation, Component> modData : mismatchedChannelData.entrySet()) {
rows.add(new Row(toChannelNameComponent(modData.getKey()), modData.getValue().copy()));
if (++i >= 10) {
for (Map.Entry<ResourceLocation, Pair<Integer, Component>> channelData : collapsedChannelData.entrySet()) {
rows.add(new Row(toChannelNameComponent(channelData.getKey(), channelData.getValue().getLeft(), i % 2 == 0 ? ChatFormatting.GOLD : ChatFormatting.YELLOW), channelData.getValue().getRight().copy()));
if (++i == 20 && collapsedChannelData.size() > 20) {
//If too many mismatched mod entries are present, append a line referencing how to see the full list and stop rendering any more entries
rows.add(new Row(Component.literal(""), Component.translatable("fml.modmismatchscreen.additional", mismatchedChannelData.size() - i).withStyle(ChatFormatting.ITALIC)));
rows.add(new Row(Component.literal(""), Component.translatable("fml.modmismatchscreen.additional", collapsedChannelData.size() - i).withStyle(ChatFormatting.ITALIC)));
break;
}
}
Expand All @@ -114,7 +126,41 @@ record Row(MutableComponent name, MutableComponent reason) {}
}

/**
* Splits the raw name and version strings, making them use multiple lines if needed, to fit within the table dimensions.
* Collapses quasi-duplicate channel mismatch entries into single list entries to reduce repetition of entries in the final list.
* Quasi-duplicate channel mismatch entries share the same channel namespace and mismatch reasons, and it is thus very likely that they are caused
* by one and the same mod (that has registered all of these channels) missing/mismatching between client and server.
*
* @param mismatchedChannelData The raw mismatched channel data received from the server, which might contain quasi-duplicate entries
* @return A map containing deduplicated channel mismatch entries. For each quasi-duplicate group, only the first encountered channel id is kept,
* and all other quasi-duplicate channels then increment the associated repetition count that is mapped to that first channel id.
* Finally, the (unchanged) mismatch reason (which is the same for all quasi-duplicate entries) also gets mapped to the channel id.
*/
private Map<ResourceLocation, Pair<Integer, Component>> collapseChannelData(Map<ResourceLocation, Component> mismatchedChannelData) {
RedstoneDubstep marked this conversation as resolved.
Show resolved Hide resolved
Map<ResourceLocation, Pair<Integer, Component>> repetitions = new LinkedHashMap<>();
List<ResourceLocation> sortedChannels = mismatchedChannelData.keySet().stream().sorted(Comparator.comparing(ResourceLocation::toString)).toList();
for (ResourceLocation channel : sortedChannels) {
Component channelMismatchReason = mismatchedChannelData.get(channel);
List<ResourceLocation> namespaceChannels = repetitions.keySet().stream().filter(r -> r.getNamespace().equals(channel.getNamespace())).toList();
boolean matched = false;
if (!namespaceChannels.isEmpty()) {
for (ResourceLocation potentialRepetitionChannel : namespaceChannels) {
Pair<Integer, Component> repetitionData = repetitions.get(potentialRepetitionChannel);

if (repetitionData.getRight().equals(channelMismatchReason)) {
repetitions.put(potentialRepetitionChannel, Pair.of(repetitionData.getLeft() + 1, repetitionData.getRight()));
matched = true;
}
}
}
if (!matched)
repetitions.put(channel, Pair.of(1, channelMismatchReason));
}

return repetitions;
}

/**
* Splits the raw mod name and mismatch reason strings, making them use multiple lines if needed, to fit within the table dimensions.
* The style assigned to the name element is then applied to the entire content row.
*
* @param name The first element of the content row, usually representing a table section header or the name of a mod entry
Expand All @@ -123,9 +169,7 @@ record Row(MutableComponent name, MutableComponent reason) {}
*/
private List<Pair<FormattedCharSequence, FormattedCharSequence>> splitLineToWidth(MutableComponent name, MutableComponent reason) {
Style style = name.getStyle();
int versionColumns = 1;
int adaptedNameWidth = nameWidth + versionWidth * (2 - versionColumns) - 4; //the name width may be expanded when the version column string is missing
List<FormattedCharSequence> nameLines = font.split(name, adaptedNameWidth);
List<FormattedCharSequence> nameLines = font.split(name, nameWidth - 4);
List<FormattedCharSequence> reasonLines = font.split(reason.setStyle(style), versionWidth - 4);
List<Pair<FormattedCharSequence, FormattedCharSequence>> splitLines = new ArrayList<>();

Expand All @@ -137,25 +181,31 @@ private List<Pair<FormattedCharSequence, FormattedCharSequence>> splitLineToWidt
}

/**
* Adds a style information to the given mod name string. The style assigned to the returned component contains the color of the mod name,
* a hover event containing the given id, and an optional click event, which opens the homepage of mod, if present.
* Uses the given channel id to return a component with the name of the mod that likely owns the channel. If no such mod is found, the namespace of the channel id is used instead.
RedstoneDubstep marked this conversation as resolved.
Show resolved Hide resolved
* The style assigned to the returned component contains the color of the entry, a hover event containing the channel id, and an optional click event which, if present, opens the homepage of the mod.
*
* @param id An id that gets displayed in the hover event. Depending on the origin it may only consist of a namespace (the mod id) or a namespace + path (a channel id associated with the mod).
* @return A component with the mod name as the main text component, and an assigned style which will be used for the whole content row.
* @param id The id of the mismatched channel. Used to query the name of the mod that has likely registered the channel in order to use and display its name and homepage URL.
* @param repetitionCount How many other channels of the same mod failed negotiation with the same error message. Displayed in the hover tooltip.
* @param color Defines the color of the returned channel name component.
* @return A component with the mod name (if available) as the main text component, and an assigned color which will be used for the whole content row.
*/
private MutableComponent toChannelNameComponent(ResourceLocation id) {
private MutableComponent toChannelNameComponent(ResourceLocation id, int repetitionCount, ChatFormatting color) {
String modId = id.getNamespace();

String url = ModList.get().getModContainerById(modId)
.flatMap(container -> container.getModInfo().getModURL())
Optional<? extends ModContainer> mod = ModList.get().getModContainerById(modId);
String name = mod.map(m -> m.getModInfo().getDisplayName()).orElse(modId);
String url = mod.flatMap(container -> container.getModInfo().getModURL())
.map(URL::toString)
.orElse("");
MutableComponent result = Component.literal(id.toString()).withStyle(ChatFormatting.YELLOW);
MutableComponent result = Component.literal(name).withStyle(color);
MutableComponent hoverText = Component.literal(id.toString());
if (repetitionCount > 1)
hoverText.append(Component.literal(" [+%s more]".formatted(repetitionCount)).withStyle(ChatFormatting.GRAY));
if (!url.isEmpty()) {
result = result.withStyle(s -> s.withHoverEvent(new HoverEvent(Action.SHOW_TEXT, Component.translatable("fml.modmismatchscreen.table.visit.mod_page", id.toString()))))
.withStyle(s -> s.withClickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, url)));
hoverText.append(Component.literal("\n").append(Component.translatable("fml.modmismatchscreen.table.visit.mod_page", id.toString())));
result = result.withStyle(s -> s.withClickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, url)));
}

result = result.withStyle(s -> s.withHoverEvent(new HoverEvent(Action.SHOW_TEXT, hoverText)));
return result;
}

Expand Down
Loading