diff options
author | SoniEx2 <endermoneymod@gmail.com> | 2022-07-03 23:12:06 -0300 |
---|---|---|
committer | SoniEx2 <endermoneymod@gmail.com> | 2022-07-03 23:12:06 -0300 |
commit | bfb981cd49a6bbcd15482dceeb4ab121c0408157 (patch) | |
tree | 54f1fca2884e68f5a8e6205bf34a01cc62c8a953 /src/main/java/ganarchy/friendcode/client | |
parent | 0c1c11065062c745ce49529e9bee48b05aa4bc41 (diff) |
[Project] Friend Code
A Minecraft mod which adds friend codes, an easy way to play in the same world with remote friends.
Diffstat (limited to 'src/main/java/ganarchy/friendcode/client')
7 files changed, 758 insertions, 0 deletions
diff --git a/src/main/java/ganarchy/friendcode/client/FriendCodeScreen.java b/src/main/java/ganarchy/friendcode/client/FriendCodeScreen.java new file mode 100644 index 0000000..38703a5 --- /dev/null +++ b/src/main/java/ganarchy/friendcode/client/FriendCodeScreen.java @@ -0,0 +1,141 @@ +package ganarchy.friendcode.client; + +import ganarchy.friendcode.FriendCode; +import ganarchy.friendcode.mixin.FriendCodeIntegratedServerExt; +import net.fabricmc.api.EnvType; +import net.fabricmc.api.Environment; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.gui.screen.OpenToLanScreen; +import net.minecraft.client.gui.screen.Screen; +import net.minecraft.client.gui.widget.ButtonWidget; +import net.minecraft.client.gui.widget.CyclingButtonWidget; +import net.minecraft.client.util.NetworkUtils; +import net.minecraft.client.util.math.MatrixStack; +import net.minecraft.screen.ScreenTexts; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.text.MutableText; +import net.minecraft.text.Text; +import net.minecraft.world.GameMode; + +import java.io.IOException; + +@Environment(EnvType.CLIENT) +public class FriendCodeScreen extends Screen { + private static final Text ALLOW_COMMANDS_TEXT = Text.translatable("selectWorld.allowCommands"); + private static final Text GAME_MODE_TEXT = Text.translatable("selectWorld.gameMode"); + private static final Text OTHER_PLAYERS_TEXT = Text.translatable("lanServer.otherPlayers"); + private final Screen parent; + private GameMode gameMode = GameMode.SURVIVAL; + private boolean allowCommands; + + private static final Text START_SHARING = Text.translatable("friendcode.start"); + + public FriendCodeScreen(Screen parent) { + super(Text.translatable("friendcode.title")); + this.parent = parent; + } + + @Override + protected void init() { + // title button allows going back to LAN screen + this.addDrawableChild(new ButtonWidget( + this.width / 2 - 155, + 50, + 310, + 20, + this.title, + button -> this.client.setScreen(new OpenToLanScreen(this.parent)) + )); + + // game setting buttons + this.addDrawableChild( + CyclingButtonWidget + .builder(GameMode::getSimpleTranslatableName) + .values(GameMode.SURVIVAL, GameMode.SPECTATOR, GameMode.CREATIVE, GameMode.ADVENTURE) + .initially(this.gameMode) + .build( + this.width / 2 - 155, + 100, + 150, + 20, + GAME_MODE_TEXT, + (button, gameMode) -> { + this.gameMode = gameMode; + } + ) + ); + this.addDrawableChild( + CyclingButtonWidget + .onOffBuilder(this.allowCommands) + .build( + this.width / 2 + 5, + 100, + 150, + 20, + ALLOW_COMMANDS_TEXT, + (button, allowCommands) -> { + this.allowCommands = allowCommands; + } + ) + ); + + // start sharing + this.addDrawableChild(new ButtonWidget( + this.width / 2 - 155, + this.height - 28, + 150, + 20, + START_SHARING, + button -> { + // FIXME + int i = NetworkUtils.findLocalPort(); + var samPinger = openToFriends(this.client, this.gameMode, this.allowCommands, i); + MutableText text = samPinger != null ? Text.translatable("commands.publish.started", i) : Text.translatable("commands.publish.failed"); + this.client.inGameHud.getChatHud().addMessage(text); + this.client.updateWindowTitle(); + this.client.setScreen(new WaitingForFriendCodeScreen(samPinger)); + } + )); + + // go back to options + this.addDrawableChild(new ButtonWidget( + this.width / 2 + 5, + this.height - 28, + 150, + 20, + ScreenTexts.CANCEL, + button -> this.client.setScreen(this.parent) + )); + } + + private static I2PSamPinger openToFriends(MinecraftClient client, GameMode gameMode, boolean allowCommands, int port) { + try { + client.loadBlockList(); + client.getServer().getNetworkIo().bind(null, port); + FriendCode.LOGGER.info("Started serving on {}", port); + ((FriendCodeIntegratedServerExt) client.getServer()).lanPort(port); + // reuse LAN pinger machinery instead of rolling our own + var lanPinger = new I2PSamPinger(client.getServer().getServerMotd(), "" + port); + ((FriendCodeIntegratedServerExt) client.getServer()).lanPinger(lanPinger); + lanPinger.start(); + ((FriendCodeIntegratedServerExt) client.getServer()).forcedGameMode(gameMode); + client.getServer().getPlayerManager().setCheatsAllowed(allowCommands); + int i = client.getServer().getPermissionLevel(client.player.getGameProfile()); + client.player.setClientPermissionLevel(i); + for (ServerPlayerEntity serverPlayerEntity : client.getServer().getPlayerManager().getPlayerList()) { + client.getServer().getCommandManager().sendCommandTree(serverPlayerEntity); + } + return lanPinger; + } + catch (IOException iOException) { + return null; + } + } + + @Override + public void render(MatrixStack matrices, int mouseX, int mouseY, float delta) { + this.renderBackground(matrices); + FriendCodeScreen.drawCenteredText(matrices, this.textRenderer, OTHER_PLAYERS_TEXT, this.width / 2, 82, 0xFFFFFF); + super.render(matrices, mouseX, mouseY, delta); + } +} diff --git a/src/main/java/ganarchy/friendcode/client/FriendConnectScreen.java b/src/main/java/ganarchy/friendcode/client/FriendConnectScreen.java new file mode 100644 index 0000000..53282d7 --- /dev/null +++ b/src/main/java/ganarchy/friendcode/client/FriendConnectScreen.java @@ -0,0 +1,111 @@ +package ganarchy.friendcode.client; + +import it.unimi.dsi.fastutil.booleans.BooleanConsumer; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.gui.screen.DirectConnectScreen; +import net.minecraft.client.gui.screen.Screen; +import net.minecraft.client.gui.widget.ButtonWidget; +import net.minecraft.client.gui.widget.TextFieldWidget; +import net.minecraft.client.network.ServerInfo; +import net.minecraft.client.util.math.MatrixStack; +import net.minecraft.screen.ScreenTexts; +import net.minecraft.text.Text; +import org.lwjgl.glfw.GLFW; + +import java.util.regex.Pattern; + +public class FriendConnectScreen extends Screen { + private static final Text ENTER_CODE_TEXT = Text.translatable("friendcode.enter_code"); + /** + * Pattern for old-style b32 addresses. + */ + // FIXME? maybe add support for b33 addresses? + private static final Pattern B32_PATTERN = Pattern.compile("^[A-Za-z2-7]{52}\\.b32\\.i2p$"); + private final Screen parent; + private final ServerInfo serverEntry; + private final BooleanConsumer callback; + private TextFieldWidget addressField; + private ButtonWidget selectServerButton; + + public FriendConnectScreen(Screen parent, BooleanConsumer callback, ServerInfo server) { + super(Text.translatable("friendcode.connect")); + this.parent = parent; + this.serverEntry = server; + this.callback = callback; + } + + @Override + public void tick() { + this.addressField.tick(); + } + + @Override + public boolean keyPressed(int keyCode, int scanCode, int modifiers) { + if (this.selectServerButton.active && this.getFocused() == this.addressField && (keyCode == GLFW.GLFW_KEY_ENTER || keyCode == GLFW.GLFW_KEY_KP_ENTER)) { + this.saveAndClose(); + return true; + } + return super.keyPressed(keyCode, scanCode, modifiers); + } + + @Override + protected void init() { + this.addDrawableChild(new ButtonWidget( + this.width / 2 - 100, + 20, + 200, + 20, + this.title, + button -> this.client.setScreen(new DirectConnectScreen(this.parent, this.callback, this.serverEntry)) + )); + this.client.keyboard.setRepeatEvents(true); + this.selectServerButton = this.addDrawableChild(new ButtonWidget(this.width / 2 - 100, this.height / 4 + 96 + 12, 200, 20, Text.translatable("friendcode.select"), button -> this.saveAndClose())); + this.addDrawableChild(new ButtonWidget(this.width / 2 - 100, this.height / 4 + 120 + 12, 200, 20, ScreenTexts.CANCEL, button -> this.callback.accept(false))); + this.addressField = new TextFieldWidget(this.textRenderer, this.width / 2 - 100, 116, 200, 20, ENTER_CODE_TEXT); + this.addressField.setMaxLength(128); + this.addressField.setTextFieldFocused(true); + //this.addressField.setText(this.client.options.lastServer); + this.addressField.setChangedListener(text -> this.onAddressFieldChanged()); + this.addSelectableChild(this.addressField); + this.setInitialFocus(this.addressField); + this.onAddressFieldChanged(); + } + + @Override + public void resize(MinecraftClient client, int width, int height) { + String string = this.addressField.getText(); + this.init(client, width, height); + this.addressField.setText(string); + } + + private void saveAndClose() { + this.client.setScreen(new FriendConnectingScreen(this.serverEntry, this.callback, this.addressField.getText())); +// this.serverEntry.address = this.addressField.getText(); +// this.callback.accept(true); + } + + @Override + public void close() { + this.client.setScreen(this.parent); + } + + @Override + public void removed() { + this.client.keyboard.setRepeatEvents(false); + // FIXME store this.addressField.getText() somewhere. +// this.client.options.lastServer = this.addressField.getText(); +// this.client.options.write(); + } + + private void onAddressFieldChanged() { + this.selectServerButton.active = B32_PATTERN.matcher(this.addressField.getText()).matches(); + } + + @Override + public void render(MatrixStack matrices, int mouseX, int mouseY, float delta) { + this.renderBackground(matrices); + FriendConnectScreen.drawTextWithShadow(matrices, this.textRenderer, ENTER_CODE_TEXT, this.width / 2 - 100, 100, 0xA0A0A0); + this.addressField.render(matrices, mouseX, mouseY, delta); + super.render(matrices, mouseX, mouseY, delta); + } +} diff --git a/src/main/java/ganarchy/friendcode/client/FriendConnectingScreen.java b/src/main/java/ganarchy/friendcode/client/FriendConnectingScreen.java new file mode 100644 index 0000000..748801c --- /dev/null +++ b/src/main/java/ganarchy/friendcode/client/FriendConnectingScreen.java @@ -0,0 +1,110 @@ +package ganarchy.friendcode.client; + +import it.unimi.dsi.fastutil.booleans.BooleanConsumer; +import net.minecraft.client.gui.screen.LoadingDisplay; +import net.minecraft.client.gui.screen.Screen; +import net.minecraft.client.gui.widget.ButtonWidget; +import net.minecraft.client.network.ServerInfo; +import net.minecraft.client.util.math.MatrixStack; +import net.minecraft.screen.ScreenTexts; +import net.minecraft.text.MutableText; +import net.minecraft.text.Text; +import net.minecraft.util.Util; +import org.apache.commons.codec.binary.Base32; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Base64; +import java.util.Locale; + +public class FriendConnectingScreen extends Screen { + private static final Text PATIENCE_TEXT = Text.translatable("friendcode.patience"); + private final ServerInfo serverEntry; + private final BooleanConsumer callback; + private final SamProxyThread proxyThread; + private long startTimeMs; + private Text renderText = null; + + public FriendConnectingScreen(ServerInfo serverEntry, BooleanConsumer callback, String text) { + super(Text.translatable("friendcode.connecting")); + this.serverEntry = serverEntry; + this.callback = callback; + this.startTimeMs = Util.getMeasuringTimeMs(); + this.proxyThread = new SamProxyThread(text); + } + + @Override + protected void init() { + try { + this.proxyThread.start(); + } catch (IllegalThreadStateException ignored) { + } + // cancel friend code. + this.addDrawableChild(new ButtonWidget( + this.width / 2 - 155, + this.height - 28, + 310, + 20, + ScreenTexts.CANCEL, + button -> this.close() + )); + } + + + @Override + public void close() { + super.close(); + if (this.proxyThread.status() != SamProxyThread.Status.RUNNING) { + this.proxyThread.stopProxy(); + } + } + + @Override + public void render(MatrixStack matrices, int mouseX, int mouseY, float delta) { + this.renderBackground(matrices); + FriendConnectingScreen.drawCenteredText(matrices, this.textRenderer, this.title, this.width / 2, 50, 0xFFFFFF); + if (this.renderText != null) { + FriendConnectingScreen.drawCenteredText(matrices, this.textRenderer, this.renderText, this.width / 2, 82, 0xFFFFFF); + } else { + FriendConnectingScreen.drawCenteredText(matrices, this.textRenderer, PATIENCE_TEXT, this.width / 2, 82, 0xFFFFFF); + final String string = LoadingDisplay.get(Util.getMeasuringTimeMs()); + this.client.textRenderer.draw(matrices, string, (float) (this.client.currentScreen.width / 2 - this.client.textRenderer.getWidth(string) / 2), (float) (this.client.currentScreen.height / 2 + this.client.textRenderer.fontHeight), 0x808080); + } + super.render(matrices, mouseX, mouseY, delta); + } + + @Override + public void tick() { + super.tick(); + if (proxyThread.status() != SamProxyThread.Status.IDLE) { + switch (proxyThread.status()) { + case CONNECTION_FAILED -> { + this.renderText = Text.translatable("friendcode.connect.no_i2p"); + break; + } + case SETUP_FAILED -> { + this.renderText = Text.translatable("friendcode.connect.failed"); + break; + } + case RESOLUTION_FAILED -> { + this.renderText = Text.translatable("friendcode.connect.no_such_code"); + break; + } + case RUNNING -> { + this.serverEntry.address = "[::1]:" + this.proxyThread.port(); + this.callback.accept(true); + break; + } + } + } + if (!this.proxyThread.isConnecting()) { + startTimeMs = Util.getMeasuringTimeMs(); + } + if (Util.getMeasuringTimeMs() - startTimeMs > 20 * 1000) { + // keep it from getting stuck. + // FIXME this is wrong. + //this.proxyThread.interrupt(); + } + } +} diff --git a/src/main/java/ganarchy/friendcode/client/I2PSamPinger.java b/src/main/java/ganarchy/friendcode/client/I2PSamPinger.java new file mode 100644 index 0000000..96448db --- /dev/null +++ b/src/main/java/ganarchy/friendcode/client/I2PSamPinger.java @@ -0,0 +1,113 @@ +package ganarchy.friendcode.client; + +import ganarchy.friendcode.FriendCode; +import ganarchy.friendcode.sam.I2PSamControl; +import ganarchy.friendcode.sam.I2PSamStreamForwarder; +import net.minecraft.client.network.LanServerPinger; + +import java.io.*; + +public class I2PSamPinger extends LanServerPinger implements LanSendPing { + private final I2PSamControl sam; + private I2PSamStreamForwarder stream; + private final String port; + private volatile Status status = Status.IDLE; + private boolean hasWarned; + private volatile boolean stopSam; + + public I2PSamPinger(String motd, String port) throws IOException { + super(motd, port); + // cba to mixin + this.port = port; + this.sam = new I2PSamControl(true); + } + + @Override + public void run() { + try (final I2PSamControl sam = this.sam) { + if (!sam.connect()) { + this.status = Status.CONNECTION_FAILED; + // fall back to LAN server. + super.run(); + return; + } + + if (!sam.start()) { + this.status = Status.SETUP_FAILED; + // fall back to LAN server. + super.run(); + return; + } + + try (final I2PSamStreamForwarder stream = this.stream = sam.forwardStream(this.port)) { + if (!stream.connect()) { + this.status = Status.CONNECTION_FAILED; + super.run(); + return; + } + + if (!stream.start()) { + this.status = Status.SETUP_FAILED; + super.run(); + return; + } + + this.status = Status.RUNNING; + super.run(); + } catch (IOException ignored) { + } + } catch (IOException ignored) { + } + } + + @Override + public void friendcodeSendPing() { + if (this.stopSam) { + if (this.stream != null) { + try { + this.stream.close(); + } catch (IOException ignored) { + } + } + try { + this.sam.close(); + } catch (IOException ignored) { + } + this.status = Status.CONNECTION_CLOSED; + this.hasWarned = true; + return; + } + if (this.sam.sendPing() < 0) { + this.status = Status.CONNECTION_CLOSED; + } + if (this.stream != null) { + if (this.stream.sendPing() < 0) { + this.status = Status.CONNECTION_CLOSED; + } + } + if (this.status == Status.CONNECTION_CLOSED && !this.hasWarned) { + this.hasWarned = true; + FriendCode.LOGGER.warn("Friend Code connection closed!"); + } + } + + public Status status() { + return this.status; + } + + public String pubkey() { + return this.sam.pubkey(); + } + + public void stopSam() { + this.stopSam = true; + } + + public enum Status { + IDLE, + RUNNING, + CONNECTION_FAILED, + SETUP_FAILED, + CONNECTION_CLOSED; + } +} diff --git a/src/main/java/ganarchy/friendcode/client/LanSendPing.java b/src/main/java/ganarchy/friendcode/client/LanSendPing.java new file mode 100644 index 0000000..914a8d5 --- /dev/null +++ b/src/main/java/ganarchy/friendcode/client/LanSendPing.java @@ -0,0 +1,6 @@ +package ganarchy.friendcode.client; + +public interface LanSendPing { + + void friendcodeSendPing(); +} diff --git a/src/main/java/ganarchy/friendcode/client/SamProxyThread.java b/src/main/java/ganarchy/friendcode/client/SamProxyThread.java new file mode 100644 index 0000000..c7db41f --- /dev/null +++ b/src/main/java/ganarchy/friendcode/client/SamProxyThread.java @@ -0,0 +1,184 @@ +package ganarchy.friendcode.client; + +import ganarchy.friendcode.FriendCode; +import ganarchy.friendcode.sam.I2PSamControl; +import ganarchy.friendcode.sam.I2PSamStreamConnector; + +import java.io.IOException; +import java.net.Inet6Address; +import java.net.ServerSocket; +import java.net.Socket; + +public class SamProxyThread extends Thread { + private final String target; + private volatile int localPort; + private volatile Status status = Status.IDLE; + private volatile boolean running = true; + private volatile boolean isConnecting = false; + + public SamProxyThread(String target) { + this.target = target; + this.setDaemon(true); + this.setName("SAM Proxy Control Thread"); + } + + @Override + public void run() { + if (this.running) { + tryRun(true); + } + if (this.running) { + tryRun(false); + } + this.status = Status.SETUP_FAILED; + } + + private void tryRun(final boolean zeroHop) { + // this is all very inefficient but we don't particularly care + // who cares if you have 3 threads just to connect to a minecraft server/friend code + // it's only 3 threads + // the "server" is far more efficient, only requiring one + try (final I2PSamControl control = new I2PSamControl(zeroHop)) { + if (!control.connect()) { + this.running = false; + this.status = Status.CONNECTION_FAILED; + return; + } + if (!control.start()) { + this.running = false; + this.status = Status.SETUP_FAILED; + return; + } + switch (control.checkName(this.target)) { + case UNKNOWN, INVALID -> { + this.running = false; + this.status = Status.RESOLUTION_FAILED; + return; + } + case FAILED -> { + return; + } + } + // we can't distinguish if we should retry the connection + // (e.g. because building tunnels took too long, destination is known but gateway/peer is not, etc) + // vs if we should try with 1hop (because we can't reach peer from our connection, + // e.g. ipv4-only client vs ipv6-only friend, symmetric NAT, etc) + // so we just try to "warm up" the connection and hope it works. + I2PSamStreamConnector warmup = control.connectStream(this.target); + if (!warmup.connect()) { + this.running = false; + this.status = Status.CONNECTION_FAILED; + return; + } + warmup.start(); + warmup.close(); + I2PSamStreamConnector stream = control.connectStream(this.target); + if (!stream.connect()) { + this.running = false; + this.status = Status.CONNECTION_FAILED; + return; + } + this.isConnecting = true; + if (!stream.start()) { + FriendCode.LOGGER.warn("[SAM error] {}", stream.getStatus()); + this.isConnecting = false; + if (!zeroHop) { + this.running = false; + this.status = Status.SETUP_FAILED; + } + return; + } + try ( + Socket socket = stream.unwrap(); + ServerSocket server = new ServerSocket(0, 8, Inet6Address.getByAddress("localhost", new byte[]{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}, 0)); + ) { + this.localPort = server.getLocalPort(); + server.setSoTimeout(3000); + this.status = Status.RUNNING; + try (Socket single = server.accept()) { + socket.setTcpNoDelay(true); + single.setTcpNoDelay(true); + Thread clientThread = new Thread("SAM Proxy CTS Thread") { + @Override + public void run() { + try ( + var clientRead = single.getInputStream(); + var serverWrite = socket.getOutputStream(); + ) { + byte[] buffer = new byte[128*1024]; + while (SamProxyThread.this.running) { + int read = clientRead.read(buffer); + if (read > 0) { + serverWrite.write(buffer, 0, read); + } + } + } catch (IOException ignored) { + } + } + }; + Thread serverThread = new Thread("SAM Proxy STC Thread") { + @Override + public void run() { + try ( + var serverRead = socket.getInputStream(); + var clientWrite = single.getOutputStream(); + ) { + byte[] buffer = new byte[128*1024]; + while (SamProxyThread.this.running) { + int read = serverRead.read(buffer); + if (read > 0) { + clientWrite.write(buffer, 0, read); + } + } + } catch (IOException ignored) { + } + } + }; + clientThread.setDaemon(true); + clientThread.start(); + serverThread.setDaemon(true); + serverThread.start(); + while (!this.isInterrupted() && this.running) { + if (control.sendPing() < 0) { + this.running = false; + } + try { + Thread.sleep(1500L); + } catch (InterruptedException ignored) { + } + } + } finally { + this.running = false; + } + } + } catch (IOException e) { + // don't really need to do anything + } + } + + public void stopProxy() { + this.running = false; + // FIXME this is wrong, need to actually close sockets. + this.interrupt(); + } + + public Status status() { + return this.status; + } + + public int port() { + return this.localPort; + } + + public boolean isConnecting() { + return this.isConnecting; + } + + public enum Status { + IDLE, + RUNNING, + CONNECTION_FAILED, + SETUP_FAILED, + RESOLUTION_FAILED; + } +} diff --git a/src/main/java/ganarchy/friendcode/client/WaitingForFriendCodeScreen.java b/src/main/java/ganarchy/friendcode/client/WaitingForFriendCodeScreen.java new file mode 100644 index 0000000..8429a41 --- /dev/null +++ b/src/main/java/ganarchy/friendcode/client/WaitingForFriendCodeScreen.java @@ -0,0 +1,93 @@ +package ganarchy.friendcode.client; + +import net.minecraft.client.gui.screen.LoadingDisplay; +import net.minecraft.client.gui.screen.Screen; +import net.minecraft.client.gui.widget.ButtonWidget; +import net.minecraft.client.util.math.MatrixStack; +import net.minecraft.screen.ScreenTexts; +import net.minecraft.text.MutableText; +import net.minecraft.text.Text; +import net.minecraft.util.Util; +import org.apache.commons.codec.binary.Base32; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Base64; +import java.util.Locale; + +public class WaitingForFriendCodeScreen extends Screen { + private final I2PSamPinger samPinger; + + public WaitingForFriendCodeScreen(I2PSamPinger samPinger) { + super(Text.translatable("friendcode.opening")); + this.samPinger = samPinger; + } + + @Override + protected void init() { + // cancel friend code. + this.addDrawableChild(new ButtonWidget( + this.width / 2 - 155, + this.height - 28, + 310, + 20, + ScreenTexts.CANCEL, + button -> this.close() + )); + } + + @Override + public void render(MatrixStack matrices, int mouseX, int mouseY, float delta) { + this.renderBackground(matrices); + WaitingForFriendCodeScreen.drawCenteredText(matrices, this.textRenderer, this.title, this.width / 2, 50, 0xFFFFFF); + final String string = LoadingDisplay.get(Util.getMeasuringTimeMs()); + this.client.textRenderer.draw(matrices, string, (float)(this.client.currentScreen.width / 2 - this.client.textRenderer.getWidth(string) / 2), (float)(this.client.currentScreen.height / 2 + this.client.textRenderer.fontHeight), 0x808080); + super.render(matrices, mouseX, mouseY, delta); + } + + @Override + public void close() { + super.close(); + this.samPinger.stopSam(); + final MutableText text = Text.translatable("friendcode.publish.stopped"); + this.client.inGameHud.getChatHud().addMessage(text); + } + + @Override + public void tick() { + super.tick(); + if (samPinger.status() != I2PSamPinger.Status.IDLE) { + this.client.setScreen(null); + switch (samPinger.status()) { + case CONNECTION_FAILED -> { + final MutableText text = Text.translatable("friendcode.publish.no_i2p"); + this.client.inGameHud.getChatHud().addMessage(text); + break; + } + case SETUP_FAILED, CONNECTION_CLOSED -> { + final MutableText text = Text.translatable("friendcode.publish.failed"); + this.client.inGameHud.getChatHud().addMessage(text); + break; + } + case RUNNING -> { + // convert from I2P base64 to URL-safe base64 so we don't have to write our own Base64 decoder + final String pubkey_b64 = samPinger.pubkey().replace('~', '_'); + final byte[] pubkey = Base64.getUrlDecoder().decode(pubkey_b64.getBytes(StandardCharsets.UTF_8)); + // a .b32.i2p is just a Base32-encoded SHA256 + final String b32; + try { + b32 = new Base32().encodeAsString(MessageDigest.getInstance("SHA-256").digest(pubkey)); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + // but we wanna massage it a little + final String b32_i2p = b32.replaceFirst("=+$", "").toLowerCase(Locale.ROOT) + ".b32.i2p"; + final MutableText text = Text.translatable("friendcode.publish.started", b32_i2p); + this.client.inGameHud.getChatHud().addMessage(text); + break; + } + } + } + } +} |