feat: WIP: multiplayer

This commit is contained in:
huanghongxun 2021-09-06 00:45:25 +08:00
parent c844a77f02
commit d521d10a85
13 changed files with 394 additions and 1 deletions

View File

@ -44,6 +44,7 @@ mainClassName = 'org.jackhuang.hmcl.Main'
dependencies {
implementation project(":HMCLCore")
implementation project(":JSTUN")
implementation rootProject.files("lib/JFoenix.jar")
}

View File

@ -48,6 +48,7 @@ import org.jackhuang.hmcl.ui.download.DownloadPage;
import org.jackhuang.hmcl.ui.download.ModpackInstallWizardProvider;
import org.jackhuang.hmcl.ui.main.LauncherSettingsPage;
import org.jackhuang.hmcl.ui.main.RootPage;
import org.jackhuang.hmcl.ui.multiplayer.MultiplayerPage;
import org.jackhuang.hmcl.ui.versions.GameListPage;
import org.jackhuang.hmcl.ui.versions.ModDownloadListPage;
import org.jackhuang.hmcl.ui.versions.VersionPage;
@ -101,6 +102,7 @@ public final class Controllers {
accountListPage.accountsProperty().bindContent(Accounts.accountsProperty());
return accountListPage;
});
private static Lazy<MultiplayerPage> multiplayerPage = new Lazy<>(MultiplayerPage::new);
private static Lazy<LauncherSettingsPage> settingsPage = new Lazy<>(LauncherSettingsPage::new);
private Controllers() {
@ -141,6 +143,11 @@ public final class Controllers {
return modDownloadListPage.get();
}
// FXThread
public static MultiplayerPage getMultiplayerPage() {
return multiplayerPage.get();
}
// FXThread
public static LauncherSettingsPage getSettingsPage() {
return settingsPage.get();

View File

@ -391,6 +391,12 @@ public final class SVG {
fill, width, height);
}
public static Node accountArrowRightOutline(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath(
"M19,21V19H15V17H19V15L22,18L19,21M13,18C13,18.71 13.15,19.39 13.42,20H2V17C2,14.79 5.58,13 10,13C11,13 11.96,13.09 12.85,13.26C13.68,13.42 14.44,13.64 15.11,13.92C13.83,14.83 13,16.32 13,18M4,17V18H11C11,16.96 11.23,15.97 11.64,15.08L10,15C6.69,15 4,15.9 4,17M10,4A4,4 0 0,1 14,8A4,4 0 0,1 10,12A4,4 0 0,1 6,8A4,4 0 0,1 10,4M10,6A2,2 0 0,0 8,8A2,2 0 0,0 10,10A2,2 0 0,0 12,8A2,2 0 0,0 10,6Z",
fill, width, height);
}
public static Node styleOutline(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath(
"M2.5 19.6L3.8 20.2V11.2L1.4 17C1 18.1 1.5 19.2 2.5 19.6M15.2 4.8L20.2 16.8L12.9 19.8L7.9 7.9V7.8L15.2 4.8M15.3 2.8C15 2.8 14.8 2.8 14.5 2.9L7.1 6C6.4 6.3 5.9 7 5.9 7.8C5.9 8 5.9 8.3 6 8.6L11 20.5C11.3 21.3 12 21.7 12.8 21.7C13.1 21.7 13.3 21.7 13.6 21.6L21 18.5C22 18.1 22.5 16.9 22.1 15.9L17.1 4C16.8 3.2 16 2.8 15.3 2.8M10.5 9.9C9.9 9.9 9.5 9.5 9.5 8.9S9.9 7.9 10.5 7.9C11.1 7.9 11.5 8.4 11.5 8.9S11.1 9.9 10.5 9.9M5.9 19.8C5.9 20.9 6.8 21.8 7.9 21.8H9.3L5.9 13.5V19.8Z",

View File

@ -107,6 +107,7 @@ public abstract class ToolbarListPageSkin<T extends ListPageBase<? extends Node>
ret.setOnMouseClicked(e -> onClick.run());
return ret;
}
public static JFXButton createDecoratorButton(String tooltip, SVG.SVGIcon creator, Runnable onClick) {
JFXButton ret = new JFXButton();
ret.getStyleClass().add("jfx-decorator-button");

View File

@ -176,6 +176,14 @@ public class RootPage extends DecoratorTabPage {
downloadItem.setOnAction(e -> Controllers.navigate(Controllers.getDownloadPage()));
// fifth item in left sidebar
AdvancedListItem multiplayerItem = new AdvancedListItem();
multiplayerItem
.setLeftGraphic(AdvancedListItem.createImageView(newImage("/assets/img/command.png")).getKey());
multiplayerItem.setActionButtonVisible(false);
multiplayerItem.setTitle(i18n("multiplayer"));
multiplayerItem.setOnAction(e -> Controllers.navigate(Controllers.getMultiplayerPage()));
// sixth item in left sidebar
AdvancedListItem launcherSettingsItem = new AdvancedListItem();
launcherSettingsItem
.setLeftGraphic(AdvancedListItem.createImageView(newImage("/assets/img/command.png")).getKey());
@ -191,6 +199,8 @@ public class RootPage extends DecoratorTabPage {
.add(gameListItem)
.add(gameItem)
.add(downloadItem)
.startCategory(i18n("settings.launcher.general").toLowerCase())
// .add(multiplayerItem)
.add(launcherSettingsItem);
// the root page, with the sidebar in left, navigator in center.

View File

@ -0,0 +1,27 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.multiplayer;
public class MultiplayerManager {
enum State {
DISCONNECTED,
MASTER,
SLAVE
}
}

View File

@ -0,0 +1,86 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.multiplayer;
import de.javawi.jstun.test.DiscoveryInfo;
import de.javawi.jstun.test.DiscoveryTest;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.control.Control;
import javafx.scene.control.Skin;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class MultiplayerPage extends Control implements DecoratorPage {
private final ReadOnlyObjectWrapper<State> state = new ReadOnlyObjectWrapper<>(State.fromTitle(i18n("multiplayer"), -1));
private final ObjectProperty<MultiplayerManager.State> multiplayerState = new SimpleObjectProperty<>();
private final ReadOnlyObjectWrapper<DiscoveryInfo> natState = new ReadOnlyObjectWrapper<>();
public MultiplayerPage() {
testNAT();
}
@Override
protected Skin<?> createDefaultSkin() {
return new MultiplayerPageSkin(this);
}
public MultiplayerManager.State getMultiplayerState() {
return multiplayerState.get();
}
public ObjectProperty<MultiplayerManager.State> multiplayerStateProperty() {
return multiplayerState;
}
public void setMultiplayerState(MultiplayerManager.State multiplayerState) {
this.multiplayerState.set(multiplayerState);
}
public DiscoveryInfo getNatState() {
return natState.get();
}
public ReadOnlyObjectProperty<DiscoveryInfo> natStateProperty() {
return natState.getReadOnlyProperty();
}
public void testNAT() {
Task.supplyAsync(() -> {
DiscoveryTest tester = new DiscoveryTest(null, 0, "stun.qq.com", 3478);
return tester.test();
}).whenComplete(Schedulers.javafx(), (info, exception) -> {
if (exception == null) {
natState.set(info);
} else {
natState.set(null);
}
}).start();
}
@Override
public ReadOnlyObjectProperty<State> stateProperty() {
return state;
}
}

View File

@ -0,0 +1,174 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.multiplayer;
import de.javawi.jstun.test.DiscoveryInfo;
import javafx.geometry.Insets;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.SkinBase;
import javafx.scene.layout.*;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.util.javafx.BindingMapping;
import static org.jackhuang.hmcl.ui.versions.VersionPage.wrap;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class MultiplayerPageSkin extends SkinBase<MultiplayerPage> {
/**
* Constructor for all SkinBase instances.
*
* @param control The control for which this Skin should attach to.
*/
protected MultiplayerPageSkin(MultiplayerPage control) {
super(control);
BorderPane root = new BorderPane();
root.setPadding(new Insets(10));
getChildren().setAll(root);
{
VBox roomPane = new VBox();
{
AdvancedListItem createRoomItem = new AdvancedListItem();
createRoomItem.setTitle(i18n("multiplayer.room.create"));
createRoomItem.setLeftGraphic(wrap(SVG.plusCircleOutline(null, 24, 24)));
createRoomItem.setOnAction(e -> FXUtils.openLink(""));
AdvancedListItem joinRoomItem = new AdvancedListItem();
joinRoomItem.setTitle(i18n("multiplayer.room.join"));
joinRoomItem.setLeftGraphic(wrap(SVG.accountArrowRightOutline(null, 24, 24)));
joinRoomItem.setOnAction(e -> FXUtils.openLink(""));
AdvancedListItem copyLinkItem = new AdvancedListItem();
copyLinkItem.setTitle(i18n("multiplayer.room.copy_room_code"));
copyLinkItem.setLeftGraphic(wrap(SVG.accountArrowRightOutline(null, 24, 24)));
copyLinkItem.setOnAction(e -> FXUtils.openLink(""));
AdvancedListItem quitItem = new AdvancedListItem();
quitItem.setTitle(i18n("multiplayer.room.quit"));
quitItem.setLeftGraphic(wrap(SVG.closeCircle(null, 24, 24)));
quitItem.setOnAction(e -> FXUtils.openLink(""));
AdvancedListItem closeRoomItem = new AdvancedListItem();
closeRoomItem.setTitle(i18n("multiplayer.room.quit"));
closeRoomItem.setLeftGraphic(wrap(SVG.closeCircle(null, 24, 24)));
closeRoomItem.setOnAction(e -> FXUtils.openLink(""));
FXUtils.onChangeAndOperate(getSkinnable().multiplayerStateProperty(), state -> {
if (state == MultiplayerManager.State.DISCONNECTED) {
roomPane.getChildren().setAll(createRoomItem, joinRoomItem);
} else if (state == MultiplayerManager.State.MASTER) {
roomPane.getChildren().setAll(copyLinkItem);
roomPane.getChildren().setAll(closeRoomItem);
} else if (state == MultiplayerManager.State.SLAVE) {
roomPane.getChildren().setAll(copyLinkItem);
roomPane.getChildren().setAll(quitItem);
}
});
}
AdvancedListBox sideBar = new AdvancedListBox()
.startCategory("multiplayer.room")
.add(roomPane)
.startCategory("help")
.addNavigationDrawerItem(settingsItem -> {
settingsItem.setTitle(i18n("help"));
settingsItem.setLeftGraphic(wrap(SVG.gamepad(null, 20, 20)));
settingsItem.setOnAction(e -> FXUtils.openLink(""));
});
FXUtils.setLimitWidth(sideBar, 200);
root.setLeft(sideBar);
}
{
VBox content = new VBox(16);
content.setFillWidth(true);
ScrollPane scrollPane = new ScrollPane(content);
scrollPane.setFitToWidth(true);
scrollPane.setFitToHeight(true);
root.setCenter(scrollPane);
ComponentList roomPane = new ComponentList();
{
VBox pane = new VBox();
}
ComponentList natDetectionPane = new ComponentList();
{
GridPane pane = new GridPane();
ColumnConstraints title = new ColumnConstraints();
ColumnConstraints value = new ColumnConstraints();
pane.getColumnConstraints().setAll(title, value);
value.setFillWidth(true);
value.setHgrow(Priority.ALWAYS);
pane.setHgap(16);
pane.setVgap(8);
HintPane hintPane = new HintPane(MessageDialogPane.MessageType.INFORMATION);
hintPane.setText(i18n("multiplayer.nat.hint"));
GridPane.setColumnSpan(hintPane, 2);
pane.addRow(0, hintPane);
Label natResult = new Label();
natResult.textProperty().bind(BindingMapping.of(getSkinnable().natStateProperty())
.map(MultiplayerPageSkin::getNATType));
pane.addRow(1, new Label(i18n("multiplayer.nat.type")), natResult);
// Label natResult = new Label();
// natResult.textProperty().bind(BindingMapping.of(getSkinnable().natStateProperty())
// .map(MultiplayerPageSkin::getNATType));
// pane.addRow(1, new Label(i18n("multiplayer.nat.latency")), natResult);
natDetectionPane.getContent().add(pane);
}
content.getChildren().setAll(
ComponentList.createComponentListTitle(i18n("multiplayer.room")),
roomPane,
ComponentList.createComponentListTitle(i18n("multiplayer.nat")),
natDetectionPane
);
}
}
private static String getNATType(DiscoveryInfo info) {
if (info == null) {
return i18n("multiplayer.nat.testing");
} else if (info.isBlockedUDP()) {
return i18n("multiplayer.nat.type.blocked_udp");
} else if (info.isFullCone()) {
return i18n("multiplayer.nat.type.full_cone");
} else if (info.isOpenAccess()) {
return i18n("multiplayer.nat.type.open_access");
} else if (info.isPortRestrictedCone()) {
return i18n("multiplayer.nat.type.port_restricted_cone");
} else if (info.isRestrictedCone()) {
return i18n("multiplayer.nat.type.restricted_cone");
} else if (info.isSymmetric()) {
return i18n("multiplayer.nat.type.symmetric");
} else if (info.isSymmetricUDPFirewall()) {
return i18n("multiplayer.nat.type.symmetric_udp_firewall");
} else {
return i18n("multiplayer.nat.type.unknown");
}
}
}

View File

@ -450,6 +450,35 @@ mods.name=Name
mods.not_modded=You should install a modloader first (Fabric, Forge or LiteLoader)
mods.url=Official Page
multiplayer=Multiplayer
multiplayer.nat=Network Type Detection
multiplayer.nat.hint=Network type detection will make it clear whether your network fulfills our requirement for multiplayer mode.
multiplayer.nat.latency=Latency
multiplayer.nat.not_yet_tested=Not yet testsed
multiplayer.nat.packet_loss_ratio=Packet Loss Ratio
multiplayer.nat.testing=Testing
multiplayer.nat.type=NAT Type
multiplayer.nat.type.blocked_udp=Very bad (Blocked UDP)
multiplayer.nat.type.full_cone=Medium (Full Cone)
multiplayer.nat.type.open_access=Good (Open Access)
multiplayer.nat.type.port_restricted_cone=Medium (Port Restricted Cone)
multiplayer.nat.type.restricted_cone=Medium (Restricted Cone)
multiplayer.nat.type.symmetric=Bad (Symmetric)
multiplayer.nat.type.symmetric_udp_firewall=Bad (Symmetric with UDP Firewall)
multiplayer.nat.type.unknown=Unknownn
multiplayer.room=Room
multiplayer.room.name.format=%1$s's Room
multiplayer.room.close=Close Room
multiplayer.room.copy_room_code=Copy Invitation Code
multiplayer.room.create=Create Room
multiplayer.room.error.port=Cannot detect game port, you must click "Open LAN Server" in game to enable multiplayer functionality.
multiplayer.room.hint=You must click "Open LAN Server" in game in order to enable multiplayer functionality.
multiplayer.room.join=Join Room
multiplayer.room.members=Room Members
multiplayer.room.port.prompt=Enter game port
multiplayer.room.quit=Quit Room
multiplayer.room.username=Username
datapack=Datapacks
datapack.add=Install datapack
datapack.choose_datapack=Choose the datapack zip to import

View File

@ -426,6 +426,35 @@ mods.mangage=模組管理
mods.name=名稱
mods.not_modded=你需要先在自動安裝頁面安裝 Fabric、Forge 或 LiteLoader 才能進行模組管理。
multiplayer=聯機
multiplayer.nat=網路檢測
multiplayer.nat.hint=執行網路檢測可以讓你更清楚裡的網路狀況是否符合聯機功能的需求。不符合聯機功能運行條件的網路狀況將可能導致聯機失敗。
multiplayer.nat.latency=延遲
multiplayer.nat.not_yet_tested=尚未檢測
multiplayer.nat.packet_loss_ratio=丟包率
multiplayer.nat.testing=檢測中
multiplayer.nat.type=NAT 類型
multiplayer.nat.type.blocked_udp=極差(網路禁止 UDP 協議)
multiplayer.nat.type.full_cone=中(完全圓錐型)
multiplayer.nat.type.open_access=好(公網開放型)
multiplayer.nat.type.port_restricted_cone=中(埠受限圓錐型)
multiplayer.nat.type.restricted_cone=中(受限圓錐型)
multiplayer.nat.type.symmetric=差(對稱型)
multiplayer.nat.type.symmetric_udp_firewall=差(對稱型+防火牆)
multiplayer.nat.type.unknown=未知
multiplayer.room=房間
multiplayer.room.name.format=%1$s 的房間
multiplayer.room.close=關閉房間
multiplayer.room.copy_room_code=複製邀請碼
multiplayer.room.create=創建房間
multiplayer.room.error.port=無法檢測遊戲埠號,你必須先啟動遊戲並在遊戲內打開對區域網路開放選項後才能啟動聯機。
multiplayer.room.hint=你必須先啟動遊戲並在遊戲內打開對區域網路開放選項後才能創建房間
multiplayer.room.join=加入房間
multiplayer.room.members=房間成員
multiplayer.room.port.prompt=輸入埠號
multiplayer.room.quit=退出房間
multiplayer.room.username=使用者名稱
datapack=資料包
datapack.add=加入資料包
datapack.choose_datapack=選擇要匯入的資料包壓縮檔

View File

@ -469,7 +469,26 @@ multiplayer.nat.not_yet_tested=尚未检测
multiplayer.nat.packet_loss_ratio=丢包率
multiplayer.nat.testing=检测中
multiplayer.nat.type=NAT 类型
multiplayer.nat.type.blocked_udp=极差(网络禁止 UDP 协议)
multiplayer.nat.type.full_cone=中(完全圆锥型)
multiplayer.nat.type.open_access=好(公网开放型)
multiplayer.nat.type.port_restricted_cone=中(端口受限圆锥型)
multiplayer.nat.type.restricted_cone=中(受限圆锥型)
multiplayer.nat.type.symmetric=差(对称型)
multiplayer.nat.type.symmetric_udp_firewall=差(对称型+防火墙)
multiplayer.nat.type.unknown=未知
multiplayer.room=房间
multiplayer.room.name.format=%1$s 的房间
multiplayer.room.close=关闭房间
multiplayer.room.copy_room_code=复制邀请码
multiplayer.room.create=创建房间
multiplayer.room.error.port=无法检测游戏端口号,你必须先启动游戏并在游戏内打开对局域网开放选项后才能启动联机。
multiplayer.room.hint=你必须先启动游戏并在游戏内打开对局域网开放选项后才能创建房间
multiplayer.room.join=加入房间
multiplayer.room.members=房间成员
multiplayer.room.port.prompt=输入端口号
multiplayer.room.quit=退出房间
multiplayer.room.username=用户名
datapack=数据包
datapack.add=添加数据包

View File

@ -28,6 +28,10 @@ subprojects {
sourceSets = []
}
tasks.withType(Checkstyle) {
exclude 'de/javawi/jstun'
}
sourceCompatibility = 1.8
compileJava.options.encoding = "UTF-8"
compileTestJava.options.encoding = "UTF-8"

View File

@ -2,4 +2,4 @@ rootProject.name = 'HMCL3'
include ':HMCL'
include ':HMCLCore'
include ':HMCLTransformerDiscoveryService'
include ':JSTUN'