/*
* Copyright 2015 MovingBlocks
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.terasology.rendering.nui.layers.mainMenu;
import com.google.common.base.Joiner;
import com.google.common.base.Predicate;
import com.google.common.collect.Collections2;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.terasology.assets.ResourceUrn;
import org.terasology.config.Config;
import org.terasology.config.ServerInfo;
import org.terasology.engine.GameEngine;
import org.terasology.engine.GameThread;
import org.terasology.engine.modes.StateLoading;
import org.terasology.engine.module.ModuleManager;
import org.terasology.i18n.TranslationSystem;
import org.terasology.input.Keyboard;
import org.terasology.module.ModuleRegistry;
import org.terasology.naming.NameVersion;
import org.terasology.network.JoinStatus;
import org.terasology.network.NetworkSystem;
import org.terasology.network.PingService;
import org.terasology.network.ServerInfoMessage;
import org.terasology.network.ServerInfoService;
import org.terasology.registry.In;
import org.terasology.rendering.FontColor;
import org.terasology.rendering.nui.Color;
import org.terasology.rendering.nui.CoreScreenLayer;
import org.terasology.rendering.nui.WidgetUtil;
import org.terasology.rendering.nui.animation.MenuAnimationSystems;
import org.terasology.rendering.nui.databinding.BindHelper;
import org.terasology.rendering.nui.databinding.IntToStringBinding;
import org.terasology.rendering.nui.databinding.ReadOnlyBinding;
import org.terasology.rendering.nui.events.NUIKeyEvent;
import org.terasology.rendering.nui.itemRendering.StringTextRenderer;
import org.terasology.rendering.nui.layouts.CardLayout;
import org.terasology.rendering.nui.widgets.ActivateEventListener;
import org.terasology.rendering.nui.widgets.UIButton;
import org.terasology.rendering.nui.widgets.UILabel;
import org.terasology.rendering.nui.widgets.UIList;
import org.terasology.world.internal.WorldInfo;
import org.terasology.world.time.WorldTime;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
/**
*/
public class JoinGameScreen extends CoreScreenLayer {
public static final ResourceUrn ASSET_URI = new ResourceUrn("engine:joinGameScreen");
private static final Logger logger = LoggerFactory.getLogger(JoinGameScreen.class);
@In
private Config config;
@In
private NetworkSystem networkSystem;
@In
private GameEngine engine;
@In
private ModuleManager moduleManager;
@In
private TranslationSystem translationSystem;
private Map<ServerInfo, Future<ServerInfoMessage>> extInfo = new HashMap<>();
private ServerInfoService infoService;
private ServerListDownloader downloader;
private UIList<ServerInfo> visibleList;
private List<ServerInfo> listedServers = new ArrayList<>();
private Predicate<ServerInfo> activeServersOnly = ServerInfo::isActive;
private boolean updateComplete;
@Override
public void initialise() {
setAnimationSystem(MenuAnimationSystems.createDefaultSwipeAnimation());
downloader = new ServerListDownloader(config.getNetwork().getMasterServer());
CardLayout cards = find("cards", CardLayout.class);
UIList<ServerInfo> customServerList = find("customServerList", UIList.class);
if (customServerList != null) {
customServerList.setList(config.getNetwork().getServerInfos());
configureServerList(customServerList);
}
UIList<ServerInfo> onlineServerList = find("onlineServerList", UIList.class);
if (onlineServerList != null) {
onlineServerList.setList(listedServers);
configureServerList(onlineServerList);
}
ActivateEventListener activateCustom = e -> {
cards.setDisplayedCard("customServerListScrollArea");
find("customButton", UIButton.class).setFamily("highlight");
find("onlineButton", UIButton.class).setFamily("default");
visibleList = customServerList;
};
WidgetUtil.trySubscribe(this, "customButton", activateCustom);
ActivateEventListener activateOnline = e -> {
cards.setDisplayedCard("onlineServerListScrollArea");
find("customButton", UIButton.class).setFamily("default");
find("onlineButton", UIButton.class).setFamily("highlight");
visibleList = onlineServerList;
};
WidgetUtil.trySubscribe(this, "onlineButton", activateOnline);
bindCustomButtons();
bindInfoLabels();
WidgetUtil.trySubscribe(this, "close", button -> {
config.save();
triggerBackAnimation();
});
activateOnline.onActivated(null);
}
@Override
public void onOpened() {
super.onOpened();
infoService = new ServerInfoService();
if (!config.getPlayer().hasEnteredUsername()) {
getManager().pushScreen(EnterUsernamePopup.ASSET_URI, EnterUsernamePopup.class);
}
}
@Override
public void update(float delta) {
super.update(delta);
if (!updateComplete) {
if (downloader.isDone()) {
updateComplete = true;
}
listedServers.clear();
listedServers.addAll(Collections2.filter(downloader.getServers(), activeServersOnly));
}
}
@Override
public void onClosed() {
infoService.close();
super.onClosed();
}
@Override
public boolean isLowerLayerVisible() {
return false;
}
private void join(final String address, final int port) {
Callable<JoinStatus> operation = () -> {
JoinStatus joinStatus = networkSystem.join(address, port);
return joinStatus;
};
final WaitPopup<JoinStatus> popup = getManager().pushScreen(WaitPopup.ASSET_URI, WaitPopup.class);
popup.setMessage(translationSystem.translate("${engine:menu#join-game-online}"),
translationSystem.translate("${engine:menu#connecting-to}")
+ " '"
+ address
+ ":"
+ port
+ "' - "
+ translationSystem.translate("${engine:menu#please-wait}"));
popup.onSuccess(result -> {
if (result.getStatus() != JoinStatus.Status.FAILED) {
engine.changeState(new StateLoading(result));
} else {
MessagePopup screen = getManager().pushScreen(MessagePopup.ASSET_URI, MessagePopup.class);
screen.setMessage(translationSystem.translate("${engine:menu#failed-to-join}"),
translationSystem.translate("${engine:menu#could-not-connect-to-server}") + " - " + result.getErrorMessage());
}
});
popup.startOperation(operation, true);
}
private void configureServerList(final UIList<ServerInfo> serverList) {
serverList.subscribe((widget, item) -> join(item.getAddress(), item.getPort()));
serverList.subscribeSelection((widget, item) -> {
extInfo.remove(item);
if (item != null) {
extInfo.put(item, infoService.requestInfo(item.getAddress(), item.getPort()));
refreshPing();
}
});
serverList.setItemRenderer(new StringTextRenderer<ServerInfo>() {
@Override
public String getString(ServerInfo value) {
return value.getName();
}
});
}
private void bindInfoLabels() {
final ReadOnlyBinding<ServerInfo> infoBinding = new ReadOnlyBinding<ServerInfo>() {
@Override
public ServerInfo get() {
return visibleList.getSelection();
}
};
UILabel name = find("name", UILabel.class);
if (name != null) {
name.bindText(BindHelper.bindBoundBeanProperty("name", infoBinding, ServerInfo.class, String.class));
}
UILabel owner = find("owner", UILabel.class);
if (owner != null) {
owner.bindText(BindHelper.bindBoundBeanProperty("owner", infoBinding, ServerInfo.class, String.class));
}
UILabel address = find("address", UILabel.class);
if (address != null) {
address.bindText(BindHelper.bindBoundBeanProperty("address", infoBinding, ServerInfo.class, String.class));
}
UILabel port = find("port", UILabel.class);
if (port != null) {
port.bindText(new IntToStringBinding(BindHelper.bindBoundBeanProperty("port", infoBinding, ServerInfo.class, int.class)));
}
UILabel modules = find("modules", UILabel.class);
modules.bindText(new ReadOnlyBinding<String>() {
@Override
public String get() {
Future<ServerInfoMessage> info = extInfo.get(visibleList.getSelection());
if (info != null) {
if (info.isDone()) {
return getModulesText(info);
} else {
return translationSystem.translate("${engine:menu#join-server-requested}");
}
}
return null;
}
});
UILabel worlds = find("worlds", UILabel.class);
worlds.bindText(new ReadOnlyBinding<String>() {
@Override
public String get() {
Future<ServerInfoMessage> info = extInfo.get(visibleList.getSelection());
if (info != null) {
if (info.isDone()) {
return getWorldText(info);
} else {
return translationSystem.translate("${engine:menu#join-server-requested}");
}
}
return null;
}
});
UIButton joinButton = find("join", UIButton.class);
if (joinButton != null) {
joinButton.bindEnabled(new ReadOnlyBinding<Boolean>() {
@Override
public Boolean get() {
return infoBinding.get() != null;
}
});
joinButton.subscribe(button -> {
config.save();
ServerInfo item = infoBinding.get();
if (item != null) {
join(item.getAddress(), item.getPort());
}
});
}
UIButton refreshButton = find("refresh", UIButton.class);
if (refreshButton != null) {
refreshButton.bindEnabled(new ReadOnlyBinding<Boolean>() {
@Override
public Boolean get() {
return visibleList.getSelection() != null;
}
});
refreshButton.subscribe(button -> {
refresh();
});
}
}
private void bindCustomButtons() {
UIList<?> customServerList = find("customServerList", UIList.class);
ReadOnlyBinding<Boolean> localSelectedServerOnly = new ReadOnlyBinding<Boolean>() {
@Override
public Boolean get() {
return customServerList.getSelection() != null;
}
};
UIButton add = find("add", UIButton.class);
if (add != null) {
add.subscribe(button -> {
AddServerPopup popup = getManager().pushScreen(AddServerPopup.ASSET_URI, AddServerPopup.class);
// select the entry if added successfully
popup.onSuccess(item -> {
config.getNetwork().addServerInfo(item);
visibleList.setSelection(item);
});
});
}
UIButton edit = find("edit", UIButton.class);
if (edit != null) {
edit.bindEnabled(localSelectedServerOnly);
edit.subscribe(button -> {
AddServerPopup popup = getManager().pushScreen(AddServerPopup.ASSET_URI, AddServerPopup.class);
ServerInfo info = visibleList.getSelection();
popup.setServerInfo(info);
// editing invalidates the currently known info, so query it again
popup.onSuccess(item -> extInfo.put(item, infoService.requestInfo(item.getAddress(), item.getPort())));
});
}
UIButton removeButton = find("remove", UIButton.class);
if (removeButton != null) {
removeButton.bindEnabled(localSelectedServerOnly);
removeButton.subscribe(button -> {
ServerInfo info = visibleList.getSelection();
if (info != null) {
config.getNetwork().removeServerInfo(info);
extInfo.remove(info);
visibleList.setSelection(null);
}
});
}
UILabel downloadLabel = find("download", UILabel.class);
if (downloadLabel != null) {
downloadLabel.bindText(new ReadOnlyBinding<String>() {
@Override
public String get() {
return translationSystem.translate(downloader.getStatus());
}
});
}
}
private String getWorldText(Future<ServerInfoMessage> info) {
try {
List<String> codedWorldInfo = new ArrayList<>();
for (WorldInfo wi : info.get().getWorldInfoList()) {
float timeInDays = wi.getTime() / (float) WorldTime.DAY_LENGTH;
codedWorldInfo.add(String.format("%s (%.2f days)", wi.getTitle(), timeInDays));
}
return Joiner.on('\n').join(codedWorldInfo);
} catch (ExecutionException | InterruptedException e) {
return FontColor.getColored(translationSystem.translate("${engine:menu#connection-failed}"), Color.RED);
}
}
private String getModulesText(Future<ServerInfoMessage> info) {
try {
ServerInfoMessage serverInfoMessage = info.get();
List<String> codedModInfo = new ArrayList<>();
ModuleRegistry reg = moduleManager.getRegistry();
for (NameVersion entry : serverInfoMessage.getModuleList()) {
boolean isInstalled = reg.getModule(entry.getName(), entry.getVersion()) != null;
Color color = isInstalled ? Color.GREEN : Color.RED;
codedModInfo.add(FontColor.getColored(entry.toString(), color));
}
Collections.sort(codedModInfo, String.CASE_INSENSITIVE_ORDER);
return Joiner.on('\n').join(codedModInfo);
} catch (ExecutionException | InterruptedException e) {
return FontColor.getColored(translationSystem.translate("${engine:menu#connection-failed}"), Color.RED);
}
}
private void refreshPing() {
String address = visibleList.getSelection().getAddress();
int port = visibleList.getSelection().getPort();
UILabel ping = find("ping", UILabel.class);
ping.setText("Requested");
Thread getPing = new Thread(() -> {
PingService pingService = new PingService(address, port);
// we're not on the game thread, so we cannot modify GUI elements directly
try {
long responseTime = pingService.call();
if (visibleList.getSelection().getAddress().equals(address)) {
GameThread.asynch(() -> ping.setText(responseTime + " ms."));
}
} catch (IOException e) {
String text = translationSystem.translate("${engine:menu#connection-failed}");
GameThread.asynch(() -> ping.setText(FontColor.getColored(text, Color.RED)));
}
});
// TODO: once the common thread pool is in place this could be posted there and the
// returned Future could be kept and cancelled as soon the selected menu entry changes
getPing.start();
}
public boolean onKeyEvent(NUIKeyEvent event) {
if (event.isDown() && event.getKey() == Keyboard.Key.R) {
refresh();
}
return false;
}
public void refresh() {
ServerInfo i = visibleList.getSelection();
visibleList.setSelection(null);
extInfo.clear();
visibleList.setSelection(i);
}
}