/*
* This file is part of the Illarion project.
*
* Copyright © 2015 - Illarion e.V.
*
* Illarion is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Illarion 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.
*/
package illarion.client;
import de.lessvoid.nifty.Nifty;
import de.lessvoid.nifty.spi.time.impl.AccurateTimeProvider;
import illarion.client.graphics.FontLoader;
import illarion.client.input.InputReceiver;
import illarion.client.states.*;
import illarion.client.util.ConnectionPerformanceClock;
import illarion.client.util.Lang;
import illarion.client.world.World;
import illarion.common.config.ConfigChangedEvent;
import org.bushe.swing.event.annotation.AnnotationProcessor;
import org.bushe.swing.event.annotation.EventTopicSubscriber;
import org.illarion.engine.GameContainer;
import org.illarion.engine.GameListener;
import org.illarion.engine.assets.TextureManager;
import org.illarion.engine.graphic.Color;
import org.illarion.engine.graphic.Font;
import org.illarion.engine.input.ForwardingListener;
import org.illarion.engine.input.ForwardingTarget;
import org.illarion.engine.nifty.IgeInputSystem;
import org.illarion.engine.nifty.IgeRenderDevice;
import org.illarion.engine.nifty.IgeSoundDevice;
import org.illarion.engine.sound.Sounds;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.io.IOException;
import java.util.Properties;
/**
* This is the game Illarion. This class takes care for actually building up Illarion. It will maintain the different
* states of the game and allow switching them.
*
* @author Martin Karing <nitram@illarion.org>
*/
public final class Game implements GameListener {
/**
* The ID of no state. In case this "state" is chosen the game will not display anything.
*/
public static final int STATE_NONE = -1;
/**
* The ID of the login state. This is one of the constants to use in order to switch the current state of the game.
*/
public static final int STATE_LOGIN = 0;
/**
* The ID of the loading state. This state can be used in order to display the current loading progress.
*/
public static final int STATE_LOADING = 1;
/**
* The ID of the playing state. This can be used in order to display the current game.
*/
public static final int STATE_PLAYING = 2;
/**
* The ID of the ending state. This displays the last screen before the shutdown.
*/
public static final int STATE_ENDING = 3;
/**
* The ID of the ending state. This displays the last screen before the shutdown.
*/
public static final int STATE_LOGOUT = 4;
/**
* The ID of the disconnected state. This state is displayed in case the client lost the connection to the server.
* This may happen due to the server shutting down the connection or by the connection being interrupted.
*/
public static final int STATE_DISCONNECT = 5;
@Nullable
private Nifty nifty;
@Nonnull
private final GameState[] gameStates;
private int activeListener = STATE_NONE;
private int targetListener = STATE_NONE;
private boolean showFPS;
private boolean showPing;
/**
* Create the game with the fitting title, showing the name of the application and its version.
*/
public Game() {
gameStates = new GameState[6];
AnnotationProcessor.process(this);
showFPS = IllaClient.getCfg().getBoolean("showFps");
showPing = IllaClient.getCfg().getBoolean("showPing");
}
/**
* Sets the next game state to the given state, if between -1 and 5
* Game will advance to the given state upon the next call of update()
* @param stateId the state to enter. Using a class constant is recommended for readability.
*/
public void enterState(int stateId) {
if ((stateId >= -1) && (stateId < gameStates.length)) {
targetListener = stateId;
} else {
throw new IllegalArgumentException("Illegal stateId: " + stateId);
}
}
@Nullable
private GameState getCurrentState() {
if ((activeListener >= 0) && (activeListener < gameStates.length)) {
return gameStates[activeListener];
}
return null;
}
private static final Logger LOGGER = LoggerFactory.getLogger(Game.class);
/**
* Initializes fields and prepares the Game for launch
* Enters the login state when finished
* @param container the game container
*/
@Override
public void create(@Nonnull GameContainer container) {
TextureManager texManager = container.getEngine().getAssets().getTextureManager();
texManager.addTextureDirectory("gui");
texManager.addTextureDirectory("chars");
texManager.addTextureDirectory("items");
texManager.addTextureDirectory("tiles");
texManager.addTextureDirectory("effects");
try {
FontLoader.getInstance().prepareAllFonts(container.getEngine().getAssets());
} catch (@Nonnull IOException e) {
LOGGER.error("Error while loading fonts!", e);
}
InputReceiver inputReceiver = new InputReceiver(container.getEngine().getInput());
// Prepare the game's Nifty and its properties
nifty = new Nifty(new IgeRenderDevice(container, "gui/"), new IgeSoundDevice(container.getEngine()),
new IgeInputSystem(container.getEngine().getInput(), inputReceiver),
new AccurateTimeProvider());
Properties niftyProperties = nifty.getGlobalProperties();
if (niftyProperties == null) {
niftyProperties = new Properties();
nifty.setGlobalProperties(niftyProperties);
}
niftyProperties.setProperty("MULTI_CLICK_TIME",
Integer.toString(IllaClient.getCfg().getInteger("doubleClickInterval")));
nifty.setLocale(Lang.getInstance().getLocale());
container.getEngine().getInput().addForwardingListener(new ForwardingListener() {
@Override
public void forwardingEnabledFor(@Nonnull ForwardingTarget target) {
// nothing
}
@Override
public void forwardingDisabledFor(@Nonnull ForwardingTarget target) {
if ((target == ForwardingTarget.Mouse) || (target == ForwardingTarget.All)) {
nifty.resetMouseInputEvents();
}
}
});
gameStates[STATE_LOGIN] = new LoginState();
gameStates[STATE_LOADING] = new LoadingState();
gameStates[STATE_PLAYING] = new PlayingState(inputReceiver);
gameStates[STATE_ENDING] = new EndState();
gameStates[STATE_LOGOUT] = new LogoutState();
gameStates[STATE_DISCONNECT] = new DisconnectedState();
// Prepare the sounds and music for use, set volume based on the current configuration settings
Sounds sounds = container.getEngine().getSounds();
if (IllaClient.getCfg().getBoolean("musicOn")) {
sounds.setMusicVolume(IllaClient.getCfg().getFloat("musicVolume") / 100.f);
} else {
sounds.setMusicVolume(0.f);
}
if (IllaClient.getCfg().getBoolean("soundOn")) {
sounds.setSoundVolume(IllaClient.getCfg().getFloat("soundVolume") / 100.f);
} else {
sounds.setSoundVolume(0.f);
}
for (@Nonnull GameState listener : gameStates) {
listener.create(this, container, nifty);
}
enterState(STATE_LOGIN);
}
/**
* Returns the state associated with the given index
* @param index the ID of the state to use, between 0 and {@code gameStates.length - 1}
* @return the state associated with the given index, if a valid index
*/
@Nonnull
public GameState getState(int index) {
if ((index < 0) || (index >= gameStates.length)) {
throw new IndexOutOfBoundsException(String.format("Index is expected between 0 and %d, got: %d",
gameStates.length - 1, index));
}
return gameStates[index];
}
@Nonnull
public <T extends GameState> T getState(@Nonnull Class<T> clazz, int index) {
GameState state = getState(index);
if (clazz.isAssignableFrom(state.getClass())) {
//noinspection unchecked
return (T) state;
}
throw new IllegalArgumentException(
String.format("Requested state contains the class %s but the expected class was: %s",
state.getClass().getName(), clazz.getName()));
}
@Nonnull
public <T extends GameState> T getState(@Nonnull Class<T> clazz) {
for (int i = 0; i < gameStates.length; i++) {
GameState state = getState(i);
if (clazz.isAssignableFrom(state.getClass())) {
//noinspection unchecked
return (T) state;
}
}
throw new IllegalArgumentException(
String.format("Failed to locate any state with the requested class: %s",
clazz.getName()));
}
/**
* Sets this instance's nifty to null, disposes of each GameState
*/
@Override
public void dispose() {
nifty = null;
for (@Nonnull GameState listener : gameStates) {
listener.dispose();
}
}
/**
* Changes the container's dimensions
* Sets the Client's config's window height and width to the new dimensions
*
* @param container the game container
* @param width the new width
* @param height the new height
*/
@Override
public void resize(@Nonnull GameContainer container, int width, int height) {
IllaClient.getCfg().set("windowHeight", height);
IllaClient.getCfg().set("windowWidth", width);
if (nifty != null) {
nifty.resolutionChanged();
}
GameState activeListener = getCurrentState();
if (activeListener != null) {
activeListener.resize(container, width, height);
}
}
/**
* During the call of this function the application is supposed to perform the update of the game logic.
*
* If the Game is not in the state given by the last call of enterState(), enters that state
* Updates the Nifty gui for the (now current) state
*
* @param container the game container
* @param delta the time since the last update call
*/
@Override
public void update(@Nonnull GameContainer container, int delta) {
assert nifty != null;
if (targetListener != activeListener) {
GameState activeState = getCurrentState();
if (activeState != null) {
activeState.leaveState(container);
}
activeListener = targetListener;
GameState newState = getCurrentState();
if (newState != null) {
newState.enterState(container, nifty);
}
}
nifty.update();
container.getEngine().getSounds().poll(delta);
GameState activeListener = getCurrentState();
if (activeListener != null) {
activeListener.update(container, delta);
}
}
/**
* Perform all rendering operations
* If more diagnostic data should be shown, add to this method
* @param container the game container
*/
@Override
public void render(@Nonnull GameContainer container) {
assert nifty != null;
GameState activeListener = getCurrentState();
if (activeListener != null) {
activeListener.render(container);
}
nifty.render(false);
if (showFPS || showPing) {
Font fpsFont = container.getEngine().getAssets().getFontManager().getFont(FontLoader.CONSOLE_FONT);
if (fpsFont != null) {
// Only show render data unless on devserver, testerver, or a custom server
boolean showRenderDiagnostic = IllaClient.DEFAULT_SERVER != Servers.Illarionserver;
int renderLine = 10;
if (showFPS) {
container.getEngine().getGraphics()
.drawText(fpsFont, "FPS: " + container.getFPS(), Color.WHITE, 10, renderLine);
renderLine += fpsFont.getLineHeight();
if (showRenderDiagnostic) {
for (CharSequence line : container.getDiagnosticLines()) {
container.getEngine().getGraphics().drawText(fpsFont, line, Color.WHITE, 10, renderLine);
renderLine += fpsFont.getLineHeight();
}
}
}
if (showRenderDiagnostic && World.isInitDone()) {
String tileLine = "Tile count: " + World.getMap().getTileCount();
container.getEngine().getGraphics().drawText(fpsFont, tileLine, Color.WHITE, 10, renderLine);
renderLine += fpsFont.getLineHeight();
String sceneLine = "Scene objects: " + World.getMapDisplay().getGameScene().getElementCount();
container.getEngine().getGraphics().drawText(fpsFont, sceneLine, Color.WHITE, 10, renderLine);
renderLine += fpsFont.getLineHeight();
}
if (showPing) {
long serverPing = ConnectionPerformanceClock.getServerPing();
long netCommPing = ConnectionPerformanceClock.getNetCommPing();
if (serverPing > -1) {
container.getEngine().getGraphics().drawText(fpsFont, "Ping: " + serverPing + '+' +
Math.max(0, netCommPing - serverPing) + " ms", Color.WHITE, 10, renderLine);
renderLine += fpsFont.getLineHeight();
}
}
// If more diagnostics are wanted, add them here
}
}
}
@EventTopicSubscriber(topic = "showFps")
public void onFpsConfigChanged(@Nonnull String topic, @Nonnull ConfigChangedEvent event) {
showFPS = event.getConfig().getBoolean(event.getKey());
}
@EventTopicSubscriber(topic = "showPing")
public void onPingConfigChanged(@Nonnull String topic, @Nonnull ConfigChangedEvent event) {
showPing = event.getConfig().getBoolean(event.getKey());
}
/**
* This function is called in case the game receives a request to be closed.
*
* @return {@code true} in case the game is supposed to shutdown, else the closing request is rejected
*/
@Override
public boolean isClosingGame() {
GameState activeListener = getCurrentState();
if (activeListener != null) {
return activeListener.isClosingGame();
}
return true; // According to the interface, default reply is false. Consider rewriting docs there or this method.
}
}