/*
* CharacterManager.java
* Copyright 2010 Connor Petty <cpmeister@users.sourceforge.net>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*
* Created on May 8, 2010, 5:13:06 PM
*/
package pcgen.system;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.logging.Level;
import pcgen.cdom.base.Constants;
import pcgen.core.Globals;
import pcgen.core.PlayerCharacter;
import pcgen.facade.core.CampaignFacade;
import pcgen.facade.core.CharacterFacade;
import pcgen.facade.core.CharacterStubFacade;
import pcgen.facade.core.DataSetFacade;
import pcgen.facade.core.GameModeFacade;
import pcgen.facade.core.PartyFacade;
import pcgen.facade.core.SourceSelectionFacade;
import pcgen.facade.core.UIDelegate;
import pcgen.facade.util.ListFacade;
import pcgen.facade.util.ListFacades;
import pcgen.gui2.facade.CharacterFacadeImpl;
import pcgen.gui2.facade.PartyFacadeImpl;
import pcgen.gui2.util.HtmlInfoBuilder;
import pcgen.io.PCGFile;
import pcgen.io.PCGIOHandler;
import pcgen.pluginmgr.PCGenMessageHandler;
import pcgen.pluginmgr.PluginManager;
import pcgen.pluginmgr.messages.PlayerCharacterWasLoadedMessage;
import pcgen.util.Logging;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
/**
* This class stores the characters that are currently opened by
* PCGen. It also handles creating new characters and opening
* characters from files. The getCharacters method returns
* a listenable list that allows users of this class to not
* only see what characters are open but to easily track any
* changes to the list of available characters.
*
* <br>
*
* @author Connor Petty <cpmeister@users.sourceforge.net>
*/
public final class CharacterManager
{
private static final PartyFacadeImpl characters;
private static final RecentFileList recentCharacters;
private static final RecentFileList recentParties;
private static final PCGenMessageHandler messageHandler;
static
{
characters = new PartyFacadeImpl();
recentCharacters = new RecentFileList(PCGenSettings.RECENT_CHARACTERS);
recentParties = new RecentFileList(PCGenSettings.RECENT_PARTIES);
messageHandler = PluginManager.getInstance().getPostbox();
}
private CharacterManager()
{
}
/**
* Create a new character using the supplied data sets.
* @param delegate the UIDelegate that this character will use.
* @param dataset the dataset that this will be loaded with.
* @return The character that was created.
*/
public static CharacterFacade createNewCharacter(UIDelegate delegate, DataSetFacade dataset)
{
@SuppressWarnings("rawtypes")
List campaigns = ListFacades.wrap(dataset.getCampaigns());
try
{
@SuppressWarnings("unchecked")
PlayerCharacter pc = new PlayerCharacter(campaigns);
Globals.getPCList().add(pc);
CharacterFacade character = new CharacterFacadeImpl(pc, delegate, dataset);
String name = createNewCharacterName();
character.setName(name);
characters.addElement(character);
Logging.log(Logging.INFO, "Created new character " + name + "."); //$NON-NLS-1$ //$NON-NLS-2$
messageHandler.handleMessage(new PlayerCharacterWasLoadedMessage(delegate, pc));
return character;
}
catch (final Exception e)
{
Logging.errorPrint("Unable to create character with data " //$NON-NLS-1$
+ dataset, e);
delegate.showErrorMessage(
LanguageBundle.getString("in_cmCreateErrorTitle"), //$NON-NLS-1$
LanguageBundle.getFormattedString("in_cmCreateErrorMessage", //$NON-NLS-1$
e.getMessage()));
return null;
}
}
public static ListFacade<File> getRecentCharacters()
{
return recentCharacters;
}
public static ListFacade<File> getRecentParties()
{
return recentParties;
}
/**
* This opens an existing character from a file and adds it to the
* list of open characters. If there is a character already open
* that uses this file, then this method does nothing.
* @param file the file to load this character from
* @param delegate the UIDelegate that this character will use
* @param dataset the dataset that this will be loaded with
* @return The character that was opened.
*/
public static CharacterFacade openCharacter(File file, UIDelegate delegate, DataSetFacade dataset)
{
final PlayerCharacter newPC = openPcInternal(file, delegate, dataset, false);
if (newPC == null)
{
return null;
}
return createChracterFacade(delegate, dataset, newPC);
}
/**
* This opens an existing character from a file and adds it to the
* list of open characters. If there is a character already open
* that uses this file, then this method does nothing.
* @param file the file to load this character from
* @param delegate the UIDelegate that this character will use
* @param dataset the dataset that this will be loaded with
* @param blockLoadedMessage Should we stop the character loaded message being sent out to listeners.
* @return The character that was opened.
*/
public static PlayerCharacter openPlayerCharacter(File file,
UIDelegate delegate, DataSetFacade dataset, boolean blockLoadedMessage)
{
final PlayerCharacter newPC = openPcInternal(file, delegate, dataset, blockLoadedMessage);
if (newPC == null)
{
return null;
}
createChracterFacade(delegate, dataset, newPC);
return newPC;
}
@SuppressWarnings("unchecked")
private static PlayerCharacter openPcInternal(File file,
UIDelegate delegate, DataSetFacade dataset, boolean blockLoadedMessage)
{
@SuppressWarnings("rawtypes")
List campaigns = ListFacades.wrap(dataset.getCampaigns());
final PCGIOHandler ioHandler = new PCGIOHandler();
final PlayerCharacter newPC;
try
{
newPC = new PlayerCharacter(campaigns);
newPC.setFileName(file.getAbsolutePath());
ioHandler.read(newPC, file.getAbsolutePath());
// Ensure any custom equipment held by the character is added to the dataset's list
dataset.refreshEquipment();
if (!showLoadNotices(true, ioHandler.getErrors(), file.getName(),
delegate))
{
// if we've had errors, then abort trying to add the new PC, it's most likely "broken"
return null;
}
if (!showLoadNotices(false, ioHandler.getWarnings(), file.getName(),
delegate))
{
return null;
}
Logging.log(Logging.INFO, "Loaded character " + newPC.getName() //$NON-NLS-1$
+ " - " + file.getAbsolutePath()); //$NON-NLS-1$
// if it's not broken, then only warnings should have been generated, and we won't count those
// Register the character so that future checks to see if file already loaded will work
Globals.getPCList().add(newPC);
if (!blockLoadedMessage)
{
messageHandler.handleMessage(new PlayerCharacterWasLoadedMessage(delegate, newPC));
}
return newPC;
}
catch (final Exception e)
{
Logging.errorPrint("Unable to load character " + file, e); //$NON-NLS-1$
delegate.showErrorMessage(
LanguageBundle.getString("in_cmLoadErrorTitle"), //$NON-NLS-1$
LanguageBundle.getFormattedString("in_cmLoadErrorMessage", //$NON-NLS-1$
file, e.getMessage()));
return null;
}
}
private static CharacterFacade createChracterFacade(UIDelegate delegate,
DataSetFacade dataset, final PlayerCharacter newPC)
{
CharacterFacade character = new CharacterFacadeImpl(newPC, delegate, dataset);
characters.addElement(character);
return character;
}
/**
* Show the user any warnings or errors from the character load and get
* their approval to continue.
*
* @param errors Is this a list of errors?
* @param warnings The warnings generated on load.
* @param fileName The name of the file being loaded.
* @param delegate The UIDelegate to use for notifications.
* @return true if the character should be loaded, false if not.
*/
private static boolean showLoadNotices(boolean errors, List<String> warnings, String fileName, UIDelegate delegate)
{
if (warnings.isEmpty())
{
return true;
}
HtmlInfoBuilder warningMsg = new HtmlInfoBuilder();
Level lvl;
if (errors)
{
warningMsg.append(LanguageBundle.getString("in_cmErrorStart")); //$NON-NLS-1$
Logging.errorPrint("The following errors were encountered while loading " + fileName);
lvl = Logging.ERROR;
}
else
{
warningMsg.append(LanguageBundle.getString("in_cmWarnStart")); //$NON-NLS-1$
Logging.log(Logging.WARNING, "The following warnings were encountered while loading " + fileName);
lvl = Logging.WARNING;
}
warningMsg.appendLineBreak();
warningMsg.append("<UL>"); //$NON-NLS-1$
for (final String string : warnings)
{
warningMsg.appendLineBreak();
warningMsg.append("<li>"); //$NON-NLS-1$
warningMsg.append(string);
warningMsg.append("</li>"); //$NON-NLS-1$
Logging.log(lvl, "* " + string); //$NON-NLS-1$
}
warningMsg.append("</UL>"); //$NON-NLS-1$
warningMsg.appendLineBreak();
if (errors)
{
warningMsg.append(LanguageBundle.getString("in_cmErrorEnd")); //$NON-NLS-1$
delegate.showErrorMessage(fileName, warningMsg.toString());
return false;
}
warningMsg.append(LanguageBundle.getString("in_cmWarnEnd")); //$NON-NLS-1$
return delegate.showWarningConfirm(fileName, warningMsg.toString());
}
/**
* This opens an existing party from a file and adds all characters to the
* list of open characters.
*
* @param file the file to load this party from
* @param delegate the UIDelegate that these characters will use
* @param dataset the dataset that this will be loaded with
* @return The party that was opened.
*/
public static PartyFacade openParty(File file, final UIDelegate delegate, final DataSetFacade dataset)
{
Logging.log(Logging.INFO, "Loading party " + file.getAbsolutePath()); //$NON-NLS-1$
PCGIOHandler ioHandler = new PCGIOHandler();
ioHandler.readCharacterFileList(file).forEach(charFile ->
openCharacter(charFile, delegate, dataset));
characters.setFile(file);
return characters;
}
public static SourceSelectionFacade getRequiredSourcesForParty(File pcpFile, UIDelegate delegate)
{
PCGIOHandler ioHandler = new PCGIOHandler();
List<File> files = ioHandler.readCharacterFileList(pcpFile);
if ((files == null) || files.isEmpty())
{
return null;
}
GameModeFacade gameMode = null;
HashSet<CampaignFacade> campaignSet = new HashSet<>();
for (final File file : files)
{
SourceSelectionFacade selection = getRequiredSourcesForCharacter(file, delegate);
if (selection == null)
{
Logging.errorPrint("Failed to find sources in: " + file.getAbsolutePath());
continue;
}
GameModeFacade game = selection.getGameMode().get();
if (gameMode == null)
{
gameMode = game;
}
else if (gameMode != game)
{
Logging.errorPrint("Characters in " + pcpFile.getAbsolutePath()
+ " do not share the same game mode");
return null;
}
for (final CampaignFacade campaign : selection.getCampaigns())
{
campaignSet.add(campaign);
}
}
//TODO: check to make sure that the campaigns are compatable
return FacadeFactory.createSourceSelection(gameMode, new ArrayList<>(campaignSet));
}
/**
*
* @param pcgFile a character file
* @param delegate The UIDelegate used to display message to the user
* @return a SourceSelectionFacade or null if no sources could be found
*/
public static SourceSelectionFacade getRequiredSourcesForCharacter(File pcgFile, UIDelegate delegate)
{
if (!PCGFile.isPCGenCharacterFile(pcgFile))
{
throw new IllegalArgumentException();
}
final PCGIOHandler ioHandler = new PCGIOHandler();
SourceSelectionFacade selection = ioHandler.readSources(pcgFile);
if (!ioHandler.getErrors().isEmpty())
{
ioHandler.getErrors().forEach(msg ->
{
delegate.showErrorMessage(Constants.APPLICATION_NAME, msg);
Logging.errorPrint(msg);
});
return null;
}
return selection;
}
/**
* Check if the character's filename is ready to be saved to.
* @param character The character to be checked.
* @return true if the file can be written to, false otherwise.
*/
public static boolean characterFilenameValid(CharacterFacade character)
{
if (character.getFileRef().get() == null
|| StringUtils.isEmpty(character.getFileRef().get()
.getName()))
{
return false;
}
File file = character.getFileRef().get();
if (StringUtils.isBlank(file.getName()))
{
return false;
}
return !file.isDirectory() && (!file.exists() || file.canWrite());
}
/**
* Saves this character to the character's file specified
* by character.getFileRef().getReference()
* This is expected to be called before a character is to
* be removed from the list of open characters.
* @param character the character to be saved
* @return true if the save succeeded, false if not
*/
public static boolean saveCharacter(CharacterFacade character)
{
File file = character.getFileRef().get();
if (StringUtils.isBlank(file.getName()))
{
return false;
}
Logging.log(Logging.INFO,
"Saving character " + character.getNameRef().get() //$NON-NLS-1$
+ " - " + file.getAbsolutePath()); //$NON-NLS-1$
if (character instanceof CharacterFacadeImpl)
{
UIDelegate delegate = character.getUIDelegate();
try
{
((CharacterFacadeImpl) character).save();
}
catch (final NullPointerException e)
{
Logging.errorPrint("Could not save " + character.getNameRef().get(), e);
delegate.showErrorMessage(Constants.APPLICATION_NAME,
"Could not save " + character.getNameRef().get());
return false;
}
catch (final IOException e)
{
Logging.errorPrint("Could not save " + character.getNameRef().get(), e);
delegate.showErrorMessage(Constants.APPLICATION_NAME,
"Could not save " + character.getNameRef().get()
+ " due to the error:\n" + e.getMessage());
return false;
}
}
else
{
Logging.errorPrint("Could not save " + character.getNameRef().get()
+ " due to unexpected class of character: "
+ character.getClass().getCanonicalName());
return false;
}
recentCharacters.addRecentFile(file);
return true;
}
public static boolean saveCurrentParty()
{
File file = characters.getFileRef().get();
if (file == null)
{
return false;
}
Logging.log(Logging.INFO, "Saving party " + file.getAbsolutePath()); //$NON-NLS-1$
characters.save();
return true;
}
/**
* removes a character from the list of open characters.
* This is called at the end of a close character operation.
* Note: this operation does not save the character!
* @param character the character to be closed
*/
public static void removeCharacter(CharacterFacade character)
{
characters.removeElement(character);
// This advises the message handler also.
character.closeCharacter();
File charFile = character.getFileRef().get();
recentCharacters.addRecentFile(charFile);
if (characters.isEmpty())
{
recentParties.addRecentFile(characters.getFileRef().get());
characters.setFile(null);
}
Logging.log(Logging.INFO,
"Closed character " + character.getNameRef().get() //$NON-NLS-1$
+ " - " + charFile.getAbsolutePath()); //$NON-NLS-1$
}
public static void removeAllCharacters()
{
for (final CharacterFacade characterFacade : characters)
{
recentCharacters.addRecentFile(characterFacade.getFileRef().get());
// This advises the message handler also.
characterFacade.closeCharacter();
}
characters.clearContents();
recentParties.addRecentFile(characters.getFileRef().get());
characters.setFile(null);
Logging.log(Logging.INFO, "Closed all characters"); //$NON-NLS-1$
}
public static PartyFacade getCharacters()
{
return characters;
}
/**
* Retrieve the loaded character matching the character stub. The character
* may not have been saved yet, so may not have a file name, in which case
* the match is made on character name. This is often used for retrieval of
* a loaded master or companion.
*
* @param companion The companion to be searched for.
* @return The character, or null if the companion is not loaded.
*/
public static CharacterFacade getCharacterMatching(CharacterStubFacade companion)
{
File compFile = companion.getFileRef().get();
if (compFile == null || StringUtils.isEmpty(compFile.getName()))
{
String compName = companion.getNameRef().get();
for (final CharacterFacade character : getCharacters())
{
String charName = character.getNameRef().get();
if (ObjectUtils.equals(compName, charName))
{
return character;
}
}
}
else
{
for (final CharacterFacade character : getCharacters())
{
File charFile = character.getFileRef().get();
if (compFile.equals(charFile))
{
return character;
}
}
}
return null;
}
private static String createNewCharacterName()
{
String name = "Unnamed ";
int i = 1;
while (isNameUsed(name + i))
{
i++;
}
return name + i;
}
private static boolean isNameUsed(String name)
{
for (final CharacterFacade character : characters)
{
if (character.getNameRef().get().equals(name))
{
return true;
}
}
return false;
}
}