p)`d1coES}#v0l^nZUC0eu;D4cz+A>p9E%F9AvM@j26UoSWDSWn=9yOYKkroR%
zZq&v|ONFkU$LB-E-ExCoXG|N!qui%Z5AwXCHI#MvhWbGq8|(|&Ot(0w7kJNcBbxW-
z;x`7-Cu3=o1v;Gd%N31?x8;7P4s9#QklEHC(Rs+PE*s(NH_FNJf%LOoCRkeOEQr~r@AMS2A|+H)(s_!95m3@N1hcI*zeJLtUxVCZZzO<3
z(Q~ox_TTXY{0vcUN>%2nicbUdrtbw5hdM=~Fe$RfCKo>k_1*`|a0e!3B3cks?eT05
zF6~tFIVG0FI+6g>-lO>Ik2^jep+3EfL?h;AGM^iAnyKOwvNNTErq
zJ9KEKmcLzGq17BF-h+I(o0+ooWU`&l`IFHdTM^Lfj*F^|*gxN#L8X1Opx)OsIgo>G
zep+dAI+ZqAhwDhm6bEiy;?e1;Y&o{pt%k7$eu@!Q{My(w?Kf;#o9)P8B!Fn8m_okJ
zQ1ooQU6Qg+Sbe|j!LFSpgat2`*Lkk?hfarVK83B%2&n^Ny)U=gw!y&PuBt=MNoua#
zv4Uk#MC^->1$v_U-pJJDF8<|bgg^AMS*dvql=nXO*|{r9+3EdD$z)6R-zzJgHjSf|
z4i1({&0zc8YrVt%Q{6u#+EC#t4B8L?)`s9*@hl!_P)X{ptquE&7|2`*|*Dw$V**-7|34$pK_Ta%@1fvd)%HWI6tECIy12!jSo
z1jfDzvJ5DH5IC}wJ`Z>~iee|%yzfrxh>FN*cxvz`q{Sw?c)!AvX1~X&Fn3xf1PSPC
zSt8#A4oq~vS%C^Y*%$~qVD5#IrUDMg>NQGnFeK6T(Ssfaao$-@b^Zb}6J&OZT1?7y
z()IesZ*Qdhlgb)A2@uDV9IqRp9m%Gkurm2BW{7C9Z$q>Nn_(6>y-Y_$&@p5U{L3kk
zsYw_Efrq~g+FVH1QM?1yVKX5Es=vwB>Hww#>@wn#L@~8CDonWGyYLl5J$pZXkCFXp
za=279PKQd6*Rd3K-cCF?;EVV7ZlfrkH0|cJh|hs^`(`4x>mPvyL)Gz*=}|+BVI`R_
zN{%+l5E3*s_0=^tbf2;5CS1ia0;XH-+qEi!Ap)gjwByK69(#%Hp@PiZMyS4;CRf9+
z0mt*Iu*?8Fd|i;>A|US)+e~Bvy0T|7f*!;}0ojP18(3xGft(W4rBjvQyA>pi~lSAi)VAjSO8L7vN|{5?z%VUY5~Gdo;7^C4X2Y;
z{{!h%Ardm<(*Y+O}`!Ap(wq(o%DAXSY{m=oy
zu=o>tDdjf0c~mfI@33Xu=o-lZz^RE=1>4JXdu@=e(@@FCa=+
zkDL#POY}3jI51)`Y&a87aX?>x6EtoCg2A-X;Q}n-sb4}cJ7&PC4%o%c81TzNh4aY-
xZs4M4Z0gi;7`;V2If33F*3mbRKTRS7Fro47uA2_45d8$uyJ2*_Li1MSe*pMJvGf1{
diff --git a/src/main/resources/assets/coffeeproxy/logo.png b/src/main/resources/assets/coffeeproxy/logo.png
new file mode 100644
index 0000000000000000000000000000000000000000..72c1b2323c2b85dda7eb34a2050d7f0b2191744f
GIT binary patch
literal 3201
zcmZ8jcQo8v*Ph>)xkj`hdc@Uo34$Ssh&IY#+z>$!@v6}~qf7Lj=)FaW-i;b{h#Ev6
zqKn>x=;Rysz3)HYUTdG{+2!ng);epSbwX8D6v%He-2#C?4%TDG>f+S=OG)zy7{
zeRp?v38&)W;nCC6d-dv-nwpw}gTvU^*xugWmoH!D=H`lvi&Iike*XN~*VotH-oC%T
z-`UxDa&nTBlVfjhKR7sOYirBR&8?}a85$aDXJ^;n-yaYVpsK3M!NIY;z3uGm?CgtNe+0&l!^3N9YjbmRzkT~=W@eU=
zkufnb!NS6VLZL7iOm}y;q@*Mb4UM(6wXw1B`}gmcmzRxGv
z-@bioZf>rxum9=Or=6Xh@87=*3JU7z=nM}JfBg6nfk3=|{kp8IY<+#5fq`LQU;vB7
zHZ(LuM@KI$Efp3PzIpS;#KeS^m9?_6vZkiy=;&x>X6DzgUs6(1U0q#2e*7338ah2a
zZES2DA0N-k${HCNdH($Q!oq@>n3%Aza8y)OM@Prz<|Y#plcJ&`FE8)>{5%?sj*X2a
zBO?Nysiq6pZi}M+Zk{qU5s+B^*Er2iN6?YYbxww-8J)}UZBY{J
zYW1O*r|Isy`Sgpc-K|v5)&`$H$eA9R4`nXCCT`y^P;BVtd2&h=sqQ5Kdx1Bn!f8!w
zOWY!{qQ?J)@TY=v=9l~(2%9}8w?OpQ3rTP|8EYQrt#h@I9Epae)`?^J@fBnAU8Q+@
zUaof(=Z^Pm2I&(%D_VF6&sI9&Ph4MPN^-MbMr*RYx&M#{pFK_S>%1f}}aO#F77H
z4T`-lgI7RG_YW%C-49bN0{!2#Ojpdb&N!zHTd^L>h;hiwob*S@KdwL=0XSMJCEUK@XZeC&0u=y7)cJ+
z!L{O}!PPxYkKXUJR15~{U->nMq+tx%?*fcr%x;nhUfxO!gP1^#Ta_jL<6}N5hUkWA
z3l<2CFgP&hrLtxw*WuC1XhPkg7P3w0uAosst;-04A^Mp=thGA$_PR$fRe@yI^3_?r
zSKY9y3HWxk?>#w)Ib7a4U-QWZ)lm3;cBGJoL^R02`tWioaW6hiL`ln~)%90=wgH3T
z(>{`>=Zrw{53NMW(mQ0Sc6TDYW|DW)_aENQ
zEXnC`cb>lm13}eCg`N3chzLt;84;B>`-&}%iQ0C1T*>3twywm|?ufOBPUx(<
zF+i?Ve#}g)dA%n)-%|Sv%ey4V3G0)Df*-i#lyiGqj_x^qxRW%+=2}j!wYnsexkND?
zGIzC2*@<*ppgS7nP;^QKtV<(_0HEU#4r$Z1&*36lhcf6jYw;jy1QTfJ!G|s;kgyJQ
z4|n-xMK9(rfgl_wWeAM@&Z&4p&-;U&%QH6&Ae|`im<%~-n^re)-1`I$GEL*ti}9N0
zX=DX6n9!|1h#&=SW1H6OV?(Kcp`9tr;w^2?H>Vh87#sryyoJ9=U8BEkJLfK=1OJow
zDp>c=Fd~2|0%UI}=CS_q5D9D&vl3fhg-Rih3-E(F@woh
z7I?c~kN<%)=<@%r>opR%FML$~6c*6d7@IMY)og6FaXs~PJhd(AQGa$a+X9`9#+#LD
z$mF_0vhYu*8WMPjr$X`x+tA4dB6=H7G%*5!hXYG9lGyJM?Xw-yZ~*77d)fm0FHjd39jcvs#fzEV1&pfwUF4(NQAT-#S6{<}yPDIMcl
zoX*J3-Cw36v{Tiaf@`atA}$gE!5oFl@w=x@h~V6AnSCfwD6klgGz#kIo*UehrIxj@
zDRZK0*qSQ$t}?^tf5jHU{BWK=S}eia#BusChkP1(_lVp6{#&+S>FBx4>6$6Qi9X)~$SrJ8ajO)EZl|IBjeVhls!R>wx67^+4@
z$6aB@IkHG0vl6}-vGZUc6ya!>qP$5N*uW*I@+Y^|$0$mllBcl2y6s=pV}I_YMwD^j
zbn|24f+QQmhgvgUT1Coa9!`NeR_4|Vx!cB#Rh%EujHOoXl7R=k=f!fU0ljU-TL5*n_9!#n-b6{SUBVD`Br_UI8?CRZD5Uyww)YeyhnFp$^KLJw9ina%DShyo*n
z1~?GmI1^v167Xy89ButS0aO
zEU~ci{q+_#_q2~fNsxkGoNatt_-}Mw);BYN9q1JCK_?24ZFMK*v-0C6^Rq8^b=hY>F#|xe_1L?
z4EQC_gy3e~?0_Ni16h>0-4Dx}ox>vh0YW?JKEC!ug`&|!hqVhQ&Sg&umV21-GR3pv
z?W)eg_2B7t1f%cZ#kTFFQ7j4EpgrU1zUr?z{eXveVQ9)?dab*EJ8Yf$WO}#sj53S%
zd`10~DDIS5erk7VdC9=Nm6acwZ>{&gmvGUW_&3zyIPcb%D0u(P7Y2<|kt>ri@cTcR
C0n?BG
literal 0
HcmV?d00001
diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json
index 2e6e63b..8183a95 100644
--- a/src/main/resources/fabric.mod.json
+++ b/src/main/resources/fabric.mod.json
@@ -10,8 +10,8 @@
"contact": {
"sources": "https://github.com/axlecoffee/coffeeproxy"
},
- "license": "MIT",
- "icon": "assets/coffeeproxy/icon.png",
+ "license": "AGPL-3.0-or-later",
+ "icon": "assets/coffeeproxy/logo.png",
"environment": "*",
"entrypoints": {
"main": [
From 53910fbf43a21e950754f1dbf876554b1fd2c783 Mon Sep 17 00:00:00 2001
From: axlecoffee <95949634+axlecoffee@users.noreply.github.com>
Date: Thu, 8 Oct 2026 05:11:18 +0000
Subject: [PATCH 4/9] Major CoffeeProxy rewrite + 26.3 code, credits to
EricBakreChef for the suggestions! makes this wayyy more usable and such
---
.../java/coffee/axle/proxy/Coffeeproxy.java | 18 +-
.../proxy/CoffeeproxyModMenuIntegration.java | 5 +-
src/main/java/coffee/axle/proxy/Config.java | 47 ++-
src/main/java/coffee/axle/proxy/GuiProxy.java | 159 ++++-----
.../java/coffee/axle/proxy/GuiProxyList.java | 326 ++++++++++++++++++
src/main/java/coffee/axle/proxy/Proxy.java | 72 +++-
src/main/java/coffee/axle/proxy/TestPing.java | 110 +++---
.../proxy/mixin/ClientConnectionInit.java | 34 +-
.../proxy/mixin/MultiplayerScreenOpen.java | 52 +--
.../assets/coffeeproxy/lang/en_us.json | 14 +-
10 files changed, 595 insertions(+), 242 deletions(-)
create mode 100644 src/main/java/coffee/axle/proxy/GuiProxyList.java
diff --git a/src/main/java/coffee/axle/proxy/Coffeeproxy.java b/src/main/java/coffee/axle/proxy/Coffeeproxy.java
index dad6b91..b29825e 100644
--- a/src/main/java/coffee/axle/proxy/Coffeeproxy.java
+++ b/src/main/java/coffee/axle/proxy/Coffeeproxy.java
@@ -1,21 +1,23 @@
+// SPDX-FileCopyrightText: 2026 Axle Duggan (axlecoffee)
+//
+// SPDX-License-Identifier: AGPL-3.0-or-later
package coffee.axle.proxy;
import net.fabricmc.api.ModInitializer;
-import net.minecraft.client.gui.components.Button;
+import net.minecraft.client.Minecraft;
+import net.minecraft.client.gui.screens.Screen;
public class Coffeeproxy implements ModInitializer {
public static boolean proxyEnabled = false;
public static Proxy proxy = new Proxy();
- public static Proxy lastUsedProxy = new Proxy();
- public static Button proxyMenuButton;
- public static boolean suppressProxyMixin = false;
-
- public static String getLastUsedProxyIp() {
- return lastUsedProxy.ipPort.isEmpty() ? "none" : lastUsedProxy.getIp();
- }
@Override
public void onInitialize() {
Config.loadConfig();
+ Config.setActive(Config.activeName);
+ }
+
+ public static void openScreen(Screen screen) {
+ Minecraft.getInstance().setScreen(screen);
}
}
\ No newline at end of file
diff --git a/src/main/java/coffee/axle/proxy/CoffeeproxyModMenuIntegration.java b/src/main/java/coffee/axle/proxy/CoffeeproxyModMenuIntegration.java
index f36e053..ba0b863 100644
--- a/src/main/java/coffee/axle/proxy/CoffeeproxyModMenuIntegration.java
+++ b/src/main/java/coffee/axle/proxy/CoffeeproxyModMenuIntegration.java
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: 2026 Axle Duggan (axlecoffee)
+//
+// SPDX-License-Identifier: AGPL-3.0-or-later
package coffee.axle.proxy;
import com.terraformersmc.modmenu.api.ConfigScreenFactory;
@@ -6,6 +9,6 @@
public class CoffeeproxyModMenuIntegration implements ModMenuApi {
@Override
public ConfigScreenFactory> getModConfigScreenFactory() {
- return GuiProxy::new;
+ return GuiProxyList::new;
}
}
diff --git a/src/main/java/coffee/axle/proxy/Config.java b/src/main/java/coffee/axle/proxy/Config.java
index 4cbffa6..ddab7fe 100644
--- a/src/main/java/coffee/axle/proxy/Config.java
+++ b/src/main/java/coffee/axle/proxy/Config.java
@@ -1,43 +1,45 @@
+// SPDX-FileCopyrightText: 2026 Axle Duggan (axlecoffee)
+//
+// SPDX-License-Identifier: AGPL-3.0-or-later
package coffee.axle.proxy;
import com.google.gson.*;
-import com.google.gson.reflect.TypeToken;
import net.minecraft.client.Minecraft;
import org.apache.commons.io.FileUtils;
import java.io.File;
import java.io.IOException;
-import java.lang.reflect.Type;
import java.nio.charset.StandardCharsets;
-import java.util.HashMap;
+import java.util.LinkedHashMap;
public class Config {
private static final String CONFIG_PATH = Minecraft.getInstance().gameDirectory
+ "/config/CoffeeProxy.json";
- public static HashMap accounts = new HashMap<>();
- public static String lastPlayerName = "";
+ public static LinkedHashMap proxies = new LinkedHashMap<>();
+ public static String activeName = "";
public static boolean showMultiplayerButton = true;
public static void loadConfig() {
File configFile = new File(CONFIG_PATH);
try {
if (!configFile.exists()) {
- if (!configFile.createNewFile()) {
- System.out.println("Error creating CoffeeProxy.json file");
- }
+ // assume that if the config file doenst exist its prob first launch/whatever so just ... recreate it... you little weasle
+ FileUtils.touch(configFile);
return;
}
String configString = FileUtils.readFileToString(configFile, "UTF-8");
if (!configString.isEmpty()) {
JsonObject configJson = JsonParser.parseString(configString).getAsJsonObject();
- Coffeeproxy.proxyEnabled = configJson.get("proxy-enabled").getAsBoolean();
JsonElement showBtn = configJson.get("show-multiplayer-button");
Config.showMultiplayerButton = showBtn == null || showBtn.getAsBoolean();
- Type type = new TypeToken>() {
- }.getType();
- accounts = new Gson().fromJson(configJson.get("accounts"), type);
- if (accounts == null) {
- accounts = new HashMap<>();
+ JsonElement active = configJson.get("active");
+ Config.activeName = active == null ? "" : active.getAsString();
+ proxies = new LinkedHashMap<>();
+ JsonElement entries = configJson.get("proxies");
+ if (entries != null) {
+ for (var entry : entries.getAsJsonObject().entrySet()) {
+ proxies.put(entry.getKey(), entry.getValue().getAsString());
+ }
}
}
} catch (Exception e) {
@@ -46,18 +48,25 @@ public static void loadConfig() {
}
}
- public static void setDefaultProxy(Proxy proxy) {
- accounts.put("", proxy);
+ public static void setActive(String name) {
+ activeName = name == null ? "" : name;
+ String entry = proxies.get(activeName);
+ if (entry == null) {
+ Coffeeproxy.proxy = new Proxy();
+ Coffeeproxy.proxyEnabled = false;
+ } else {
+ Coffeeproxy.proxy = Proxy.parse(entry);
+ Coffeeproxy.proxyEnabled = true;
+ }
}
public static void saveConfig() {
try {
- JsonElement accountsJsonObject = new Gson().toJsonTree(accounts);
JsonObject configJson = new JsonObject();
- configJson.addProperty("proxy-enabled", Coffeeproxy.proxyEnabled);
configJson.addProperty("show-multiplayer-button", showMultiplayerButton);
- configJson.add("accounts", accountsJsonObject);
+ configJson.addProperty("active", activeName);
Gson gsonPretty = new GsonBuilder().setPrettyPrinting().create();
+ configJson.add("proxies", gsonPretty.toJsonTree(proxies));
FileUtils.write(new File(CONFIG_PATH), gsonPretty.toJson(configJson), StandardCharsets.UTF_8);
} catch (IOException e) {
System.out.println("Error writing CoffeeProxy.json file");
diff --git a/src/main/java/coffee/axle/proxy/GuiProxy.java b/src/main/java/coffee/axle/proxy/GuiProxy.java
index fd59487..3080acf 100644
--- a/src/main/java/coffee/axle/proxy/GuiProxy.java
+++ b/src/main/java/coffee/axle/proxy/GuiProxy.java
@@ -1,36 +1,29 @@
+// SPDX-FileCopyrightText: 2026 Axle Duggan (axlecoffee)
+//
+// SPDX-License-Identifier: AGPL-3.0-or-later
package coffee.axle.proxy;
-import net.minecraft.client.Minecraft;
-//? if <26 {
import net.minecraft.client.gui.GuiGraphics;
-//?} else {
-/*import net.minecraft.client.gui.GuiGraphicsExtractor;
-*///?}
import net.minecraft.client.gui.screens.Screen;
-import net.minecraft.client.gui.screens.TitleScreen;
-import net.minecraft.client.gui.screens.multiplayer.JoinMultiplayerScreen;
import net.minecraft.client.gui.components.Button;
-import net.minecraft.client.gui.components.Checkbox;
import net.minecraft.client.gui.components.EditBox;
//? if >=1.21.10 {
-import net.minecraft.client.input.InputWithModifiers;
import net.minecraft.client.input.KeyEvent;
//?}
import net.minecraft.network.chat.Component;
import net.minecraft.ChatFormatting;
-import net.fabricmc.loader.api.FabricLoader;
import org.apache.commons.lang3.StringUtils;
public class GuiProxy extends Screen {
private Proxy.ProxyType currentType = Proxy.ProxyType.SOCKS5;
+ private EditBox name;
private EditBox ipPort;
private EditBox username;
private EditBox password;
- private Checkbox enabledCheck;
- private Checkbox showMultiplayerCheck;
private final Screen parentScreen;
+ private final String editName;
private String msg = "";
@@ -41,9 +34,10 @@ public class GuiProxy extends Screen {
private static final String TEXT_PROXY = Component.translatable("ui.coffeeproxy.options.proxy").getString();
- public GuiProxy(Screen parentScreen) {
+ public GuiProxy(Screen parentScreen, String editName) {
super(Component.literal(TEXT_PROXY));
this.parentScreen = parentScreen;
+ this.editName = editName;
}
private static boolean isValidIpPort(String ipP) {
@@ -97,68 +91,66 @@ public boolean keyPressed(KeyEvent keyEvent) {
public void render(GuiGraphics guiGraphics, int mouseX, int mouseY, float partialTicks) {
super.render(guiGraphics, mouseX, mouseY, partialTicks);
//?} else {
- /*public void extractRenderState(GuiGraphicsExtractor guiGraphics, int mouseX, int mouseY, float partialTicks) {
+ /*public void extractRenderState(GuiGraphics guiGraphics, int mouseX, int mouseY, float partialTicks) {
super.extractRenderState(guiGraphics, mouseX, mouseY, partialTicks);
*///?}
- if (enabledCheck.selected() && !isValidIpPort(ipPort.getValue())) {
- //? if >=1.21.10 {
- enabledCheck.onPress((InputWithModifiers) new KeyEvent(0, 0, 0));
- //?} else {
- /*enabledCheck.onPress();
- *///?}
- }
-
//? if <26 {
- guiGraphics.drawString(this.font, Component.translatable("ui.coffeeproxy.options.proxyType").getString(),
+ guiGraphics.drawString(this.font, Component.translatable("ui.coffeeproxy.options.name").getString(),
positionX, positionY[1] - 10, 0xFFA0A0A0);
+ guiGraphics.drawString(this.font, Component.translatable("ui.coffeeproxy.options.proxyType").getString(),
+ positionX, positionY[2] - 10, 0xFFA0A0A0);
guiGraphics.drawCenteredString(this.font,
- Component.translatable("ui.coffeeproxy.options.auth").getString(), this.width / 2, positionY[3] + 8,
+ Component.translatable("ui.coffeeproxy.options.auth").getString(), this.width / 2, positionY[4] + 8,
0xFFFFFFFF);
guiGraphics.drawString(this.font, Component.translatable("ui.coffeeproxy.options.ipPort").getString(),
- positionX, positionY[2] - 10, 0xFFA0A0A0);
+ positionX, positionY[3] - 10, 0xFFA0A0A0);
+ this.name.render(guiGraphics, mouseX, mouseY, partialTicks);
this.ipPort.render(guiGraphics, mouseX, mouseY, partialTicks);
if (currentType == Proxy.ProxyType.SOCKS4) {
guiGraphics.drawString(this.font, Component.translatable("ui.coffeeproxy.auth.id").getString(),
- positionX, positionY[4] - 10, 0xFFA0A0A0);
+ positionX, positionY[5] - 10, 0xFFA0A0A0);
this.username.render(guiGraphics, mouseX, mouseY, partialTicks);
} else {
guiGraphics.drawString(this.font, Component.translatable("ui.coffeeproxy.auth.password").getString(),
- positionX, positionY[5] - 10, 0xFFA0A0A0);
+ positionX, positionY[6] - 10, 0xFFA0A0A0);
guiGraphics.drawString(this.font, Component.translatable("ui.coffeeproxy.auth.username").getString(),
- positionX, positionY[4] - 10, 0xFFA0A0A0);
+ positionX, positionY[5] - 10, 0xFFA0A0A0);
this.username.render(guiGraphics, mouseX, mouseY, partialTicks);
this.password.render(guiGraphics, mouseX, mouseY, partialTicks);
}
guiGraphics.drawCenteredString(this.font, !msg.isEmpty() ? msg : testPing.state, this.width / 2,
- positionY[6] + 5, 0xFFA0A0A0);
+ positionY[7] + 5, 0xFFA0A0A0);
//?} else {
- /*guiGraphics.text(this.font, Component.translatable("ui.coffeeproxy.options.proxyType").getString(),
+ /*guiGraphics.text(this.font, Component.translatable("ui.coffeeproxy.options.name").getString(),
positionX, positionY[1] - 10, 0xFFA0A0A0);
+ guiGraphics.text(this.font, Component.translatable("ui.coffeeproxy.options.proxyType").getString(),
+ positionX, positionY[2] - 10, 0xFFA0A0A0);
guiGraphics.centeredText(this.font,
- Component.translatable("ui.coffeeproxy.options.auth").getString(), this.width / 2, positionY[3] + 8,
+ Component.translatable("ui.coffeeproxy.options.auth").getString(), this.width / 2, positionY[4] + 8,
0xFFFFFFFF);
guiGraphics.text(this.font, Component.translatable("ui.coffeeproxy.options.ipPort").getString(),
- positionX, positionY[2] - 10, 0xFFA0A0A0);
+ positionX, positionY[3] - 10, 0xFFA0A0A0);
+ this.name.extractWidgetRenderState(guiGraphics, mouseX, mouseY, partialTicks);
this.ipPort.extractWidgetRenderState(guiGraphics, mouseX, mouseY, partialTicks);
if (currentType == Proxy.ProxyType.SOCKS4) {
guiGraphics.text(this.font, Component.translatable("ui.coffeeproxy.auth.id").getString(),
- positionX, positionY[4] - 10, 0xFFA0A0A0);
+ positionX, positionY[5] - 10, 0xFFA0A0A0);
this.username.extractWidgetRenderState(guiGraphics, mouseX, mouseY, partialTicks);
} else {
guiGraphics.text(this.font, Component.translatable("ui.coffeeproxy.auth.password").getString(),
- positionX, positionY[5] - 10, 0xFFA0A0A0);
+ positionX, positionY[6] - 10, 0xFFA0A0A0);
guiGraphics.text(this.font, Component.translatable("ui.coffeeproxy.auth.username").getString(),
- positionX, positionY[4] - 10, 0xFFA0A0A0);
+ positionX, positionY[5] - 10, 0xFFA0A0A0);
this.username.extractWidgetRenderState(guiGraphics, mouseX, mouseY, partialTicks);
this.password.extractWidgetRenderState(guiGraphics, mouseX, mouseY, partialTicks);
}
guiGraphics.centeredText(this.font, !msg.isEmpty() ? msg : testPing.state, this.width / 2,
- positionY[6] + 5, 0xFFA0A0A0);
+ positionY[7] + 5, 0xFFA0A0A0);
*///?}
}
@@ -172,34 +164,44 @@ public void init() {
int buttonLength = 160;
centerButtons(11, buttonLength, 32);
- String savedIpPort = this.ipPort != null ? this.ipPort.getValue() : Coffeeproxy.proxy.ipPort;
- String savedUsername = this.username != null ? this.username.getValue() : Coffeeproxy.proxy.username;
- String savedPassword = this.password != null ? this.password.getValue() : Coffeeproxy.proxy.password;
+ String entry = editName == null ? "" : Config.proxies.getOrDefault(editName, "");
+ Proxy saved = entry.isEmpty() ? new Proxy() : Proxy.parse(entry);
+
+ String savedName = this.name != null ? this.name.getValue() : (editName == null ? "" : editName);
+ String savedIpPort = this.ipPort != null ? this.ipPort.getValue() : saved.ipPort;
+ String savedUsername = this.username != null ? this.username.getValue() : saved.username;
+ String savedPassword = this.password != null ? this.password.getValue() : saved.password;
if (this.ipPort == null) {
- currentType = Coffeeproxy.proxy.type;
+ currentType = saved.type;
}
+ this.name = new EditBox(this.font, positionX, positionY[1], buttonLength, 20,
+ Component.literal(""));
+ this.name.setValue(savedName);
+ this.name.setMaxLength(64);
+ this.name.setFocused(true);
+ this.addWidget(this.name);
+
Button proxyType = Button.builder(Component.literal(currentType.name()), button -> {
Proxy.ProxyType[] values = Proxy.ProxyType.values();
currentType = values[(currentType.ordinal() + 1) % values.length];
button.setMessage(Component.literal(currentType.name()));
- }).bounds(positionX, positionY[1], buttonLength, 20).build();
+ }).bounds(positionX, positionY[2], buttonLength, 20).build();
this.addRenderableWidget(proxyType);
- this.ipPort = new EditBox(this.font, positionX, positionY[2], buttonLength, 20,
+ this.ipPort = new EditBox(this.font, positionX, positionY[3], buttonLength, 20,
Component.literal(""));
this.ipPort.setValue(savedIpPort);
this.ipPort.setMaxLength(1024);
- this.ipPort.setFocused(true);
this.addWidget(this.ipPort);
- this.username = new EditBox(this.font, positionX, positionY[4], buttonLength, 20,
+ this.username = new EditBox(this.font, positionX, positionY[5], buttonLength, 20,
Component.literal(""));
this.username.setMaxLength(255);
this.username.setValue(savedUsername);
this.addWidget(this.username);
- this.password = new EditBox(this.font, positionX, positionY[5], buttonLength, 20,
+ this.password = new EditBox(this.font, positionX, positionY[6], buttonLength, 20,
Component.literal(""));
this.password.setMaxLength(255);
this.password.setValue(savedPassword);
@@ -207,23 +209,29 @@ public void init() {
int posXButtons = (this.width / 2) - (((buttonLength / 2) * 3) / 2);
- Button apply = Button.builder(Component.translatable("ui.coffeeproxy.options.apply"), button -> {
- Config.showMultiplayerButton = showMultiplayerCheck.selected();
- if (!ipPort.getValue().isEmpty() && !checkProxy()) {
- Config.saveConfig();
+ Button apply = Button.builder(Component.translatable("ui.coffeeproxy.options.save"), button -> {
+ String entryName = name.getValue().trim();
+ if (entryName.isEmpty()) {
+ msg = ChatFormatting.RED + Component.translatable("ui.coffeeproxy.err.specName").getString();
+ this.name.setFocused(true);
return;
}
- if (!ipPort.getValue().isEmpty()) {
- Coffeeproxy.proxy = new Proxy(currentType, ipPort.getValue(), username.getValue(), password.getValue());
- Coffeeproxy.proxyEnabled = enabledCheck.selected();
- Config.setDefaultProxy(Coffeeproxy.proxy);
+ if (!checkProxy()) {
+ return;
+ }
+ Proxy edited = new Proxy(currentType, ipPort.getValue(), username.getValue(), password.getValue());
+ if (editName != null && !editName.equals(entryName)) {
+ Config.proxies.remove(editName);
+ if (Config.activeName.equals(editName)) {
+ Config.activeName = entryName;
+ }
+ }
+ Config.proxies.put(entryName, edited.format());
+ if (entryName.equals(Config.activeName)) {
+ Config.setActive(entryName);
}
Config.saveConfig();
- //? if <26.2 {
- Minecraft.getInstance().setScreen(new JoinMultiplayerScreen(new TitleScreen()));
- //?} else {
- /*Minecraft.getInstance().gui.setScreen(new JoinMultiplayerScreen(new TitleScreen()));
- *///?}
+ Coffeeproxy.openScreen(parentScreen);
}).bounds(posXButtons, positionY[9], buttonLength / 2 - 3, 20).build();
this.addRenderableWidget(apply);
@@ -234,45 +242,14 @@ public void init() {
}
if (checkProxy()) {
testPing = new TestPing();
- testPing.run("mc.hypixel.net", 25565,
+ testPing.run("anticheat-test.com", 25565,
new Proxy(currentType, ipPort.getValue(), username.getValue(), password.getValue()));
}
}).bounds(posXButtons + buttonLength / 2 + 3, positionY[9], buttonLength / 2 - 3, 20).build();
this.addRenderableWidget(test);
- Checkbox.Builder checkboxBuilder = Checkbox
- .builder(Component.translatable("ui.coffeeproxy.options.proxyEnabled"), this.font);
- checkboxBuilder.pos(
- (this.width / 2)
- - (15 + font.width(Component.translatable("ui.coffeeproxy.options.proxyEnabled"))) / 2,
- positionY[7]);
- boolean shouldBeChecked = this.enabledCheck != null ? this.enabledCheck.selected() : Coffeeproxy.proxyEnabled;
- if (shouldBeChecked) {
- checkboxBuilder.selected(true);
- }
- this.enabledCheck = checkboxBuilder.build();
- this.addRenderableWidget(this.enabledCheck);
-
- Checkbox.Builder showMultiplayerBuilder = Checkbox
- .builder(Component.translatable("ui.coffeeproxy.options.showMultiplayerButton"), this.font);
- showMultiplayerBuilder.pos(
- (this.width / 2)
- - (15 + font.width(Component.translatable("ui.coffeeproxy.options.showMultiplayerButton"))) / 2,
- positionY[8]);
- boolean shouldShowMultiplayer = this.showMultiplayerCheck != null ? this.showMultiplayerCheck.selected() : Config.showMultiplayerButton;
- if (shouldShowMultiplayer) {
- showMultiplayerBuilder.selected(true);
- }
- this.showMultiplayerCheck = showMultiplayerBuilder.build();
- this.showMultiplayerCheck.active = FabricLoader.getInstance().isModLoaded("modmenu");
- this.addRenderableWidget(this.showMultiplayerCheck);
-
Button cancel = Button.builder(Component.translatable("ui.coffeeproxy.options.cancel"), (button) -> {
- //? if <26.2 {
- Minecraft.getInstance().setScreen(parentScreen);
- //?} else {
- /*Minecraft.getInstance().gui.setScreen(parentScreen);
- *///?}
+ Coffeeproxy.openScreen(parentScreen);
}).bounds(posXButtons + (buttonLength / 2 + 3) * 2, positionY[9], buttonLength / 2 - 3, 20).build();
this.addRenderableWidget(cancel);
}
diff --git a/src/main/java/coffee/axle/proxy/GuiProxyList.java b/src/main/java/coffee/axle/proxy/GuiProxyList.java
new file mode 100644
index 0000000..bd1e178
--- /dev/null
+++ b/src/main/java/coffee/axle/proxy/GuiProxyList.java
@@ -0,0 +1,326 @@
+// SPDX-FileCopyrightText: 2026 Axle Duggan (axlecoffee)
+//
+// SPDX-License-Identifier: AGPL-3.0-or-later
+package coffee.axle.proxy;
+
+import net.minecraft.client.Minecraft;
+import net.minecraft.client.gui.GuiGraphics;
+import net.minecraft.client.gui.screens.Screen;
+import net.minecraft.client.gui.components.Button;
+import net.minecraft.client.gui.components.Checkbox;
+import net.minecraft.client.gui.components.ObjectSelectionList;
+//? if >=1.21.10 {
+import net.minecraft.client.input.MouseButtonEvent;
+//?} else {
+/*import net.minecraft.Util;
+*///?}
+import net.minecraft.network.chat.Component;
+import net.minecraft.resources.ResourceLocation;
+import net.minecraft.client.renderer.RenderPipelines;
+import net.fabricmc.loader.api.FabricLoader;
+
+public class GuiProxyList extends Screen {
+ private final Screen parentScreen;
+
+ private ProxyList proxyList;
+ private Button connectButton;
+ private Button deleteButton;
+ private Button editButton;
+ private Checkbox showMultiplayerCheck;
+
+ private static final String TEXT_TITLE = Component.translatable("ui.coffeeproxy.list.title").getString();
+ private static final String TEXT_FAILED = Component.translatable("ui.coffeeproxy.list.failed").getString();
+ private static final Component TEXT_CONNECT = Component.translatable("ui.coffeeproxy.list.connect");
+ private static final Component TEXT_DISCONNECT = Component.translatable("ui.coffeeproxy.list.disconnect");
+
+ public GuiProxyList(Screen parentScreen) {
+ super(Component.literal(TEXT_TITLE));
+ this.parentScreen = parentScreen;
+ }
+
+ @Override
+ public void init() {
+ this.proxyList = new ProxyList(this.minecraft);
+ this.addRenderableWidget(this.proxyList);
+ for (var entry : Config.proxies.entrySet()) {
+ this.proxyList.addProxy(entry.getKey(), entry.getValue());
+ }
+
+ int x1 = this.width / 2 - 154;
+ int x2 = x1 + 104;
+ int x3 = x1 + 208;
+
+ this.connectButton = this.addRenderableWidget(Button
+ .builder(TEXT_CONNECT, button -> connectSelected())
+ .bounds(x1, this.height - 52, 100, 20).build());
+ this.addRenderableWidget(Button
+ .builder(Component.translatable("ui.coffeeproxy.list.add"), button -> openEditor(null))
+ .bounds(x2, this.height - 52, 100, 20).build());
+ this.editButton = this.addRenderableWidget(Button
+ .builder(Component.translatable("ui.coffeeproxy.list.edit"), button -> {
+ ProxyEntry selected = this.proxyList.getSelected();
+ if (selected != null) {
+ openEditor(selected.name);
+ }
+ })
+ .bounds(x3, this.height - 52, 100, 20).build());
+
+ this.deleteButton = this.addRenderableWidget(Button
+ .builder(Component.translatable("ui.coffeeproxy.list.delete"), button -> deleteSelected())
+ .bounds(x1, this.height - 28, 100, 20).build());
+ this.addRenderableWidget(Button
+ .builder(Component.translatable("ui.coffeeproxy.list.refresh"), button -> this.proxyList.refreshPings())
+ .bounds(x2, this.height - 28, 100, 20).build());
+ this.addRenderableWidget(Button
+ .builder(Component.translatable("ui.coffeeproxy.list.done"), button -> onClose())
+ .bounds(x3, this.height - 28, 100, 20).build());
+
+ Checkbox.Builder showMultiplayerBuilder = Checkbox
+ .builder(Component.translatable("ui.coffeeproxy.options.showMultiplayerButton"), this.font);
+ showMultiplayerBuilder.pos(0, this.height - 78);
+ showMultiplayerBuilder.selected(Config.showMultiplayerButton);
+ this.showMultiplayerCheck = showMultiplayerBuilder.build();
+ this.showMultiplayerCheck.setX(this.width / 2 - this.showMultiplayerCheck.getWidth() / 2);
+ this.showMultiplayerCheck.active = FabricLoader.getInstance().isModLoaded("modmenu");
+ this.addRenderableWidget(this.showMultiplayerCheck);
+ }
+
+ private void connectSelected() {
+ ProxyEntry selected = this.proxyList.getSelected();
+ if (selected != null) {
+ connectTo(selected.name);
+ }
+ }
+
+ void connectTo(String name) {
+ Config.setActive(Config.activeName.equals(name) ? "" : name);
+ Config.saveConfig();
+ ProxyEntry selected = this.proxyList.getSelected();
+ if (selected != null) {
+ selected.startPing();
+ }
+ }
+
+ void openEditor(String editName) {
+ Coffeeproxy.openScreen(new GuiProxy(this, editName));
+ }
+
+ private void deleteSelected() {
+ ProxyEntry selected = this.proxyList.getSelected();
+ if (selected == null) {
+ return;
+ }
+ Config.proxies.remove(selected.name);
+ if (Config.activeName.equals(selected.name)) {
+ Config.setActive("");
+ }
+ Config.saveConfig();
+ this.proxyList.removeSelected(selected);
+ this.proxyList.setSelected(null);
+ }
+
+ @Override
+ public void tick() {
+ this.proxyList.tickEntries();
+ }
+
+ @Override
+ //? if <26 {
+ public void render(GuiGraphics guiGraphics, int mouseX, int mouseY, float partialTicks) {
+ super.render(guiGraphics, mouseX, mouseY, partialTicks);
+ //?} else {
+ /*public void extractRenderState(GuiGraphics guiGraphics, int mouseX, int mouseY, float partialTicks) {
+ super.extractRenderState(guiGraphics, mouseX, mouseY, partialTicks);
+ *///?}
+ ProxyEntry selected = this.proxyList.getSelected();
+ this.connectButton.active = selected != null;
+ this.connectButton.setMessage(selected != null && selected.name.equals(Config.activeName)
+ ? TEXT_DISCONNECT : TEXT_CONNECT);
+ this.deleteButton.active = selected != null;
+ this.editButton.active = selected != null;
+
+ //? if <26 {
+ guiGraphics.drawCenteredString(this.font, TEXT_TITLE, this.width / 2, 8, 0xFFFFFFFF);
+ //?} else {
+ /*guiGraphics.centeredText(this.font, TEXT_TITLE, this.width / 2, 8, 0xFFFFFFFF);
+ *///?}
+ }
+
+ @Override
+ public void onClose() {
+ Config.showMultiplayerButton = this.showMultiplayerCheck.selected();
+ Config.saveConfig();
+ Coffeeproxy.openScreen(this.parentScreen);
+ }
+
+ private class ProxyList extends ObjectSelectionList {
+ ProxyList(Minecraft minecraft) {
+ super(minecraft, GuiProxyList.this.width, GuiProxyList.this.height - 116, 28, 36);
+ }
+
+ @Override
+ public int getRowWidth() {
+ return 305;
+ }
+
+ void addProxy(String name, String entry) {
+ this.addEntry(new ProxyEntry(name, entry));
+ }
+
+ void removeSelected(ProxyEntry row) {
+ this.removeEntry(row);
+ }
+
+ void refreshPings() {
+ for (ProxyEntry row : this.children()) {
+ row.startPing();
+ }
+ }
+
+ void tickEntries() {
+ for (ProxyEntry row : this.children()) {
+ row.tickPing();
+ }
+ }
+ }
+
+ private class ProxyEntry extends ObjectSelectionList.Entry {
+ final String name;
+ final Proxy proxy;
+ private TestPing testPing = new TestPing();
+ private long lastClickTime;
+ private int rowLeft;
+ private int rowTop;
+ private int rowWidth;
+
+ ProxyEntry(String name, String endpoint) {
+ this.name = name;
+ this.proxy = Proxy.parse(endpoint);
+ startPing();
+ }
+
+ void startPing() {
+ this.testPing = new TestPing();
+ this.testPing.run("anticheat-test.com", 25565, this.proxy);
+ }
+
+ void tickPing() {
+ this.testPing.pingPendingNetworks();
+ }
+
+ @Override
+ public Component getNarration() {
+ return Component.literal(this.name);
+ }
+
+ private int rowColor() {
+ if (this.testPing.failed) {
+ return 0xFFFF5555;
+ }
+ if (this.name.equals(Config.activeName)) {
+ return 0xFF55FF55;
+ }
+ return 0xFFA0A0A0;
+ }
+
+ private String pingText() {
+ if (this.testPing.failed) {
+ return TEXT_FAILED;
+ }
+ if (this.testPing.latency >= 0) {
+ return Component.translatable("multiplayer.status.ping", this.testPing.latency).getString();
+ }
+ return "";
+ }
+
+ // vanilla ServerSelectionList bands: <=150 full bars, then 300/600/1000
+ private String pingSprite() {
+ long l = this.testPing.latency;
+ if (l < 0) {
+ return "unreachable";
+ }
+ if (l <= 150) return "ping_5";
+ if (l <= 300) return "ping_4";
+ if (l <= 600) return "ping_3";
+ if (l <= 1000) return "ping_2";
+ return "ping_1";
+ }
+
+ private boolean clickRow(boolean doubleClick) {
+ proxyList.setSelected(this);
+ if (doubleClick) {
+ connectTo(this.name);
+ }
+ return true;
+ }
+
+ //? if <1.21.10 {
+ /*@Override
+ public void render(GuiGraphics guiGraphics, int index, int top, int left, int width, int height, int mouseX,
+ int mouseY, boolean hovering, float partialTicks) {
+ this.rowLeft = left;
+ this.rowTop = top;
+ this.rowWidth = width;
+ drawRow(guiGraphics);
+ }
+
+ @Override
+ public boolean mouseClicked(double mouseX, double mouseY, int button) {
+ boolean doubleClick = Util.getMillis() - this.lastClickTime < 250;
+ this.lastClickTime = Util.getMillis();
+ return clickRow(doubleClick);
+ }
+ *///?}
+ //? if >=1.21.10 <26 {
+ @Override
+ public void renderContent(GuiGraphics guiGraphics, int mouseX, int mouseY, boolean hovering, float partialTicks) {
+ this.rowLeft = this.getX();
+ this.rowTop = this.getY();
+ this.rowWidth = this.getWidth();
+ drawRow(guiGraphics);
+ }
+ //?}
+ //? if >=26 {
+ /*@Override
+ public void extractContent(GuiGraphics guiGraphics, int mouseX, int mouseY, boolean hovering,
+ float partialTicks) {
+ this.rowLeft = this.getX();
+ this.rowTop = this.getY();
+ this.rowWidth = this.getWidth();
+ drawRow(guiGraphics);
+ }
+ *///?}
+ //? if >=1.21.10 {
+ @Override
+ public boolean mouseClicked(MouseButtonEvent event, boolean doubleClick) {
+ return clickRow(doubleClick);
+ }
+ //?}
+
+ //? if <26 {
+ private void drawRow(GuiGraphics guiGraphics) {
+ int color = rowColor();
+ String ping = pingText();
+ guiGraphics.drawString(GuiProxyList.this.font, this.name, this.rowLeft + 2, this.rowTop + 2, color);
+ guiGraphics.drawString(GuiProxyList.this.font, this.proxy.ipPort, this.rowLeft + 2, this.rowTop + 13, 0xFF808080);
+ int iconX = this.rowLeft + this.rowWidth - 14;
+ ResourceLocation sprite = ResourceLocation.withDefaultNamespace("server_list/" + pingSprite());
+ guiGraphics.blitSprite(RenderPipelines.GUI_TEXTURED, sprite,
+ iconX, this.rowTop + 2, 10, 8);
+ guiGraphics.drawString(GuiProxyList.this.font, ping, iconX - 2 - GuiProxyList.this.font.width(ping), this.rowTop + 2, color);
+ }
+ //?} else {
+ /*private void drawRow(GuiGraphics guiGraphics) {
+ int color = rowColor();
+ String ping = pingText();
+ guiGraphics.text(GuiProxyList.this.font, this.name, this.rowLeft + 2, this.rowTop + 2, color);
+ guiGraphics.text(GuiProxyList.this.font, this.proxy.ipPort, this.rowLeft + 2, this.rowTop + 13, 0xFF808080);
+ int iconX = this.rowLeft + this.rowWidth - 14;
+ ResourceLocation sprite = ResourceLocation.withDefaultNamespace("server_list/" + pingSprite());
+ guiGraphics.blitSprite(RenderPipelines.GUI_TEXTURED, sprite,
+ iconX, this.rowTop + 2, 10, 8);
+ guiGraphics.text(GuiProxyList.this.font, ping, iconX - 2 - GuiProxyList.this.font.width(ping), this.rowTop + 2, color);
+ }
+ *///?}
+ }
+}
diff --git a/src/main/java/coffee/axle/proxy/Proxy.java b/src/main/java/coffee/axle/proxy/Proxy.java
index 2e0006f..45e66dc 100644
--- a/src/main/java/coffee/axle/proxy/Proxy.java
+++ b/src/main/java/coffee/axle/proxy/Proxy.java
@@ -1,6 +1,12 @@
+// SPDX-FileCopyrightText: 2026 Axle Duggan (axlecoffee)
+//
+// SPDX-License-Identifier: AGPL-3.0-or-later
package coffee.axle.proxy;
-import com.google.gson.annotations.SerializedName;
+import io.netty.handler.proxy.HttpProxyHandler;
+import io.netty.handler.proxy.ProxyHandler;
+import io.netty.handler.proxy.Socks4ProxyHandler;
+import io.netty.handler.proxy.Socks5ProxyHandler;
import java.net.InetAddress;
import java.net.InetSocketAddress;
@@ -11,7 +17,6 @@ public class Proxy {
private static final ConcurrentHashMap DNS_CACHE = new ConcurrentHashMap<>();
private static final long DNS_CACHE_TTL_MS = 5 * 60 * 1000L;
- @SerializedName("IP:PORT")
public String ipPort = "";
public ProxyType type = ProxyType.SOCKS5;
public String username = "";
@@ -23,10 +28,6 @@ public class Proxy {
public Proxy() {
}
- public Proxy(boolean isSocks4, String ipPort, String username, String password) {
- this(isSocks4 ? ProxyType.SOCKS4 : ProxyType.SOCKS5, ipPort, username, password);
- }
-
public Proxy(ProxyType type, String ipPort, String username, String password) {
this.type = type;
this.ipPort = ipPort;
@@ -34,6 +35,52 @@ public Proxy(ProxyType type, String ipPort, String username, String password) {
this.password = password;
}
+ public static Proxy parse(String entry) {
+ Proxy proxy = new Proxy();
+ String rest = entry.trim();
+ int schemeEnd = rest.indexOf("://");
+ if (schemeEnd >= 0) {
+ String scheme = rest.substring(0, schemeEnd);
+ if (scheme.equalsIgnoreCase("socks4")) {
+ proxy.type = ProxyType.SOCKS4;
+ } else if (scheme.equalsIgnoreCase("http")) {
+ proxy.type = ProxyType.HTTP;
+ }
+ rest = rest.substring(schemeEnd + 3).trim();
+ }
+ int at = rest.indexOf('@');
+ if (at >= 0) {
+ String auth = rest.substring(at + 1);
+ rest = rest.substring(0, at);
+ int colon = auth.indexOf(':');
+ if (colon >= 0) {
+ proxy.username = auth.substring(0, colon);
+ proxy.password = auth.substring(colon + 1);
+ } else {
+ proxy.username = auth;
+ }
+ }
+ proxy.ipPort = rest;
+ return proxy;
+ }
+
+ public String format() {
+ String scheme = "";
+ if (type == ProxyType.SOCKS4) {
+ scheme = "socks4://";
+ } else if (type == ProxyType.HTTP) {
+ scheme = "http://";
+ }
+ String auth = "";
+ if (!username.isEmpty()) {
+ auth = "@" + username;
+ if (!password.isEmpty()) {
+ auth += ":" + password;
+ }
+ }
+ return scheme + ipPort + auth;
+ }
+
public int getPort() {
return Integer.parseInt(ipPort.split(":")[1]);
}
@@ -52,6 +99,19 @@ public InetSocketAddress resolveProxyAddress() {
return cachedAddress;
}
+ public ProxyHandler getHandler() {
+ return switch (type) {
+ case SOCKS5 -> new Socks5ProxyHandler(resolveProxyAddress(),
+ username.isEmpty() ? null : username,
+ password.isEmpty() ? null : password);
+ case SOCKS4 -> new Socks4ProxyHandler(resolveProxyAddress(),
+ username.isEmpty() ? null : username);
+ case HTTP -> new HttpProxyHandler(resolveProxyAddress(),
+ username.isEmpty() ? null : username,
+ password.isEmpty() ? "" : password);
+ };
+ }
+
public static InetAddress resolveAddress(String host) throws UnknownHostException {
CachedResolve cached = DNS_CACHE.get(host);
long now = System.currentTimeMillis();
diff --git a/src/main/java/coffee/axle/proxy/TestPing.java b/src/main/java/coffee/axle/proxy/TestPing.java
index 9a98b89..4fc2d9c 100644
--- a/src/main/java/coffee/axle/proxy/TestPing.java
+++ b/src/main/java/coffee/axle/proxy/TestPing.java
@@ -1,11 +1,12 @@
+// SPDX-FileCopyrightText: 2026 Axle Duggan (axlecoffee)
+//
+// SPDX-License-Identifier: AGPL-3.0-or-later
package coffee.axle.proxy;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.*;
import io.netty.channel.socket.nio.NioSocketChannel;
-import io.netty.handler.proxy.Socks4ProxyHandler;
-import io.netty.handler.proxy.Socks5ProxyHandler;
import io.netty.handler.timeout.ReadTimeoutHandler;
import net.minecraft.network.Connection;
import net.minecraft.network.ConnectionProtocol;
@@ -16,7 +17,6 @@
import net.minecraft.network.protocol.status.ClientStatusPacketListener;
import net.minecraft.network.protocol.status.ClientboundStatusResponsePacket;
import net.minecraft.network.protocol.status.ServerboundStatusRequestPacket;
-import net.minecraft.network.protocol.status.StatusProtocols;
import net.minecraft.network.chat.Component;
import net.minecraft.ChatFormatting;
//? if <1.21.11 {
@@ -30,6 +30,8 @@
public class TestPing {
public String state = "";
+ public long latency = -1;
+ public boolean failed = false;
private long pingSentAt;
private Connection pingDestination = null;
@@ -39,6 +41,8 @@ public class TestPing {
public void run(String ip, int port, Proxy proxy) {
this.proxy = proxy;
+ this.latency = -1;
+ this.failed = false;
TestPing.EXECUTOR.submit(() -> ping(ip, port));
}
@@ -48,37 +52,32 @@ private void ping(String ip, int port) {
try {
connection = createTestConnection(Proxy.resolveAddress(ip), port);
} catch (UnknownHostException e) {
+ failed = true;
state = ChatFormatting.RED + Component.translatable("ui.coffeeproxy.err.cantConnect").getString();
return;
} catch (Exception e) {
+ failed = true;
state = ChatFormatting.RED + Component.translatable("ui.coffeeproxy.err.cantPing", ip).getString();
return;
}
pingDestination = connection;
- connection.setupInboundProtocol(StatusProtocols.CLIENTBOUND, new ClientStatusPacketListener() {
+ connection.initiateServerboundStatusConnection(ip, port, new ClientStatusPacketListener() {
private boolean successful;
@Override
public void handlePongResponse(ClientboundPongResponsePacket packet) {
successful = true;
pingDestination = null;
- //? if <1.21.11 {
long pingToServer = Util.getMillis() - pingSentAt;
- //?} else {
- /*long pingToServer = System.currentTimeMillis() - pingSentAt;
- *///?}
+ latency = pingToServer;
state = Component.translatable("ui.coffeeproxy.ping.showPing", pingToServer).getString();
connection.disconnect(Component.translatable("multiplayer.status.finished"));
}
@Override
public void handleStatusResponse(ClientboundStatusResponsePacket packet) {
- //? if <1.21.11 {
pingSentAt = Util.getMillis();
- //?} else {
- /*pingSentAt = System.currentTimeMillis();
- *///?}
connection.send(new ServerboundPingRequestPacket(pingSentAt));
}
@@ -86,6 +85,7 @@ public void handleStatusResponse(ClientboundStatusResponsePacket packet) {
public void onDisconnect(DisconnectionDetails details) {
pingDestination = null;
if (!this.successful) {
+ failed = true;
state = ChatFormatting.RED
+ Component.translatable("ui.coffeeproxy.err.cantPingReason", ip, details.reason().getString())
.getString();
@@ -103,64 +103,46 @@ public ConnectionProtocol protocol() {
}
});
- try {
- connection.send(ServerboundStatusRequestPacket.INSTANCE);
- } catch (Throwable throwable) {
- state = ChatFormatting.RED + Component.translatable("ui.coffeeproxy.err.cantPing", ip).getString();
- }
+ // vanilla sends this right after initiate (it only sets up protocols + handshake)
+ connection.send(ServerboundStatusRequestPacket.INSTANCE);
}
private Connection createTestConnection(InetAddress address, int port) {
final Connection connection = new Connection(PacketFlow.CLIENTBOUND);
- Coffeeproxy.suppressProxyMixin = true;
- try {
- //? if <1.21.11 {
- new Bootstrap()
- .group(Connection.NETWORK_WORKER_GROUP.get())
- //?} else {
- /*var eventLoopHolder = net.minecraft.server.network.EventLoopGroupHolder.remote(false);
- new Bootstrap()
- .group(eventLoopHolder.eventLoopGroup())
- *///?}
- .handler(new ChannelInitializer() {
- @Override
- protected void initChannel(Channel channel) {
- try {
- channel.config().setOption(ChannelOption.TCP_NODELAY, true);
- } catch (ChannelException ignored) {
- }
-
- ChannelPipeline pipeline = channel.pipeline().addLast("timeout",
- new ReadTimeoutHandler(30));
- Connection.configureSerialization(pipeline, PacketFlow.CLIENTBOUND, false, null);
- connection.configurePacketHandler(pipeline);
-
- switch (proxy.type) {
- case SOCKS5 -> channel.pipeline().addFirst(new Socks5ProxyHandler(
- proxy.resolveProxyAddress(),
- proxy.username.isEmpty() ? null : proxy.username,
- proxy.password.isEmpty() ? null : proxy.password));
- case SOCKS4 -> channel.pipeline().addFirst(new Socks4ProxyHandler(
- proxy.resolveProxyAddress(),
- proxy.username.isEmpty() ? null : proxy.username));
- case HTTP -> channel.pipeline().addFirst(new io.netty.handler.proxy.HttpProxyHandler(
- proxy.resolveProxyAddress(),
- proxy.username.isEmpty() ? null : proxy.username,
- proxy.password.isEmpty() ? "" : proxy.password));
- }
+ //? if <1.21.11 {
+ new Bootstrap()
+ .group(Connection.NETWORK_WORKER_GROUP.get())
+ //?} else {
+ /*var eventLoopHolder = net.minecraft.server.network.EventLoopGroupHolder.remote(false);
+ new Bootstrap()
+ .group(eventLoopHolder.eventLoopGroup())
+ *///?}
+ .handler(new ChannelInitializer() {
+ @Override
+ protected void initChannel(Channel channel) {
+ try {
+ channel.config().setOption(ChannelOption.TCP_NODELAY, true);
+ } catch (ChannelException ignored) {
}
- })
- //? if <1.21.11 {
- .channel(NioSocketChannel.class)
- //?} else {
- /*.channel(eventLoopHolder.channelCls())
- *///?}
- .connect(address, port)
- .syncUninterruptibly();
- } finally {
- Coffeeproxy.suppressProxyMixin = false;
- }
+
+ ChannelPipeline pipeline = channel.pipeline().addLast("timeout",
+ new ReadTimeoutHandler(30));
+
+ // added before configureSerialization so the mixin sees it and skips this channel
+ pipeline.addFirst(proxy.getHandler());
+
+ Connection.configureSerialization(pipeline, PacketFlow.CLIENTBOUND, false, null);
+ connection.configurePacketHandler(pipeline);
+ }
+ })
+ //? if <1.21.11 {
+ .channel(NioSocketChannel.class)
+ //?} else {
+ /*.channel(eventLoopHolder.channelCls())
+ *///?}
+ .connect(address, port)
+ .syncUninterruptibly();
return connection;
}
diff --git a/src/main/java/coffee/axle/proxy/mixin/ClientConnectionInit.java b/src/main/java/coffee/axle/proxy/mixin/ClientConnectionInit.java
index 44b39f6..64eab2e 100644
--- a/src/main/java/coffee/axle/proxy/mixin/ClientConnectionInit.java
+++ b/src/main/java/coffee/axle/proxy/mixin/ClientConnectionInit.java
@@ -1,14 +1,15 @@
+// SPDX-FileCopyrightText: 2026 Axle Duggan (axlecoffee)
+//
+// SPDX-License-Identifier: AGPL-3.0-or-later
package coffee.axle.proxy.mixin;
import coffee.axle.proxy.Coffeeproxy;
import coffee.axle.proxy.Proxy;
import io.netty.channel.ChannelPipeline;
-import io.netty.handler.proxy.Socks4ProxyHandler;
-import io.netty.handler.proxy.Socks5ProxyHandler;
+import io.netty.handler.proxy.ProxyHandler;
import net.minecraft.network.Connection;
import net.minecraft.network.protocol.PacketFlow;
import net.minecraft.network.BandwidthDebugMonitor;
-import net.minecraft.network.chat.Component;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
@@ -17,35 +18,14 @@
@Mixin(Connection.class)
public class ClientConnectionInit {
@Inject(method = "configureSerialization", at = @At("HEAD"))
- private static void onConfigureSerialization(ChannelPipeline pipeline, PacketFlow flow, boolean local,
- BandwidthDebugMonitor bandwidthDebugMonitor, CallbackInfo ci) {
- if (local || Coffeeproxy.suppressProxyMixin)
+ private static void onConfigureSerialization(ChannelPipeline pipe, PacketFlow flow, boolean local, BandwidthDebugMonitor bandwidthDebugMonitor, CallbackInfo ci) {
+ if (local || pipe.get(ProxyHandler.class) != null)
return;
Proxy proxy = Coffeeproxy.proxy;
if (Coffeeproxy.proxyEnabled) {
- Coffeeproxy.lastUsedProxy = proxy;
-
- switch (proxy.type) {
- case SOCKS5 -> pipeline.addFirst(new Socks5ProxyHandler(
- proxy.resolveProxyAddress(),
- proxy.username.isEmpty() ? null : proxy.username,
- proxy.password.isEmpty() ? null : proxy.password));
- case SOCKS4 -> pipeline.addFirst(new Socks4ProxyHandler(
- proxy.resolveProxyAddress(),
- proxy.username.isEmpty() ? null : proxy.username));
- case HTTP -> pipeline.addFirst(new io.netty.handler.proxy.HttpProxyHandler(
- proxy.resolveProxyAddress(),
- proxy.username.isEmpty() ? null : proxy.username,
- proxy.password.isEmpty() ? "" : proxy.password));
- }
- } else {
- Coffeeproxy.lastUsedProxy = new Proxy();
- }
-
- if (Coffeeproxy.proxyMenuButton != null) {
- Coffeeproxy.proxyMenuButton.setMessage(Component.literal("Proxy: " + Coffeeproxy.getLastUsedProxyIp()));
+ pipe.addFirst(proxy.getHandler());
}
}
}
diff --git a/src/main/java/coffee/axle/proxy/mixin/MultiplayerScreenOpen.java b/src/main/java/coffee/axle/proxy/mixin/MultiplayerScreenOpen.java
index 9aa07d8..6dc276c 100644
--- a/src/main/java/coffee/axle/proxy/mixin/MultiplayerScreenOpen.java
+++ b/src/main/java/coffee/axle/proxy/mixin/MultiplayerScreenOpen.java
@@ -1,10 +1,12 @@
+// SPDX-FileCopyrightText: 2026 Axle Duggan (axlecoffee)
+//
+// SPDX-License-Identifier: AGPL-3.0-or-later
package coffee.axle.proxy.mixin;
-import coffee.axle.proxy.Config;
import coffee.axle.proxy.Coffeeproxy;
-import coffee.axle.proxy.GuiProxy;
+import coffee.axle.proxy.Config;
+import coffee.axle.proxy.GuiProxyList;
import net.fabricmc.loader.api.FabricLoader;
-import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.screens.Screen;
import net.minecraft.client.gui.screens.multiplayer.JoinMultiplayerScreen;
import net.minecraft.client.gui.components.Button;
@@ -25,39 +27,41 @@ protected MultiplayerScreenOpen(Component title) {
super(title);
}
+ // init not repositionElements on 1.21.4/5/8: JoinMultiplayerScreen only declares the latter
+ // on 1.21.10+, loom can't remap inherited targets so the jar would keep mojmap names.
+ // 1.21.10+ overrides repositionElements WITHOUT super.rebuildWidgets, so init() never
+ // re-fires on resize there -> the button would drift. Both injects needed.
+ //? if <1.21.10 {
+ /*@Inject(method = "init()V", at = @At("TAIL"))
+ private void repositionProxyButton(CallbackInfo ci) {
+ handleProxyButton();
+ }
+ *///?}
+ //? if >=1.21.10 {
@Inject(method = "repositionElements()V", at = @At("TAIL"))
private void repositionProxyButton(CallbackInfo ci) {
- if (coffeeProxy$proxyMenuButton == null) {
- String playerName = Minecraft.getInstance().getUser().getName();
- if (!playerName.equals(Config.lastPlayerName)) {
- Config.lastPlayerName = playerName;
- if (Config.accounts.containsKey(playerName)) {
- Coffeeproxy.proxy = Config.accounts.get(playerName);
- } else {
- if (Config.accounts.containsKey("")) {
- Coffeeproxy.proxy = Config.accounts.get("");
- }
- }
- }
+ handleProxyButton();
+ }
+ //?}
+ @Unique
+ private void handleProxyButton() {
+ if (coffeeProxy$proxyMenuButton == null) {
JoinMultiplayerScreen ms = (JoinMultiplayerScreen) (Object) this;
coffeeProxy$proxyMenuButton = Button
- .builder(Component.literal("Proxy: " + Coffeeproxy.getLastUsedProxyIp()), (buttonWidget) -> {
- //? if <26.2 {
- Minecraft.getInstance().setScreen(new GuiProxy(ms));
- //?} else {
- /*Minecraft.getInstance().gui.setScreen(new GuiProxy(ms));
- *///?}
+ .builder(Component.literal("Proxy: "), (buttonWidget) -> {
+ Coffeeproxy.openScreen(new GuiProxyList(ms));
}).size(120, 20).build();
}
-
+ // suggestion (also commission?) from a random guy to "hide" it - like i guess man
if (!FabricLoader.getInstance().isModLoaded("modmenu") || Config.showMultiplayerButton) {
if (!this.children().contains(coffeeProxy$proxyMenuButton)) {
this.addRenderableWidget(coffeeProxy$proxyMenuButton);
}
}
- Coffeeproxy.proxyMenuButton = coffeeProxy$proxyMenuButton;
+ coffeeProxy$proxyMenuButton.setMessage(Component.literal(
+ "Proxy: " + (Config.activeName.isEmpty() ? "none" : Config.activeName)));
coffeeProxy$proxyMenuButton.setPosition(this.width - 125, 5);
}
-}
+}
\ No newline at end of file
diff --git a/src/main/resources/assets/coffeeproxy/lang/en_us.json b/src/main/resources/assets/coffeeproxy/lang/en_us.json
index 786016c..a3dfd25 100644
--- a/src/main/resources/assets/coffeeproxy/lang/en_us.json
+++ b/src/main/resources/assets/coffeeproxy/lang/en_us.json
@@ -4,14 +4,24 @@
"ui.coffeeproxy.options.proxyType": "Proxy Type: ",
"ui.coffeeproxy.options.auth": "Proxy Authentication (optional)",
"ui.coffeeproxy.options.ipPort": "IP:PORT: ",
- "ui.coffeeproxy.options.apply": "Apply",
+ "ui.coffeeproxy.options.name": "Name: ",
+ "ui.coffeeproxy.options.save": "Save",
"ui.coffeeproxy.options.cancel": "Cancel",
"ui.coffeeproxy.options.test": "Test",
- "ui.coffeeproxy.options.proxyEnabled": "Proxy Enabled",
"ui.coffeeproxy.options.showMultiplayerButton": "Show in Multiplayer Screen",
+ "ui.coffeeproxy.list.title": "Proxies",
+ "ui.coffeeproxy.list.connect": "Connect",
+ "ui.coffeeproxy.list.disconnect": "Disconnect",
+ "ui.coffeeproxy.list.delete": "Delete",
+ "ui.coffeeproxy.list.refresh": "Refresh All",
+ "ui.coffeeproxy.list.add": "Add",
+ "ui.coffeeproxy.list.edit": "Edit",
+ "ui.coffeeproxy.list.done": "Done",
+ "ui.coffeeproxy.list.failed": "Failed",
"ui.coffeeproxy.auth.id": "User ID: ",
"ui.coffeeproxy.auth.username": "Username: ",
"ui.coffeeproxy.auth.password": "Password: ",
+ "ui.coffeeproxy.err.specName": "Specify a name",
"ui.coffeeproxy.err.specProxy": "Specify proxy to test",
"ui.coffeeproxy.err.cantConnect": "Can't connect to proxy",
"ui.coffeeproxy.err.cantPing": "Can't ping %d",
From 73e90e10f47d4af6ba1c185f56f8e1ac2eeb98d9 Mon Sep 17 00:00:00 2001
From: axlecoffee <95949634+axlecoffee@users.noreply.github.com>
Date: Thu, 8 Oct 2026 05:54:36 +0000
Subject: [PATCH 5/9] bump to gradle 9.8.1
---
gradle/wrapper/gradle-wrapper.properties | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index dbc3ce4..1a31fd1 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.0-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-9.8.1-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
From 7e772d2649d854260ef3616453b0fa0ff7fba19e Mon Sep 17 00:00:00 2001
From: axlecoffee <95949634+axlecoffee@users.noreply.github.com>
Date: Thu, 8 Oct 2026 06:00:20 +0000
Subject: [PATCH 6/9] cleanup abstraction because stonecutter is #near
---
src/main/java/coffee/axle/proxy/Draw.java | 35 ++++++++++++
src/main/java/coffee/axle/proxy/GuiProxy.java | 56 +++++--------------
.../java/coffee/axle/proxy/GuiProxyList.java | 26 ++-------
3 files changed, 52 insertions(+), 65 deletions(-)
create mode 100644 src/main/java/coffee/axle/proxy/Draw.java
diff --git a/src/main/java/coffee/axle/proxy/Draw.java b/src/main/java/coffee/axle/proxy/Draw.java
new file mode 100644
index 0000000..7e9a2fd
--- /dev/null
+++ b/src/main/java/coffee/axle/proxy/Draw.java
@@ -0,0 +1,35 @@
+// SPDX-FileCopyrightText: 2026 Axle Duggan (axlecoffee)
+//
+// SPDX-License-Identifier: AGPL-3.0-or-later
+package coffee.axle.proxy;
+
+import net.minecraft.client.gui.Font;
+import net.minecraft.client.gui.GuiGraphics;
+import net.minecraft.client.gui.components.EditBox;
+
+// abstracted or smth because of stonecutter if else coming to HARM ME
+final class Draw {
+ static void text(GuiGraphics g, Font font, String s, int x, int y, int color) {
+ //? if <26 {
+ g.drawString(font, s, x, y, color);
+ //?} else {
+ /*g.text(font, s, x, y, color);
+ *///?}
+ }
+
+ static void centeredText(GuiGraphics g, Font font, String s, int x, int y, int color) {
+ //? if <26 {
+ g.drawCenteredString(font, s, x, y, color);
+ //?} else {
+ /*g.centeredText(font, s, x, y, color);
+ *///?}
+ }
+
+ static void widget(GuiGraphics g, EditBox box, int mouseX, int mouseY, float partialTicks) {
+ //? if <26 {
+ box.render(g, mouseX, mouseY, partialTicks);
+ //?} else {
+ /*box.extractWidgetRenderState(g, mouseX, mouseY, partialTicks);
+ *///?}
+ }
+}
diff --git a/src/main/java/coffee/axle/proxy/GuiProxy.java b/src/main/java/coffee/axle/proxy/GuiProxy.java
index 3080acf..d987662 100644
--- a/src/main/java/coffee/axle/proxy/GuiProxy.java
+++ b/src/main/java/coffee/axle/proxy/GuiProxy.java
@@ -95,63 +95,33 @@ public void render(GuiGraphics guiGraphics, int mouseX, int mouseY, float partia
super.extractRenderState(guiGraphics, mouseX, mouseY, partialTicks);
*///?}
- //? if <26 {
- guiGraphics.drawString(this.font, Component.translatable("ui.coffeeproxy.options.name").getString(),
+ Draw.text(guiGraphics, this.font, Component.translatable("ui.coffeeproxy.options.name").getString(),
positionX, positionY[1] - 10, 0xFFA0A0A0);
- guiGraphics.drawString(this.font, Component.translatable("ui.coffeeproxy.options.proxyType").getString(),
+ Draw.text(guiGraphics, this.font, Component.translatable("ui.coffeeproxy.options.proxyType").getString(),
positionX, positionY[2] - 10, 0xFFA0A0A0);
- guiGraphics.drawCenteredString(this.font,
+ Draw.centeredText(guiGraphics, this.font,
Component.translatable("ui.coffeeproxy.options.auth").getString(), this.width / 2, positionY[4] + 8,
0xFFFFFFFF);
- guiGraphics.drawString(this.font, Component.translatable("ui.coffeeproxy.options.ipPort").getString(),
+ Draw.text(guiGraphics, this.font, Component.translatable("ui.coffeeproxy.options.ipPort").getString(),
positionX, positionY[3] - 10, 0xFFA0A0A0);
- this.name.render(guiGraphics, mouseX, mouseY, partialTicks);
- this.ipPort.render(guiGraphics, mouseX, mouseY, partialTicks);
+ Draw.widget(guiGraphics, this.name, mouseX, mouseY, partialTicks);
+ Draw.widget(guiGraphics, this.ipPort, mouseX, mouseY, partialTicks);
if (currentType == Proxy.ProxyType.SOCKS4) {
- guiGraphics.drawString(this.font, Component.translatable("ui.coffeeproxy.auth.id").getString(),
+ Draw.text(guiGraphics, this.font, Component.translatable("ui.coffeeproxy.auth.id").getString(),
positionX, positionY[5] - 10, 0xFFA0A0A0);
- this.username.render(guiGraphics, mouseX, mouseY, partialTicks);
+ Draw.widget(guiGraphics, this.username, mouseX, mouseY, partialTicks);
} else {
- guiGraphics.drawString(this.font, Component.translatable("ui.coffeeproxy.auth.password").getString(),
+ Draw.text(guiGraphics, this.font, Component.translatable("ui.coffeeproxy.auth.password").getString(),
positionX, positionY[6] - 10, 0xFFA0A0A0);
- guiGraphics.drawString(this.font, Component.translatable("ui.coffeeproxy.auth.username").getString(),
+ Draw.text(guiGraphics, this.font, Component.translatable("ui.coffeeproxy.auth.username").getString(),
positionX, positionY[5] - 10, 0xFFA0A0A0);
- this.username.render(guiGraphics, mouseX, mouseY, partialTicks);
- this.password.render(guiGraphics, mouseX, mouseY, partialTicks);
+ Draw.widget(guiGraphics, this.username, mouseX, mouseY, partialTicks);
+ Draw.widget(guiGraphics, this.password, mouseX, mouseY, partialTicks);
}
- guiGraphics.drawCenteredString(this.font, !msg.isEmpty() ? msg : testPing.state, this.width / 2,
+ Draw.centeredText(guiGraphics, this.font, !msg.isEmpty() ? msg : testPing.state, this.width / 2,
positionY[7] + 5, 0xFFA0A0A0);
- //?} else {
- /*guiGraphics.text(this.font, Component.translatable("ui.coffeeproxy.options.name").getString(),
- positionX, positionY[1] - 10, 0xFFA0A0A0);
- guiGraphics.text(this.font, Component.translatable("ui.coffeeproxy.options.proxyType").getString(),
- positionX, positionY[2] - 10, 0xFFA0A0A0);
- guiGraphics.centeredText(this.font,
- Component.translatable("ui.coffeeproxy.options.auth").getString(), this.width / 2, positionY[4] + 8,
- 0xFFFFFFFF);
- guiGraphics.text(this.font, Component.translatable("ui.coffeeproxy.options.ipPort").getString(),
- positionX, positionY[3] - 10, 0xFFA0A0A0);
-
- this.name.extractWidgetRenderState(guiGraphics, mouseX, mouseY, partialTicks);
- this.ipPort.extractWidgetRenderState(guiGraphics, mouseX, mouseY, partialTicks);
- if (currentType == Proxy.ProxyType.SOCKS4) {
- guiGraphics.text(this.font, Component.translatable("ui.coffeeproxy.auth.id").getString(),
- positionX, positionY[5] - 10, 0xFFA0A0A0);
- this.username.extractWidgetRenderState(guiGraphics, mouseX, mouseY, partialTicks);
- } else {
- guiGraphics.text(this.font, Component.translatable("ui.coffeeproxy.auth.password").getString(),
- positionX, positionY[6] - 10, 0xFFA0A0A0);
- guiGraphics.text(this.font, Component.translatable("ui.coffeeproxy.auth.username").getString(),
- positionX, positionY[5] - 10, 0xFFA0A0A0);
- this.username.extractWidgetRenderState(guiGraphics, mouseX, mouseY, partialTicks);
- this.password.extractWidgetRenderState(guiGraphics, mouseX, mouseY, partialTicks);
- }
-
- guiGraphics.centeredText(this.font, !msg.isEmpty() ? msg : testPing.state, this.width / 2,
- positionY[7] + 5, 0xFFA0A0A0);
- *///?}
}
@Override
diff --git a/src/main/java/coffee/axle/proxy/GuiProxyList.java b/src/main/java/coffee/axle/proxy/GuiProxyList.java
index bd1e178..db86359 100644
--- a/src/main/java/coffee/axle/proxy/GuiProxyList.java
+++ b/src/main/java/coffee/axle/proxy/GuiProxyList.java
@@ -139,11 +139,7 @@ public void render(GuiGraphics guiGraphics, int mouseX, int mouseY, float partia
this.deleteButton.active = selected != null;
this.editButton.active = selected != null;
- //? if <26 {
- guiGraphics.drawCenteredString(this.font, TEXT_TITLE, this.width / 2, 8, 0xFFFFFFFF);
- //?} else {
- /*guiGraphics.centeredText(this.font, TEXT_TITLE, this.width / 2, 8, 0xFFFFFFFF);
- *///?}
+ Draw.centeredText(guiGraphics, this.font, TEXT_TITLE, this.width / 2, 8, 0xFFFFFFFF);
}
@Override
@@ -297,30 +293,16 @@ public boolean mouseClicked(MouseButtonEvent event, boolean doubleClick) {
}
//?}
- //? if <26 {
private void drawRow(GuiGraphics guiGraphics) {
int color = rowColor();
String ping = pingText();
- guiGraphics.drawString(GuiProxyList.this.font, this.name, this.rowLeft + 2, this.rowTop + 2, color);
- guiGraphics.drawString(GuiProxyList.this.font, this.proxy.ipPort, this.rowLeft + 2, this.rowTop + 13, 0xFF808080);
+ Draw.text(guiGraphics, GuiProxyList.this.font, this.name, this.rowLeft + 2, this.rowTop + 2, color);
+ Draw.text(guiGraphics, GuiProxyList.this.font, this.proxy.ipPort, this.rowLeft + 2, this.rowTop + 13, 0xFF808080);
int iconX = this.rowLeft + this.rowWidth - 14;
ResourceLocation sprite = ResourceLocation.withDefaultNamespace("server_list/" + pingSprite());
guiGraphics.blitSprite(RenderPipelines.GUI_TEXTURED, sprite,
iconX, this.rowTop + 2, 10, 8);
- guiGraphics.drawString(GuiProxyList.this.font, ping, iconX - 2 - GuiProxyList.this.font.width(ping), this.rowTop + 2, color);
+ Draw.text(guiGraphics, GuiProxyList.this.font, ping, iconX - 2 - GuiProxyList.this.font.width(ping), this.rowTop + 2, color);
}
- //?} else {
- /*private void drawRow(GuiGraphics guiGraphics) {
- int color = rowColor();
- String ping = pingText();
- guiGraphics.text(GuiProxyList.this.font, this.name, this.rowLeft + 2, this.rowTop + 2, color);
- guiGraphics.text(GuiProxyList.this.font, this.proxy.ipPort, this.rowLeft + 2, this.rowTop + 13, 0xFF808080);
- int iconX = this.rowLeft + this.rowWidth - 14;
- ResourceLocation sprite = ResourceLocation.withDefaultNamespace("server_list/" + pingSprite());
- guiGraphics.blitSprite(RenderPipelines.GUI_TEXTURED, sprite,
- iconX, this.rowTop + 2, 10, 8);
- guiGraphics.text(GuiProxyList.this.font, ping, iconX - 2 - GuiProxyList.this.font.width(ping), this.rowTop + 2, color);
- }
- *///?}
}
}
From fe5a1db5ef1e0b2eb9cf2f668ab1ac4071a83f7f Mon Sep 17 00:00:00 2001
From: axlecoffee <95949634+axlecoffee@users.noreply.github.com>
Date: Thu, 8 Oct 2026 06:37:30 +0000
Subject: [PATCH 7/9] minor cleanup
---
src/main/java/coffee/axle/proxy/GuiProxyList.java | 1 -
src/main/java/coffee/axle/proxy/Proxy.java | 13 +++++--------
2 files changed, 5 insertions(+), 9 deletions(-)
diff --git a/src/main/java/coffee/axle/proxy/GuiProxyList.java b/src/main/java/coffee/axle/proxy/GuiProxyList.java
index db86359..ae7fc61 100644
--- a/src/main/java/coffee/axle/proxy/GuiProxyList.java
+++ b/src/main/java/coffee/axle/proxy/GuiProxyList.java
@@ -229,7 +229,6 @@ private String pingText() {
return "";
}
- // vanilla ServerSelectionList bands: <=150 full bars, then 300/600/1000
private String pingSprite() {
long l = this.testPing.latency;
if (l < 0) {
diff --git a/src/main/java/coffee/axle/proxy/Proxy.java b/src/main/java/coffee/axle/proxy/Proxy.java
index 45e66dc..cc07207 100644
--- a/src/main/java/coffee/axle/proxy/Proxy.java
+++ b/src/main/java/coffee/axle/proxy/Proxy.java
@@ -99,16 +99,13 @@ public InetSocketAddress resolveProxyAddress() {
return cachedAddress;
}
+ private String meowEmpty(String value, String fallback) { return value.isEmpty() ? fallback : value; }
+
public ProxyHandler getHandler() {
return switch (type) {
- case SOCKS5 -> new Socks5ProxyHandler(resolveProxyAddress(),
- username.isEmpty() ? null : username,
- password.isEmpty() ? null : password);
- case SOCKS4 -> new Socks4ProxyHandler(resolveProxyAddress(),
- username.isEmpty() ? null : username);
- case HTTP -> new HttpProxyHandler(resolveProxyAddress(),
- username.isEmpty() ? null : username,
- password.isEmpty() ? "" : password);
+ case SOCKS5 -> new Socks5ProxyHandler(resolveProxyAddress(), meowEmpty(username, null), meowEmpty(password, null));
+ case SOCKS4 -> new Socks4ProxyHandler(resolveProxyAddress(), meowEmpty(username, null));
+ case HTTP -> new HttpProxyHandler(resolveProxyAddress(), meowEmpty(username, null), meowEmpty(password, ""));
};
}
From 579d0fca08df96e41be59666c429bb3f48094a4c Mon Sep 17 00:00:00 2001
From: axlecoffee <95949634+axlecoffee@users.noreply.github.com>
Date: Thu, 8 Oct 2026 09:54:37 +0000
Subject: [PATCH 8/9] import/export
---
.../coffee/axle/proxy/GuiImportExport.java | 227 ++++++++++++++++++
.../java/coffee/axle/proxy/GuiProxyList.java | 9 +-
.../assets/coffeeproxy/lang/en_us.json | 14 +-
3 files changed, 247 insertions(+), 3 deletions(-)
create mode 100644 src/main/java/coffee/axle/proxy/GuiImportExport.java
diff --git a/src/main/java/coffee/axle/proxy/GuiImportExport.java b/src/main/java/coffee/axle/proxy/GuiImportExport.java
new file mode 100644
index 0000000..a640c48
--- /dev/null
+++ b/src/main/java/coffee/axle/proxy/GuiImportExport.java
@@ -0,0 +1,227 @@
+// SPDX-FileCopyrightText: 2026 Axle Duggan (axlecoffee)
+//
+// SPDX-License-Identifier: AGPL-3.0-or-later
+package coffee.axle.proxy;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+import net.minecraft.ChatFormatting;
+import net.minecraft.client.gui.GuiGraphics;
+import net.minecraft.client.gui.components.Button;
+import net.minecraft.client.gui.components.EditBox;
+import net.minecraft.client.gui.screens.Screen;
+import net.minecraft.network.chat.Component;
+
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse.BodyHandlers;
+import java.nio.charset.StandardCharsets;
+import java.time.Duration;
+import java.util.Base64;
+import java.util.LinkedHashMap;
+
+public class GuiImportExport extends Screen {
+ private static final HttpClient HTTP = HttpClient.newBuilder()
+ .connectTimeout(Duration.ofSeconds(10)).build();
+
+ private static final String TEXT_TITLE = Component.translatable("ui.coffeeproxy.io.title").getString();
+ private static final String TEXT_UNDEFINED = ChatFormatting.RED
+ + Component.translatable("ui.coffeeproxy.io.undefined").getString();
+ private static final String TEXT_FETCHING = Component.translatable("ui.coffeeproxy.io.fetching").getString();
+ private static final String TEXT_FETCH_ERROR = ChatFormatting.RED
+ + Component.translatable("ui.coffeeproxy.io.fetchError").getString();
+ private static final String TEXT_COPIED = ChatFormatting.GREEN
+ + Component.translatable("ui.coffeeproxy.io.copied").getString();
+
+ private final Screen parentScreen;
+
+ private EditBox urlBox;
+ private Button urlButton;
+ private Button saveOverwrite;
+ private Button saveAppend;
+
+ private String msg = "";
+ private LinkedHashMap pending;
+ // written by the fetch thread, consumed on the tick (main) thread
+ private volatile String fetched;
+ private volatile boolean fetchFailed;
+ private boolean fetching;
+
+ public GuiImportExport(Screen parentScreen) {
+ super(Component.literal(TEXT_TITLE));
+ this.parentScreen = parentScreen;
+ }
+
+ @Override
+ public void init() {
+ int w = 160;
+ int x = (this.width - w) / 2;
+ int y0 = (this.height - 180) / 2;
+ String savedUrl = this.urlBox == null ? "" : this.urlBox.getValue();
+
+ this.urlBox = new EditBox(this.font, x, y0 + 24, w, 20, Component.literal(""));
+ this.urlBox.setValue(savedUrl);
+ this.urlBox.setMaxLength(1024);
+ this.addWidget(this.urlBox);
+
+ this.addRenderableWidget(Button.builder(Component.translatable("ui.coffeeproxy.io.clipboard"),
+ button -> applyPayload(this.minecraft.keyboardHandler.getClipboard()))
+ .bounds(x, y0, w, 20).build());
+
+ this.urlButton = this.addRenderableWidget(Button.builder(Component.translatable("ui.coffeeproxy.io.url"),
+ button -> fetchUrl())
+ .bounds(x, y0 + 48, w, 20).build());
+ this.urlButton.active = !this.fetching;
+
+ this.saveOverwrite = this.addRenderableWidget(Button
+ .builder(Component.translatable("ui.coffeeproxy.io.overwrite"), button -> save(true))
+ .bounds(x, y0 + 88, w, 20).build());
+ this.saveAppend = this.addRenderableWidget(Button
+ .builder(Component.translatable("ui.coffeeproxy.io.append"), button -> save(false))
+ .bounds(x, y0 + 112, w, 20).build());
+ refreshSaves();
+
+ this.addRenderableWidget(Button.builder(Component.translatable("ui.coffeeproxy.io.export"),
+ button -> exportClipboard())
+ .bounds(x, y0 + 136, w, 20).build());
+
+ this.addRenderableWidget(Button.builder(Component.translatable("ui.coffeeproxy.list.done"),
+ button -> onClose())
+ .bounds(x, y0 + 160, w, 20).build());
+ }
+
+ // clipboard is either raw json ({ at pos1) or base64 of it, anything else is Undefined!
+ private static LinkedHashMap parsePayload(String text) {
+ try {
+ String json = text == null ? "" : text.trim();
+ if (!json.startsWith("{")) {
+ json = new String(Base64.getDecoder().decode(json), StandardCharsets.UTF_8);
+ }
+ JsonElement proxies = JsonParser.parseString(json).getAsJsonObject().get("proxies");
+ if (proxies == null || !proxies.isJsonObject()) {
+ return null;
+ }
+ LinkedHashMap out = new LinkedHashMap<>();
+ for (var entry : proxies.getAsJsonObject().entrySet()) {
+ out.put(entry.getKey(), entry.getValue().getAsString());
+ }
+ return out;
+ } catch (Exception e) {
+ return null;
+ }
+ }
+
+ private void applyPayload(String text) {
+ LinkedHashMap parsed = parsePayload(text);
+ if (parsed == null) {
+ this.pending = null;
+ this.msg = TEXT_UNDEFINED;
+ } else {
+ this.pending = parsed;
+ this.msg = ChatFormatting.GREEN
+ + Component.translatable("ui.coffeeproxy.io.loaded", parsed.size()).getString();
+ }
+ refreshSaves();
+ }
+
+ private void fetchUrl() {
+ String url = this.urlBox.getValue().trim();
+ this.fetching = true;
+ this.urlButton.active = false;
+ this.msg = TEXT_FETCHING;
+ new Thread(() -> {
+ String body = "";
+ boolean failed = false;
+ try {
+ HttpRequest request = HttpRequest.newBuilder(URI.create(url))
+ .timeout(Duration.ofSeconds(10))
+ .GET().build();
+ body = HTTP.send(request, BodyHandlers.ofString()).body();
+ } catch (Exception e) {
+ // bad url / timeout / refused
+ failed = true;
+ }
+ this.fetchFailed = failed;
+ this.fetched = body;
+ }, "CoffeeProxy importer").start();
+ }
+
+ private void refreshSaves() {
+ this.saveOverwrite.active = this.pending != null;
+ this.saveAppend.active = this.pending != null;
+ }
+
+ private void save(boolean overwrite) {
+ if (this.pending == null) {
+ return;
+ }
+ if (overwrite) {
+ Config.proxies.clear();
+ Config.proxies.putAll(this.pending);
+ Config.setActive(Config.proxies.containsKey(Config.activeName) ? Config.activeName : "");
+ } else {
+ // colliding names get a number so re-importing the same list duplicates instead of replaces
+ for (var entry : this.pending.entrySet()) {
+ String name = entry.getKey();
+ for (int n = 2; Config.proxies.containsKey(name); n++) {
+ name = entry.getKey() + " " + n;
+ }
+ Config.proxies.put(name, entry.getValue());
+ }
+ }
+ Config.saveConfig();
+ Coffeeproxy.openScreen(this.parentScreen);
+ }
+
+ private void exportClipboard() {
+ // the exported list doubles as the selected payload, so export -> save round trips without another click
+ this.pending = new LinkedHashMap<>(Config.proxies);
+ Gson gson = new GsonBuilder().setPrettyPrinting().create();
+ JsonObject root = new JsonObject();
+ root.add("proxies", gson.toJsonTree(this.pending));
+ String b64 = Base64.getEncoder().encodeToString(gson.toJson(root).getBytes(StandardCharsets.UTF_8));
+ this.minecraft.keyboardHandler.setClipboard(b64);
+ this.msg = TEXT_COPIED;
+ refreshSaves();
+ }
+
+ @Override
+ public void tick() {
+ if (this.fetched == null) {
+ return;
+ }
+ String text = this.fetched;
+ this.fetched = null;
+ this.fetching = false;
+ this.urlButton.active = true;
+ if (this.fetchFailed) {
+ this.pending = null;
+ this.msg = TEXT_FETCH_ERROR;
+ refreshSaves();
+ } else {
+ applyPayload(text);
+ }
+ }
+
+ @Override
+ //? if <26 {
+ public void render(GuiGraphics guiGraphics, int mouseX, int mouseY, float partialTicks) {
+ super.render(guiGraphics, mouseX, mouseY, partialTicks);
+ //?} else {
+ /*public void extractRenderState(GuiGraphics guiGraphics, int mouseX, int mouseY, float partialTicks) {
+ super.extractRenderState(guiGraphics, mouseX, mouseY, partialTicks);
+ *///?}
+ int y0 = (this.height - 180) / 2;
+ Draw.centeredText(guiGraphics, this.font, this.msg, this.width / 2, y0 + 74, 0xFFA0A0A0);
+ Draw.widget(guiGraphics, this.urlBox, mouseX, mouseY, partialTicks);
+ }
+
+ @Override
+ public void onClose() {
+ Coffeeproxy.openScreen(this.parentScreen);
+ }
+}
diff --git a/src/main/java/coffee/axle/proxy/GuiProxyList.java b/src/main/java/coffee/axle/proxy/GuiProxyList.java
index ae7fc61..cf84ecf 100644
--- a/src/main/java/coffee/axle/proxy/GuiProxyList.java
+++ b/src/main/java/coffee/axle/proxy/GuiProxyList.java
@@ -46,6 +46,11 @@ public void init() {
this.proxyList.addProxy(entry.getKey(), entry.getValue());
}
+ this.addRenderableWidget(Button
+ .builder(Component.translatable("ui.coffeeproxy.list.importExport"),
+ button -> Coffeeproxy.openScreen(new GuiImportExport(this)))
+ .bounds(8, 8, 100, 20).build());
+
int x1 = this.width / 2 - 154;
int x2 = x1 + 104;
int x3 = x1 + 208;
@@ -139,7 +144,7 @@ public void render(GuiGraphics guiGraphics, int mouseX, int mouseY, float partia
this.deleteButton.active = selected != null;
this.editButton.active = selected != null;
- Draw.centeredText(guiGraphics, this.font, TEXT_TITLE, this.width / 2, 8, 0xFFFFFFFF);
+ Draw.centeredText(guiGraphics, this.font, TEXT_TITLE, this.width / 2, 14, 0xFFFFFFFF);
}
@Override
@@ -151,7 +156,7 @@ public void onClose() {
private class ProxyList extends ObjectSelectionList {
ProxyList(Minecraft minecraft) {
- super(minecraft, GuiProxyList.this.width, GuiProxyList.this.height - 116, 28, 36);
+ super(minecraft, GuiProxyList.this.width, GuiProxyList.this.height - 124, 36, 36);
}
@Override
diff --git a/src/main/resources/assets/coffeeproxy/lang/en_us.json b/src/main/resources/assets/coffeeproxy/lang/en_us.json
index a3dfd25..4ed4eb3 100644
--- a/src/main/resources/assets/coffeeproxy/lang/en_us.json
+++ b/src/main/resources/assets/coffeeproxy/lang/en_us.json
@@ -16,6 +16,7 @@
"ui.coffeeproxy.list.refresh": "Refresh All",
"ui.coffeeproxy.list.add": "Add",
"ui.coffeeproxy.list.edit": "Edit",
+ "ui.coffeeproxy.list.importExport": "Import/Export",
"ui.coffeeproxy.list.done": "Done",
"ui.coffeeproxy.list.failed": "Failed",
"ui.coffeeproxy.auth.id": "User ID: ",
@@ -27,5 +28,16 @@
"ui.coffeeproxy.err.cantPing": "Can't ping %d",
"ui.coffeeproxy.err.cantPingReason": "Can't ping %d: %d",
"ui.coffeeproxy.ping.pinging": "Pinging %d...",
- "ui.coffeeproxy.ping.showPing": "Ping: %dms"
+ "ui.coffeeproxy.ping.showPing": "Ping: %dms",
+ "ui.coffeeproxy.io.title": "Import/Export",
+ "ui.coffeeproxy.io.clipboard": "Import from Clipboard",
+ "ui.coffeeproxy.io.url": "Import from URL",
+ "ui.coffeeproxy.io.overwrite": "Save (overwrite)",
+ "ui.coffeeproxy.io.append": "Save (append)",
+ "ui.coffeeproxy.io.export": "Export to Clipboard",
+ "ui.coffeeproxy.io.fetching": "Fetching...",
+ "ui.coffeeproxy.io.loaded": "Loaded %d Proxies!",
+ "ui.coffeeproxy.io.undefined": "Undefined!",
+ "ui.coffeeproxy.io.fetchError": "Failed to fetch from URL",
+ "ui.coffeeproxy.io.copied": "Copied to clipboard!"
}
\ No newline at end of file
From 7fb15a147cf7623f83340046601f8218e2cfc45e Mon Sep 17 00:00:00 2001
From: axlecoffee <95949634+axlecoffee@users.noreply.github.com>
Date: Thu, 8 Oct 2026 09:58:00 +0000
Subject: [PATCH 9/9] cleanup
---
src/main/java/coffee/axle/proxy/GuiImportExport.java | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/src/main/java/coffee/axle/proxy/GuiImportExport.java b/src/main/java/coffee/axle/proxy/GuiImportExport.java
index a640c48..2ab3ebe 100644
--- a/src/main/java/coffee/axle/proxy/GuiImportExport.java
+++ b/src/main/java/coffee/axle/proxy/GuiImportExport.java
@@ -46,7 +46,6 @@ public class GuiImportExport extends Screen {
private String msg = "";
private LinkedHashMap pending;
- // written by the fetch thread, consumed on the tick (main) thread
private volatile String fetched;
private volatile boolean fetchFailed;
private boolean fetching;
@@ -142,7 +141,9 @@ private void fetchUrl() {
.GET().build();
body = HTTP.send(request, BodyHandlers.ofString()).body();
} catch (Exception e) {
+ // TODO: display error CODE?
// bad url / timeout / refused
+ e.printStackTrace();
failed = true;
}
this.fetchFailed = failed;
@@ -164,7 +165,7 @@ private void save(boolean overwrite) {
Config.proxies.putAll(this.pending);
Config.setActive(Config.proxies.containsKey(Config.activeName) ? Config.activeName : "");
} else {
- // colliding names get a number so re-importing the same list duplicates instead of replaces
+ // duplicate names get number appended since it's just logical
for (var entry : this.pending.entrySet()) {
String name = entry.getKey();
for (int n = 2; Config.proxies.containsKey(name); n++) {
@@ -178,7 +179,6 @@ private void save(boolean overwrite) {
}
private void exportClipboard() {
- // the exported list doubles as the selected payload, so export -> save round trips without another click
this.pending = new LinkedHashMap<>(Config.proxies);
Gson gson = new GsonBuilder().setPrettyPrinting().create();
JsonObject root = new JsonObject();