package onlinefrontlines.lobby;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Random;
import java.util.Calendar;
import java.util.Collection;
import java.sql.SQLException;
import java.awt.Point;
import org.apache.log4j.Logger;
import onlinefrontlines.Army;
import onlinefrontlines.Constants;
import onlinefrontlines.auth.User;
import onlinefrontlines.auth.UserCache;
import onlinefrontlines.game.CountryConfig;
import onlinefrontlines.game.CountryConfigCache;
import onlinefrontlines.game.Faction;
import onlinefrontlines.game.GameStateDAO;
import onlinefrontlines.lobby.actions.*;
import onlinefrontlines.userstats.UserStatsDAO;
import onlinefrontlines.utils.HexagonGrid;
import onlinefrontlines.utils.Tools;
import onlinefrontlines.utils.CacheException;
/**
* Runtime state of a lobby
*
* @author jorrit
*
* Copyright (C) 2009-2013 Jorrit Rouwe
*
* This file is part of Online Frontlines.
*
* Online Frontlines 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.
*
* Online Frontlines 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 Online Frontlines. If not, see <http://www.gnu.org/licenses/>.
*/
public class LobbyState
{
private static final Logger log = Logger.getLogger(LobbyState.class);
/**
* Max amount of messages to keep
*/
public static final int MAX_MESSAGE_COUNT = 100;
/**
* Lobby configuration
*/
public final LobbyConfig lobbyConfig;
/**
* All countries in this lobby
*/
private Country[][] countries;
/**
* Last time one of the countries changed
*/
private int countriesChangeCount = 0;
/**
* All active and timed out users
*/
private HashMap<Integer, LobbyUser> users = new HashMap<Integer, LobbyUser>();
/**
* Last time one of the users changed
*/
private int usersChangeCount = 0;
/**
* All text messages
*/
private ArrayList<TextMessage> textMessages = new ArrayList<TextMessage>();
/**
* Last time one of the messages changed
*/
private int textMessageChangeCount = 0;
/**
* Counter that is increased everytime something in lobby changes,
* this is used to determine which data to send to each client
*/
private int currentChangeCount = 0;
/**
* Time at which all countries were conquered by one faction
*/
public long allConqueredTime = Long.MAX_VALUE;
/**
* Tiles that can be defended by a particular army
* @see determineDefendableCountries
*/
private static class DefendableCountries
{
public int distanceToEnemy[][];
public int requiredDistance;
}
/**
* Defendable countries per army
*/
DefendableCountries defendableCountries[] = { null, null };
/**
* Time at which this lobby was created
*/
public final long creationTime = Calendar.getInstance().getTime().getTime();
/**
* Random generator
*/
public final Random random = new Random();
/**
* Constructor
*/
public LobbyState(LobbyConfig lobbyConfig) throws SQLException, CacheException
{
// Store lobby config
this.lobbyConfig = lobbyConfig;
// Create countries array
countries = new Country[lobbyConfig.sizeX][lobbyConfig.sizeY];
for (int y = 0; y < lobbyConfig.sizeY; ++y)
for (int x = 0; x < lobbyConfig.sizeX; ++x)
{
int id = lobbyConfig.getCountryConfigId(x, y);
if (id > 0)
{
CountryConfig countryConfig = CountryConfigCache.getInstance().get(id);
countries[x][y] = new Country(x, y, countryConfig, Army.none);
}
}
// Loop through current state of countries
for (LobbyCountryStateDAO.State state : LobbyCountryStateDAO.getCountryStates(lobbyConfig.id))
{
Country country = getCountry(state.locationX, state.locationY);
if (country != null)
{
// Copy state
country.ownerUserId = state.ownerUserId;
country.ownerExclusiveTime = state.ownerExclusiveTime;
country.army = state.army;
}
else
{
// Country no longer exists, remove it from the database
LobbyCountryStateDAO.removeCountryState(lobbyConfig.id, state.locationX, state.locationY);
// Create warning
log.warn("Could not restore country state at " + state.locationX + "," + state.locationY + " for lobby " + lobbyConfig.id);
}
}
// Countries that are not in the database yet get a random initial faction and are added to the database
ArrayList<Country> countriesToCreate = new ArrayList<Country>();
for (int y = 0; y < lobbyConfig.sizeY; ++y)
for (int x = 0; x < lobbyConfig.sizeX; ++x)
{
Country country = countries[x][y];
if (country != null && country.army == Army.none)
{
country.army = random.nextBoolean()? Army.red : Army.blue;
countriesToCreate.add(country);
}
}
if (!countriesToCreate.isEmpty())
LobbyCountryStateDAO.createCountryStates(lobbyConfig.id, countriesToCreate);
// Reload active games
for (GameStateDAO.GameForLobby game : GameStateDAO.getGamesForLobby(lobbyConfig.id))
{
Country attackedCountry = getCountry(game.attackedCountryX, game.attackedCountryY);
Country defendedCountry = getCountry(game.defendedCountryX, game.defendedCountryY);
if (attackedCountry != null && defendedCountry != null)
{
attackedCountry.currentGameId = game.gameId;
attackedCountry.currentGameUserId = game.attackerUserId;
defendedCountry.currentGameId = game.gameId;
defendedCountry.currentGameUserId = game.defenderUserId;
}
else
{
// Create warning
log.warn("Could not restore game in progress " + game.gameId + " for lobby " + lobbyConfig.id);
}
}
// Reload users
for (LobbyUserDAO.State state : LobbyUserDAO.getUsers(lobbyConfig.id))
{
User user = UserCache.getInstance().get(state.userId);
LobbyUser lobbyUser = new LobbyUser(this, user);
lobbyUser.army = state.army;
Country defendedCountry = getCountry(state.defendedCountryX, state.defendedCountryY);
Country attackedCountry = getCountry(state.attackedCountryX, state.attackedCountryY);
if (defendedCountry != null
&& attackedCountry != null
&& defendedCountry.currentGameId == 0
&& attackedCountry.currentGameId == 0)
{
lobbyUser.setCountries(defendedCountry, attackedCountry);
lobbyUser.hasAcceptedChallenge = state.hasAcceptedChallenge;
lobbyUser.challengeValidUntil = state.challengeValidUntil;
}
users.put(lobbyUser.userId, lobbyUser);
}
// Make sure that no users are missed because the lobby_users table was deleted
for (int y = 0; y < lobbyConfig.sizeY; ++y)
for (int x = 0; x < lobbyConfig.sizeX; ++x)
{
Country country = countries[x][y];
if (country != null)
{
ensureUserCreated(country.currentGameUserId, country.army);
ensureUserCreated(country.ownerUserId, country.army);
}
}
// Update state of capture points
checkCapturePoints();
// Check if all countries have been conquered
checkAllConquered();
// Reload messages
textMessages = LobbyChatDAO.getMessages(lobbyConfig.id, MAX_MESSAGE_COUNT);
// Make sure clients have a change count other than 0 to sync to
++currentChangeCount;
// Make sure challenges that should have been deleted are deleted
checkConnectedUsersTimeOut();
log.info("Created lobby " + lobbyConfig.id);
}
/**
* Helper function to make sure a user is created
*
* @param userId User id of user to create
* @param defaultUserArmy If user has no army, this army will be used
*/
private void ensureUserCreated(int userId, Army defaultUserArmy) throws SQLException, CacheException
{
if (userId != 0 && getLobbyUser(userId) == null)
{
User user = UserCache.getInstance().get(userId);
LobbyUser lobbyUser = new LobbyUser(this, user);
if (lobbyUser.army == Army.none)
lobbyUser.army = defaultUserArmy;
users.put(lobbyUser.userId, lobbyUser);
LobbyUserDAO.createOrUpdateUser(lobbyConfig.id, lobbyUser);
log.warn("Lobby user '" + userId + "' did not exist but is in use");
}
}
/**
* Get lobby id
*/
public int getLobbyId()
{
return lobbyConfig.id;
}
/**
* Get current change count
*/
public int getCurrentChangeCount()
{
return currentChangeCount;
}
/**
* Get a country
*/
public Country getCountry(int x, int y)
{
if (x >= 0 && y >= 0 && x < lobbyConfig.sizeX && y < lobbyConfig.sizeY)
return countries[x][y];
else
return null;
}
/**
* Mark that a country has changed
*/
public void notifyCountryChanged(Country country)
{
country.changeCount = currentChangeCount;
countriesChangeCount = currentChangeCount++;
try
{
LobbyCountryStateDAO.updateCountryState(lobbyConfig.id, country);
}
catch (SQLException e)
{
Tools.logException(e);
}
}
/**
* Get all users
*/
public Collection<LobbyUser> getUsers()
{
return users.values();
}
/**
* Get lobby user
*/
public LobbyUser getLobbyUser(int userId) throws SQLException
{
return users.get(userId);
}
/**
* Get or create lobby user
*/
public LobbyUser getOrCreateLobbyUser(User user)
{
// Check if already in the lobby
LobbyUser lobbyUser = users.get(user.id);
if (lobbyUser != null && lobbyUser.getIsConnected())
return lobbyUser;
// Check if there is room for another user
int userCount = 0;
for (LobbyUser u : users.values())
if (u.getIsConnected())
++userCount;
if (userCount >= lobbyConfig.maxUsers)
return null;
// Disconnected user found, return it
if (lobbyUser != null)
return lobbyUser;
// Add user to the lobby
lobbyUser = new LobbyUser(this, user);
if (user.army != Army.none)
{
lobbyUser.army = user.army;
}
else
{
int count[] = getLobbyUserCount();
int count_red = count[Army.toInt(Army.red)];
int count_blue = count[Army.toInt(Army.blue)];
if (count_red == count_blue)
lobbyUser.army = random.nextBoolean()? Army.red : Army.blue;
else if (count_red < count_blue)
lobbyUser.army = Army.red;
else
lobbyUser.army = Army.blue;
}
users.put(lobbyUser.userId, lobbyUser);
notifyLobbyUserChanged(lobbyUser);
return lobbyUser;
}
/**
* Mark one of the lobby users changed
*/
public void notifyLobbyUserChanged(LobbyUser lobbyUser)
{
// Update change count
lobbyUser.changeCount = currentChangeCount;
usersChangeCount = currentChangeCount++;
// Write new state to database
try
{
LobbyUserDAO.createOrUpdateUser(lobbyConfig.id, lobbyUser);
}
catch (SQLException e)
{
Tools.logException(e);
}
}
/**
* Look for users that timed out
*/
public void checkConnectedUsersTimeOut() throws SQLException
{
long time = Calendar.getInstance().getTime().getTime();
ArrayList<LobbyUser> toBeRemoved = new ArrayList<LobbyUser>();
for (LobbyUser u : users.values())
{
// Check if user still connected
u.checkConnectedTimeOut();
// Check if users challenge timed out
if (!u.hasAcceptedChallenge
&& u.challengeValidUntil != 0
&& u.challengeValidUntil < time)
{
try
{
// Cancel the challenge
Action cancel = new ActionCancel();
cancel.setLobbyState(this);
cancel.setLobbyUser(u);
cancel.doAction();
}
catch (Exception e)
{
Tools.logException(e);
}
}
// Check if user can be deleted
if (u.canBeRemoved())
toBeRemoved.add(u);
}
// Remove users
for (LobbyUser u : toBeRemoved)
{
// Remove from user list
users.remove(u.userId);
// Remove from database
LobbyUserDAO.removeUser(lobbyConfig.id, u.userId);
}
}
/**
* Get amount of users logged in of a particular army
*/
public int[] getLobbyUserCount()
{
int count[] = { 0, 0 };
for (LobbyUser u : users.values())
if (u.getIsConnected())
count[Army.toInt(u.army)]++;
return count;
}
/**
* Check if user is playing a game
*
* @param userId Id of user
*/
public boolean isUserInGame(int userId)
{
for (int y = 0; y < lobbyConfig.sizeY; ++y)
for (int x = 0; x < lobbyConfig.sizeX; ++x)
{
Country country = countries[x][y];
if (country != null
&& country.currentGameUserId == userId)
return true;
}
return false;
}
/**
* Check if user is playing a game or has conquered a country on the map
*
* @param userId Id of user
*/
public boolean isUserOnMap(int userId)
{
for (int y = 0; y < lobbyConfig.sizeY; ++y)
for (int x = 0; x < lobbyConfig.sizeX; ++x)
{
Country country = countries[x][y];
if (country != null
&& (country.currentGameUserId == userId
|| country.ownerUserId == userId))
return true;
}
return false;
}
/**
* Get a list of changed countries
*
* @param changeCount Point after which to get changes
*/
public ArrayList<Country> getChangedCountries(int changeCount)
{
ArrayList<Country> list = new ArrayList<Country>();
// Find changes
if (changeCount - countriesChangeCount <= 0)
for (int y = 0; y < lobbyConfig.sizeY; ++y)
for (int x = 0; x < lobbyConfig.sizeX; ++x)
{
Country c = countries[x][y];
if (c != null && changeCount - c.changeCount <= 0)
list.add(c);
}
return list;
}
/**
* Get a list of changed users
*
* @param changeCount Point after which to get changes
*/
public ArrayList<LobbyUser> getChangedUsers(int changeCount)
{
ArrayList<LobbyUser> list = new ArrayList<LobbyUser>();
// Find changes
if (changeCount - usersChangeCount <= 0)
{
for (LobbyUser u : users.values())
if (changeCount - u.changeCount <= 0)
list.add(u);
}
return list;
}
/**
* Add new text message
*
* @param message Message to add
*/
public void addTextMessage(TextMessage message) throws SQLException
{
LobbyChatDAO.addMessage(lobbyConfig.id, message);
message.changeCount = currentChangeCount;
textMessages.add(message);
if (textMessages.size() > MAX_MESSAGE_COUNT)
textMessages.remove(0);
textMessageChangeCount = currentChangeCount++;
}
/**
* Get changed text messages
*
* @param changeCount Point after which to get changes
*/
public ArrayList<TextMessage> getChangedTextMessages(int changeCount)
{
ArrayList<TextMessage> list = new ArrayList<TextMessage>();
// Find changes
if (changeCount - textMessageChangeCount <= 0)
{
for (TextMessage m : textMessages)
if (changeCount - m.changeCount <= 0)
list.add(m);
}
return list;
}
/**
* Called by the game end thread to notify that a game has ended
*/
public void notifyGameEnd(int attackerUserId, int defenderUserId, int attackedCountryX, int attackedCountryY, int defendedCountryX, int defendedCountryY, Faction winner) throws SQLException
{
// Get countries
Country attackedCountry = getCountry(attackedCountryX, attackedCountryY);
Country defendedCountry = getCountry(defendedCountryX, defendedCountryY);
if (attackedCountry == null || defendedCountry == null)
{
log.error("notifyGameEnd failed because country could not be found!");
return;
}
// Get attacker and defender
LobbyUser attacker = getLobbyUser(attackerUserId);
LobbyUser defender = getLobbyUser(defenderUserId);
if (attacker == null || defender == null)
{
log.error("notifyGameEnd failed because lobby user could not be found!");
return;
}
// End game in progress
attackedCountry.currentGameId = 0;
attackedCountry.currentGameUserId = 0;
defendedCountry.currentGameId = 0;
defendedCountry.currentGameUserId = 0;
// If friendly game, don't change colors
if (attacker.army != defender.army)
{
// Country changes faction, winner stays in the country
switch (winner)
{
case f1:
attackedCountry.army = attacker.army;
attackedCountry.ownerUserId = attacker.userId;
attackedCountry.ownerExclusiveTime = Calendar.getInstance().getTime().getTime() + Constants.EXCLUSIVE_TIME_AFTER_CONQUERED;
break;
case f2:
defendedCountry.army = defender.army;
defendedCountry.ownerUserId = defender.userId;
defendedCountry.ownerExclusiveTime = Calendar.getInstance().getTime().getTime() + Constants.EXCLUSIVE_TIME_AFTER_CONQUERED;
break;
}
}
// Notify of changes
notifyCountryChanged(attackedCountry);
notifyCountryChanged(defendedCountry);
invalidateDefendableCountries();
// Update state of capture points
checkCapturePoints();
// Check if all countries have been conquered
checkAllConquered();
}
/**
* Check state of all capture points
*/
private void checkCapturePoints()
{
// Loop through all countries
for (int y = 0; y < lobbyConfig.sizeY; ++y)
for (int x = 0; x < lobbyConfig.sizeX; ++x)
{
// Check if it is a capture point
Country c = countries[x][y];
if (c != null && c.countryConfig.isCapturePoint)
{
// Check if all neighbouring countries are of opposite faction
boolean allCountriesConquered = false;
ArrayList<Point> neighbours = lobbyConfig.getNeighbours(x, y);
for (Point n : neighbours)
{
Country nc = countries[n.x][n.y];
if (nc != null)
{
// There is at least one country neighbouring
allCountriesConquered = true;
if (nc.army == c.army)
{
// Country is of same army so not conquered yet
allCountriesConquered = false;
break;
}
}
}
// All countries are conquered
if (allCountriesConquered)
{
// Change state
c.army = Army.opposite(c.army);
c.ownerUserId = 0;
notifyCountryChanged(c);
// Award capture to all neighbours
for (Point n : neighbours)
{
Country nc = countries[n.x][n.y];
if (nc != null && nc.ownerUserId != 0)
{
try
{
// Accumulate user stats
UserStatsDAO.addCapture(nc.ownerUserId, lobbyConfig.id);
}
catch (SQLException e)
{
// Log error
Tools.logException(e);
}
}
}
}
}
}
}
/**
* Check if all countries have been conquered
*/
private void checkAllConquered()
{
int count[] = { 0, 0 };
// Loop through all countries
for (int y = 0; y < lobbyConfig.sizeY; ++y)
for (int x = 0; x < lobbyConfig.sizeX; ++x)
{
Country c = countries[x][y];
if (c != null && !c.countryConfig.isCapturePoint)
{
// Count ownership
count[Army.toInt(c.army)]++;
}
}
// Check if all countries have been conquered
if (count[0] == 0 || count[1] == 0)
{
allConqueredTime = Calendar.getInstance().getTime().getTime();
}
}
/**
* Randomize ownership of all countries
*/
public void randomizeAllCountries() throws SQLException
{
// Set all countries to random army
Random r = new Random();
for (int y = 0; y < lobbyConfig.sizeY; ++y)
for (int x = 0; x < lobbyConfig.sizeX; ++x)
{
Country country = countries[x][y];
if (country != null)
{
country.army = r.nextBoolean()? Army.red : Army.blue;
notifyCountryChanged(country);
}
}
// Reset all conquered time
allConqueredTime = Long.MAX_VALUE;
}
/**
* Helper class to determine defendable countries
*/
private static class ListEntry
{
int x;
int y;
int d;
public ListEntry(int x, int y, int d)
{
this.x = x;
this.y = y;
this.d = d;
}
}
/**
* Determine countries that are defendable
*
* After this call all countries for which distanceToEnemy[x][y] == requiredDistance
* are defendable (if the country is owned by the correct army and there is no other
* defender / attacker yet)
*/
private DefendableCountries determineDefendableCountries(Army army)
{
// Check if cached
DefendableCountries dc = defendableCountries[Army.toInt(army)];
if (dc != null)
return dc;
// Create new
dc = new DefendableCountries();
// Init array
dc.distanceToEnemy = new int[lobbyConfig.sizeX][lobbyConfig.sizeY];
dc.requiredDistance = 10000;
// Populate list with all countries that are attackable
ArrayList<ListEntry> list = new ArrayList<ListEntry>();
for (int y = 0; y < lobbyConfig.sizeY; ++y)
for (int x = 0; x < lobbyConfig.sizeX; ++x)
{
Country c = countries[x][y];
if (c != null
&& c.army != army
&& !c.countryConfig.isCapturePoint
&& c.currentGameId == 0
&& c.attacker == null)
{
dc.distanceToEnemy[x][y] = 0;
list.add(new ListEntry(x, y, 0));
}
else
{
dc.distanceToEnemy[x][y] = 10000;
}
}
// Loop while there are still countries left
while (list.size() > 0)
{
// Get first element from the list
ListEntry e = list.get(0);
int x = e.x;
int y = e.y;
int d = e.d;
list.remove(0);
// Check if other shorter path has been found
if (dc.distanceToEnemy[x][y] < d)
continue;
// Check if this is the shortest path to an attackable country so far
if (d < dc.requiredDistance)
{
Country c = countries[x][y];
if (c.army == army
&& !c.countryConfig.isCapturePoint
&& c.currentGameId == 0
&& c.defender == null)
dc.requiredDistance = d;
}
// Add all neighbours to the list to be searched
d++;
ArrayList<Point> neighbours = lobbyConfig.getNeighbours(x, y);
for (Point n : neighbours)
if (countries[n.x][n.y] != null
&& dc.distanceToEnemy[n.x][n.y] > d)
{
dc.distanceToEnemy[n.x][n.y] = d;
list.add(new ListEntry(n.x, n.y, d));
}
}
// Store result
defendableCountries[Army.toInt(army)] = dc;
return dc;
}
/**
* Invalidate cached defendable countries
*/
public void invalidateDefendableCountries()
{
defendableCountries[0] = null;
defendableCountries[1] = null;
}
/**
* Check if an attack from dcountry to acountry is possible
*/
public boolean isAttackPossible(Country dcountry, Country acountry, Army army)
{
DefendableCountries dc = determineDefendableCountries(army);
return dcountry != null
&& acountry != null
&& !dcountry.countryConfig.isCapturePoint
&& !acountry.countryConfig.isCapturePoint
&& dcountry.army == army
&& acountry.army != army
&& dcountry.currentGameId == 0
&& acountry.currentGameId == 0
&& dcountry.defender == null
&& dcountry.attacker == null
&& acountry.defender == null
&& acountry.attacker == null
&& dc.distanceToEnemy[dcountry.locationX][dcountry.locationY] == dc.requiredDistance
&& HexagonGrid.getDistance(dcountry.locationX, dcountry.locationY, acountry.locationX, acountry.locationY) == dc.requiredDistance;
}
}