diff --git a/.gitattributes b/.gitattributes index 8082dcf94ed..b10c6fc886c 100644 --- a/.gitattributes +++ b/.gitattributes @@ -14260,6 +14260,7 @@ forge-gui/res/defaults/gauntlet/LOCKED_DotP[!!-~]Preconstructed.dat -text forge-gui/res/defaults/gauntlet/LOCKED_Swimming[!!-~]With[!!-~]Sharks.dat -text forge-gui/res/defaults/home.xml svneol=native#text/xml forge-gui/res/defaults/match.xml svneol=native#text/xml +forge-gui/res/defaults/no_card.jpg -text forge-gui/res/defaults/window.xml -text forge-gui/res/defaults/workshop.xml -text forge-gui/res/draft/cube_juzamjedi.draft -text @@ -15915,7 +15916,6 @@ forge-gui/src/main/java/forge/view/arcane/util/CardPanelMouseListener.java svneo forge-gui/src/main/java/forge/view/arcane/util/OutlinedLabel.java svneol=native#text/plain forge-gui/src/main/java/forge/view/arcane/util/package-info.java svneol=native#text/plain forge-gui/src/main/java/forge/view/package-info.java svneol=native#text/plain -forge-gui/src/main/resources/no_card.jpg -text forge-gui/src/main/resources/proxy-template.ftl -text forge-gui/src/site/apt/index.apt -text forge-gui/src/test/java/forge/BoosterDraft1Test.java svneol=native#text/plain @@ -16016,6 +16016,11 @@ forge-m-base/src/forge/assets/FSkinColor.java -text forge-m-base/src/forge/assets/FSkinFont.java -text forge-m-base/src/forge/assets/FSkinImage.java -text forge-m-base/src/forge/assets/FSkinTexture.java -text +forge-m-base/src/forge/assets/FTextureImage.java -text +forge-m-base/src/forge/assets/ImageCache.java -text +forge-m-base/src/forge/assets/ImageLoader.java -text +forge-m-base/src/forge/error/BugReporter.java -text +forge-m-base/src/forge/error/ExceptionHandler.java -text forge-m-base/src/forge/model/FModel.java -text forge-m-base/src/forge/player/LobbyPlayerHuman.java -text forge-m-base/src/forge/player/PlayerControllerHuman.java -text @@ -16026,7 +16031,7 @@ forge-m-base/src/forge/screens/constructed/ConstructedScreen.java -text forge-m-base/src/forge/screens/draft/DraftScreen.java -text forge-m-base/src/forge/screens/guantlet/GuantletScreen.java -text forge-m-base/src/forge/screens/home/HomeScreen.java -text -forge-m-base/src/forge/screens/match/MatchController.java -text +forge-m-base/src/forge/screens/match/FControl.java -text forge-m-base/src/forge/screens/match/MatchScreen.java -text forge-m-base/src/forge/screens/match/views/VAvatar.java -text forge-m-base/src/forge/screens/match/views/VField.java -text @@ -16046,11 +16051,13 @@ forge-m-base/src/forge/toolbox/FDisplayObject.java -text forge-m-base/src/forge/toolbox/FGestureAdapter.java -text forge-m-base/src/forge/toolbox/FLabel.java -text forge-m-base/src/forge/toolbox/FList.java -text +forge-m-base/src/forge/toolbox/FOptionPane.java -text forge-m-base/src/forge/toolbox/FOverlay.java -text forge-m-base/src/forge/toolbox/FProgressBar.java -text forge-m-base/src/forge/toolbox/FScrollPane.java -text forge-m-base/src/forge/utils/Constants.java -text forge-m-base/src/forge/utils/ForgePreferences.java -text +forge-m-base/src/forge/utils/ForgeProfileProperties.java -text forge-m-base/src/forge/utils/Preferences.java -text forge-m-base/src/forge/utils/PreferencesStore.java -text forge-m-base/src/forge/utils/Utils.java -text diff --git a/forge-gui/src/main/resources/no_card.jpg b/forge-gui/res/defaults/no_card.jpg similarity index 100% rename from forge-gui/src/main/resources/no_card.jpg rename to forge-gui/res/defaults/no_card.jpg diff --git a/forge-gui/src/main/java/forge/ImageCache.java b/forge-gui/src/main/java/forge/ImageCache.java index bd0324b55e4..21945314679 100644 --- a/forge-gui/src/main/java/forge/ImageCache.java +++ b/forge-gui/src/main/java/forge/ImageCache.java @@ -38,7 +38,6 @@ import org.apache.commons.lang3.StringUtils; import javax.imageio.ImageIO; import java.awt.image.BufferedImage; import java.io.File; -import java.io.InputStream; import java.util.HashSet; import java.util.Set; import java.util.concurrent.ExecutionException; @@ -66,16 +65,9 @@ public class ImageCache { static { BufferedImage defImage = null; try { - ClassLoader cl = Thread.currentThread().getContextClassLoader(); - InputStream isNoCardJpg = cl.getResourceAsStream("no_card.jpg"); - defImage = ImageIO.read(isNoCardJpg); - } catch (Exception e) { - // resource not found; perhaps we're running straight from source - try { - defImage = ImageIO.read(new File("src/main/resources/no_card.jpg")); - } catch (Exception ex) { - System.err.println("could not load default card image"); - } + defImage = ImageIO.read(new File(NewConstants.NO_CARD_FILE)); + } catch (Exception ex) { + System.err.println("could not load default card image"); } finally { _defaultImage = (null == defImage) ? new BufferedImage(10, 10, BufferedImage.TYPE_INT_ARGB) : defImage; } diff --git a/forge-gui/src/main/java/forge/properties/NewConstants.java b/forge-gui/src/main/java/forge/properties/NewConstants.java index 40fa2b73f53..82e990f12ed 100644 --- a/forge-gui/src/main/java/forge/properties/NewConstants.java +++ b/forge-gui/src/main/java/forge/properties/NewConstants.java @@ -46,6 +46,7 @@ public final class NewConstants { public static final String CARD_DATA_DIR = _RES_ROOT + "cardsfolder/"; public static final String DECK_CUBE_DIR = _RES_ROOT + "cube"; public static final String AI_PROFILE_DIR = _RES_ROOT + "ai"; + public static final String NO_CARD_FILE = _RES_ROOT + "defaults/no_card.jpg"; public static final String QUEST_WORLD_DIR = _QUEST_DIR + "worlds/"; public static final String QUEST_PRECON_DIR = _QUEST_DIR + "precons/"; diff --git a/forge-m-base/src/forge/assets/FTextureImage.java b/forge-m-base/src/forge/assets/FTextureImage.java new file mode 100644 index 00000000000..d18dd55ade2 --- /dev/null +++ b/forge-m-base/src/forge/assets/FTextureImage.java @@ -0,0 +1,28 @@ +package forge.assets; + +import com.badlogic.gdx.graphics.Texture; + +import forge.Forge.Graphics; + +public class FTextureImage implements FImage { + private final Texture texture; + + public FTextureImage(Texture texture0) { + texture = texture0; + } + + @Override + public float getWidth() { + return texture.getWidth(); + } + + @Override + public float getHeight() { + return texture.getHeight(); + } + + @Override + public void draw(Graphics g, float x, float y, float w, float h) { + g.drawImage(texture, x, y, w, h); + } +} diff --git a/forge-m-base/src/forge/assets/ImageCache.java b/forge-m-base/src/forge/assets/ImageCache.java new file mode 100644 index 00000000000..d3bf397698e --- /dev/null +++ b/forge-m-base/src/forge/assets/ImageCache.java @@ -0,0 +1,263 @@ +/* + * Forge: Play Magic: the Gathering. + * Copyright (C) 2011 Forge Team + * + * 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 . + */ +package forge.assets; + +import com.badlogic.gdx.Gdx; +import com.badlogic.gdx.graphics.Pixmap.Format; +import com.badlogic.gdx.graphics.Texture; +import com.badlogic.gdx.utils.Base64Coder; +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.LoadingCache; + +import forge.ImageKeys; +import forge.card.CardDb; +import forge.card.CardRules; +import forge.card.CardSplitType; +import forge.game.card.Card; +import forge.game.player.IHasIcon; +import forge.item.InventoryItem; +import forge.item.PaperCard; +import forge.model.FModel; +import forge.screens.match.FControl; +import forge.utils.Constants; + +import org.apache.commons.lang3.StringUtils; + +import java.util.HashSet; +import java.util.Set; + +/** + * This class stores ALL card images in a cache with soft values. this means + * that the images may be collected when they are not needed any more, but will + * be kept as long as possible. + *

+ * The keys are the following: + *

+ * + * @author Forge + * @version $Id: ImageCache.java 24769 2014-02-09 13:56:04Z Hellfish $ + */ +public class ImageCache { + // short prefixes to save memory + + private static final Set _missingIconKeys = new HashSet(); + private static final LoadingCache _CACHE = CacheBuilder.newBuilder().softValues().build(new ImageLoader()); + private static final Texture _defaultImage; + static { + Texture defImage = new Texture(Gdx.files.internal(Constants.DEFAULT_DUELS_DIR)); + try { + defImage = new Texture(Gdx.files.internal(Constants.DEFAULT_DUELS_DIR)); + } catch (Exception ex) { + System.err.println("could not load default card image"); + } finally { + _defaultImage = (null == defImage) ? new Texture(10, 10, Format.RGBA8888) : defImage; + } + } + + public static void clear() { + _CACHE.invalidateAll(); + _missingIconKeys.clear(); + } + + public static Texture getImage(Card card) { + final String key; + if (!FControl.mayShowCard(card) || card.isFaceDown()) { + key = ImageKeys.TOKEN_PREFIX + ImageKeys.MORPH_IMAGE; + } else { + key = card.getImageKey(); + } + return getImage(key, true); + } + + public static Texture getImage(InventoryItem ii) { + return getImage(ImageKeys.getImageKey(ii, false), true); + } + + /** + * retrieve an icon from the cache. returns the current skin's ICO_UNKNOWN if the icon image is not found + * in the cache and cannot be loaded from disk. + */ + public static FImage getIcon(IHasIcon ihi) { + String imageKey = ihi.getIconImageKey(); + final Texture icon; + if (_missingIconKeys.contains(imageKey) || + null == (icon = getImage(ihi.getIconImageKey(), false))) { + _missingIconKeys.add(imageKey); + return FSkinImage.UNKNOWN; + } + return new FTextureImage(icon); + } + + /** + * This requests the original unscaled image from the cache for the given key. + * If the image does not exist then it can return a default image if desired. + *

+ * If the requested image is not present in the cache then it attempts to load + * the image from file (slower) and then add it to the cache for fast future access. + *

+ */ + public static Texture getImage(String imageKey, boolean useDefaultIfNotFound) { + if (StringUtils.isEmpty(imageKey)) { + return null; + } + + boolean altState = imageKey.endsWith(ImageKeys.BACKFACE_POSTFIX); + if (altState) { + imageKey = imageKey.substring(0, imageKey.length() - ImageKeys.BACKFACE_POSTFIX.length()); + } + if (imageKey.startsWith(ImageKeys.CARD_PREFIX)) { + imageKey = getImageKey(getPaperCardFromImageKey(imageKey.substring(2)), altState, true); + if (StringUtils.isBlank(imageKey)) { + return _defaultImage; + } + } + + // Load from file and add to cache if not found in cache initially. + Texture image = ImageCache._CACHE.getIfPresent(imageKey); + + // No image file exists for the given key so optionally associate with + // a default "not available" image and add to cache for given key. + if (image == null) { + if (useDefaultIfNotFound) { + image = _defaultImage; + _CACHE.put(imageKey, _defaultImage); + } + else { + image = null; + } + } + return image; + } + + private static PaperCard getPaperCardFromImageKey(String key) { + if (key == null) { + return null; + } + + PaperCard cp = FModel.getMagicDb().getCommonCards().getCard(key); + if (cp == null) { + cp = FModel.getMagicDb().getVariantCards().getCard(key); + } + return cp; + } + + private static String getImageRelativePath(PaperCard cp, boolean backFace, boolean includeSet, boolean isDownloadUrl) { + final String nameToUse = cp == null ? null : getNameToUse(cp, backFace); + if ( null == nameToUse ) + return null; + + StringBuilder s = new StringBuilder(); + + CardRules card = cp.getRules(); + String edition = cp.getEdition(); + s.append(ImageCache.toMWSFilename(nameToUse)); + + final int cntPictures; + final boolean hasManyPictures; + final CardDb db = !card.isVariant() ? FModel.getMagicDb().getCommonCards() : FModel.getMagicDb().getVariantCards(); + if (includeSet) { + cntPictures = db.getPrintCount(card.getName(), edition); + hasManyPictures = cntPictures > 1; + } else { + // without set number of pictures equals number of urls provided in Svar:Picture + String urls = card.getPictureUrl(backFace); + cntPictures = StringUtils.countMatches(urls, "\\") + 1; + + // raise the art index limit to the maximum of the sets this card was printed in + int maxCntPictures = db.getMaxPrintCount(card.getName()); + hasManyPictures = maxCntPictures > 1; + } + + int artIdx = cp.getArtIndex() - 1; + if (hasManyPictures) { + if ( cntPictures <= artIdx ) // prevent overflow + artIdx = cntPictures == 0 ? 0 : artIdx % cntPictures; + s.append(artIdx + 1); + } + + // for whatever reason, MWS-named plane cards don't have the ".full" infix + if (!card.getType().isPlane() && !card.getType().isPhenomenon()) { + s.append(".full"); + } + + final String fname; + if (isDownloadUrl) { + s.append(".jpg"); + fname = Base64Coder.encodeString(s.toString()); + } + else { + fname = s.toString(); + } + + if (includeSet) { + String editionAliased = isDownloadUrl ? FModel.getMagicDb().getEditions().getCode2ByCode(edition) : getSetFolder(edition); + return String.format("%s/%s", editionAliased, fname); + } + return fname; + } + + public static boolean hasBackFacePicture(PaperCard cp) { + CardSplitType cst = cp.getRules().getSplitType(); + return cst == CardSplitType.Transform || cst == CardSplitType.Flip; + } + + public static String getSetFolder(String edition) { + return !Constants.CACHE_CARD_PICS_SUBDIR.containsKey(edition) + ? FModel.getMagicDb().getEditions().getCode2ByCode(edition) // by default 2-letter codes from MWS are used + : Constants.CACHE_CARD_PICS_SUBDIR.get(edition); // may use custom paths though + } + + private static String getNameToUse(PaperCard cp, boolean backFace) { + final CardRules card = cp.getRules(); + if (backFace ) { + if (hasBackFacePicture(cp)) { + return card.getOtherPart().getName(); + } + return null; + } + if (CardSplitType.Split == cp.getRules().getSplitType()) { + return card.getMainPart().getName() + card.getOtherPart().getName(); + } + return cp.getName(); + } + + public static String getImageKey(PaperCard cp, boolean backFace, boolean includeSet) { + return getImageRelativePath(cp, backFace, includeSet, false); + } + + public static String getDownloadUrl(PaperCard cp, boolean backFace) { + return getImageRelativePath(cp, backFace, true, true); + } + + public static String toMWSFilename(String in) { + final StringBuffer out = new StringBuffer(); + char c; + for (int i = 0; i < in.length(); i++) { + c = in.charAt(i); + if ((c == '"') || (c == '/') || (c == ':') || (c == '?')) { + out.append(""); + } else { + out.append(c); + } + } + return out.toString(); + } +} diff --git a/forge-m-base/src/forge/assets/ImageLoader.java b/forge-m-base/src/forge/assets/ImageLoader.java new file mode 100644 index 00000000000..79e5588d02f --- /dev/null +++ b/forge-m-base/src/forge/assets/ImageLoader.java @@ -0,0 +1,97 @@ +package forge.assets; + +import com.badlogic.gdx.files.FileHandle; +import com.badlogic.gdx.graphics.Texture; +import com.google.common.cache.CacheLoader; + +import forge.ImageKeys; +import forge.error.BugReporter; +import forge.utils.Constants; + +import org.apache.commons.lang3.StringUtils; + +import java.io.File; + +final class ImageLoader extends CacheLoader { + // image file extensions for various formats in order of likelihood + // the last, empty, string is for keys that come in with an extension already in place + private static final String[] _FILE_EXTENSIONS = { ".jpg", ".png", "" }; + + @Override + public Texture load(String key) { + if (StringUtils.isEmpty(key)) { + return null; + } + + final String path; + final String filename; + if (key.startsWith(ImageKeys.TOKEN_PREFIX)) { + filename = key.substring(ImageKeys.TOKEN_PREFIX.length()); + path = Constants.CACHE_TOKEN_PICS_DIR; + } else if (key.startsWith(ImageKeys.ICON_PREFIX)) { + filename = key.substring(ImageKeys.ICON_PREFIX.length()); + path = Constants.CACHE_ICON_PICS_DIR; + } else if (key.startsWith(ImageKeys.BOOSTER_PREFIX)) { + filename = key.substring(ImageKeys.BOOSTER_PREFIX.length()); + path = Constants.CACHE_BOOSTER_PICS_DIR; + } else if (key.startsWith(ImageKeys.FATPACK_PREFIX)) { + filename = key.substring(ImageKeys.FATPACK_PREFIX.length()); + path = Constants.CACHE_FATPACK_PICS_DIR; + } else if (key.startsWith(ImageKeys.PRECON_PREFIX)) { + filename = key.substring(ImageKeys.PRECON_PREFIX.length()); + path = Constants.CACHE_PRECON_PICS_DIR; + } else if (key.startsWith(ImageKeys.TOURNAMENTPACK_PREFIX)) { + filename = key.substring(ImageKeys.TOURNAMENTPACK_PREFIX.length()); + path = Constants.CACHE_TOURNAMENTPACK_PICS_DIR; + } else { + filename = key; + path = Constants.CACHE_CARD_PICS_DIR; + } + + Texture ret = _findFile(key, path, filename); + + // some S00 cards are really part of 6ED + if (null == ret ) { + String s2kAlias = ImageCache.getSetFolder("S00"); + if ( filename.startsWith(s2kAlias) ) { + ret = _findFile(key, path, filename.replace(s2kAlias, ImageCache.getSetFolder("6ED"))); + } + } + + // try without set prefix + String setlessFilename = null; + if (null == ret && filename.contains("/")) { + setlessFilename = filename.substring(filename.indexOf('/') + 1); + ret = _findFile(key, path, setlessFilename); + + // try lowering the art index to the minimum for regular cards + if (null == ret && setlessFilename.contains(".full")) { + ret = _findFile(key, path, setlessFilename.replaceAll("[0-9]*[.]full", "1.full")); + } + } + + if (null == ret) { + System.out.println("File not found, no image created: " + key); + } + + return ret; + } + + private static Texture _findFile(String key, String path, String filename) { + for (String ext : _FILE_EXTENSIONS) { + File file = new File(path, filename + ext); + //System.out.println(String.format("Searching for %s at: %s", key, file.getAbsolutePath())); + if (file.exists()) { + //System.out.println(String.format("Found %s at: %s", key, file.getAbsolutePath())); + try { + return new Texture(new FileHandle(file)); + } catch (Exception ex) { + BugReporter.reportException(ex, "Could not read image file " + file.getAbsolutePath() + " "); + break; + } + } + } + + return null; + } +} diff --git a/forge-m-base/src/forge/error/BugReporter.java b/forge-m-base/src/forge/error/BugReporter.java new file mode 100644 index 00000000000..54f0a5b42b1 --- /dev/null +++ b/forge-m-base/src/forge/error/BugReporter.java @@ -0,0 +1,310 @@ +/* + * Forge: Play Magic: the Gathering. + * Copyright (C) 2011 Forge Team + * + * 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 . + */ +package forge.error; + +import forge.toolbox.FOptionPane; +import forge.util.BuildInfo; + +import javax.swing.*; + +import java.awt.*; +import java.awt.datatransfer.StringSelection; +import java.awt.event.ActionEvent; +import java.awt.event.KeyEvent; +import java.io.*; +import java.net.URI; +import java.util.Map; +import java.util.Map.Entry; + +public class BugReporter { + private static final int _STACK_OVERFLOW_MAX_MESSAGE_LEN = 16 * 1024; + + private static boolean dialogShown = false; + + /** + * Shows exception information in a format ready to post to the forum as a crash report. Uses the exception's message + * as the reason if message is null. + */ + public static void reportException(final Throwable ex, final String message) { + if (ex == null) { + return; + } + String threadId = ""; //FThreads.debugGetCurrThreadId() + if (message != null) { + System.err.printf("%s > %s%n", threadId, message); + } + System.err.print(threadId + " > " ); + ex.printStackTrace(); + + StringBuilder sb = new StringBuilder(); + sb.append("Description: [describe what you were doing when the crash occurred]\n\n"); + _buildSpoilerHeader(sb, ex.getClass().getSimpleName()); + sb.append("\n\n"); + if (null != message && !message.isEmpty()) { + sb.append(threadId).append(" > ").append(message).append("\n"); + } + + StringWriter sw = new StringWriter(); + PrintWriter pw = new PrintWriter(sw); + ex.printStackTrace(pw); + + String swStr = sw.toString(); + if (ex instanceof StackOverflowError && + _STACK_OVERFLOW_MAX_MESSAGE_LEN <= swStr.length()) { + // most likely a cycle. only take first portion so the message + // doesn't grow too large to post + sb.append(swStr, 0, _STACK_OVERFLOW_MAX_MESSAGE_LEN); + sb.append("\n... (truncated)"); + } else { + sb.append(swStr); + } + + _buildSpoilerFooter(sb); + + _showDialog("Report a crash", sb.toString(), true); + } + + /** + * Alias for reportException(ex, null). + */ + public static void reportException(final Throwable ex) { + reportException(ex, null); + } + + /** + * Alias for reportException(ex, String.format(format, args)). + */ + public static void reportException(final Throwable ex, final String format, final Object... args) { + reportException(ex, String.format(format, args)); + } + + /** + * Shows a forum post template for reporting a bug. + */ + public static void reportBug(String details) { + StringBuilder sb = new StringBuilder(); + sb.append("Description: [describe the problem]\n\n"); + _buildSpoilerHeader(sb, "General bug report"); + if (null != details && !details.isEmpty()) { + sb.append("\n\n"); + sb.append(details); + } + _buildSpoilerFooter(sb); + + _showDialog("Report a bug", sb.toString(), false); + } + + /** + * Shows thread stack information in a format ready to post to the forum. + */ + public static void reportThreadStacks(final String message) { + StringBuilder sb = new StringBuilder(); + sb.append("Description: [describe what you were doing at the time]\n\n"); + _buildSpoilerHeader(sb, "Thread stack dump"); + sb.append("\n\n"); + if (null != message && !message.isEmpty()) { + sb.append(message); + sb.append("\n"); + } + + StringWriter sw = new StringWriter(); + PrintWriter pw = new PrintWriter(sw); + final Map traces = Thread.getAllStackTraces(); + for (final Entry e : traces.entrySet()) { + pw.println(); + pw.printf("%s (%s):%n", e.getKey().getName(), e.getKey().getId()); + for (final StackTraceElement el : e.getValue()) { + pw.println(el); + } + } + + sb.append(sw.toString()); + _buildSpoilerFooter(sb); + _showDialog("Thread stack dump", sb.toString(), false); + } + + /** + * Alias for reportThreadStacks(String.format(format, args)) + */ + public static void reportThreadStacks(final String format, final Object... args) { + reportThreadStacks(String.format(format, args)); + } + + private static StringBuilder _buildSpoilerHeader(StringBuilder sb, String reportTitle) { + sb.append("[spoiler=").append(reportTitle).append("][code]"); + sb.append("\nForge Version: ").append(BuildInfo.getVersionString()); + sb.append("\nOperating System: ").append(System.getProperty("os.name")) + .append(" ").append(System.getProperty("os.version")) + .append(" ").append(System.getProperty("os.arch")); + sb.append("\nJava Version: ").append(System.getProperty("java.version")) + .append(" ").append(System.getProperty("java.vendor")); + return sb; + } + + private static StringBuilder _buildSpoilerFooter(StringBuilder sb) { + sb.append("[/code][/spoiler]"); + return sb; + } + + private static void _showDialog(String title, String text, boolean showExitAppBtn) { + if ( dialogShown ) + return; + + /*JTextArea area = new JTextArea(text); + area.setFont(new Font("Monospaced", Font.PLAIN, 10)); + area.setEditable(false); + area.setLineWrap(true); + area.setWrapStyleWord(true); + + String helpText = "A template for a post in the bug reports forum topic is shown below. Just select 'Copy and go to forum' " + + "and the template will be copied to your system clipboard and the forum page will open in your browser. " + + "Then all you have to do is paste the text into a forum post and edit the description line."; + String helpUrlLabel = "Reporting bugs in Forge is very important. We sincerely thank you for your time." + + " For help writing a solid bug report, please see:"; + String helpUrl = "http://www.slightlymagic.net/forum/viewtopic.php?f=26&p=109925#p109925"; + JPanel helpPanel = new JPanel(new WrapLayout(FlowLayout.LEFT, 4, 2)); + for (String word : helpUrlLabel.split(" ")) { + helpPanel.add(new FLabel.Builder().text("" + word + "").useSkinColors(false).build()); + } + helpPanel.add(new FHyperlink.Builder().url(helpUrl).text("this post").useSkinColors(false).build()); + + JPanel p = new JPanel(new MigLayout("wrap")); + p.add(new FLabel.Builder().text(helpText).useSkinColors(false).build(), "gap 5"); + p.add(helpPanel, "w 600"); + p.add(new JScrollPane(area), "w 100%, h 100%, gaptop 5"); + + // determine proper forum URL + String forgeVersion = BuildInfo.getVersionString(); + final String url; + if (StringUtils.containsIgnoreCase(forgeVersion, "svn") + || StringUtils.containsIgnoreCase(forgeVersion, "snapshot")) { + url = "http://www.slightlymagic.net/forum/viewtopic.php?f=52&t=6333&start=54564487645#bottom"; + } else { + url = "http://www.slightlymagic.net/forum/viewforum.php?f=26"; + } + + // Button is not modified, String gets the automatic listener to hide + // the dialog + ArrayList options = new ArrayList(); + options.add(new JButton(new _CopyAndGo(url, area))); + options.add(new JButton(new _SaveAction(area))); + options.add("Close"); + if (showExitAppBtn) { + options.add(new JButton(new _ExitAction())); + } + + JOptionPane pane = new JOptionPane(p, JOptionPane.PLAIN_MESSAGE, + JOptionPane.DEFAULT_OPTION, null, options.toArray(), options.get(0)); + JDialog dlg = pane.createDialog(JOptionPane.getRootFrame(), title); + dlg.setSize(showExitAppBtn ? 780 : 600, 400); + dlg.setResizable(true); + dialogShown = true; + dlg.setVisible(true); + dlg.dispose(); + dialogShown = false;*/ + } + + @SuppressWarnings("serial") + private static class _CopyAndGo extends AbstractAction { + private final String url; + private final JTextArea text; + + public _CopyAndGo(String url, JTextArea text) { + super("Copy and go to forum"); + this.putValue(Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_C, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask())); + + this.url = url; + this.text = text; + } + + @Override + public void actionPerformed(final ActionEvent e) { + try { + // copy text to clipboard + StringSelection ss = new StringSelection(text.getText()); + Toolkit.getDefaultToolkit().getSystemClipboard().setContents(ss, null); + + // browse to url + Desktop.getDesktop().browse(new URI(url)); + } + catch (Exception ex) { + FOptionPane.showMessageDialog("Sorry, a problem occurred while opening the forum in your default browser.", + "A problem occurred", FOptionPane.ERROR_ICON); + } + } + } + + @SuppressWarnings("serial") + private static class _SaveAction extends AbstractAction { + private static JFileChooser c; + private final JTextArea area; + + public _SaveAction(final JTextArea areaParam) { + super("Save to file"); + this.putValue(Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_S, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask())); + this.area = areaParam; + } + + @Override + public void actionPerformed(final ActionEvent e) { + if (c == null) { + c = new JFileChooser(); + } + + File f; + long curTime = System.currentTimeMillis(); + for (int i = 0;; i++) { + final String name = String.format("%TF-%02d.txt", curTime, i); + f = new File(name); + if (!f.exists()) { + break; + } + } + + c.setSelectedFile(f); + c.showSaveDialog(null); + f = c.getSelectedFile(); + + try { + final BufferedWriter bw = new BufferedWriter(new FileWriter(f)); + bw.write(this.area.getText()); + bw.close(); + } + catch (final IOException ex) { + FOptionPane.showMessageDialog("There was an error during saving. Sorry!\n" + ex, + "Error saving file", FOptionPane.ERROR_ICON); + } + } + } + + @SuppressWarnings("serial") + private static class _ExitAction extends AbstractAction { + public _ExitAction() { + super("Exit application"); + this.putValue(Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_X, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask())); + } + + @Override + public void actionPerformed(final ActionEvent e) { + System.exit(0); + } + } + + // disable instantiation + private BugReporter() { } +} diff --git a/forge-m-base/src/forge/error/ExceptionHandler.java b/forge-m-base/src/forge/error/ExceptionHandler.java new file mode 100644 index 00000000000..c32afc26cc6 --- /dev/null +++ b/forge-m-base/src/forge/error/ExceptionHandler.java @@ -0,0 +1,46 @@ +/* + * Forge: Play Magic: the Gathering. + * Copyright (C) 2011 Forge Team + * + * 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 . + */ +package forge.error; + +import java.lang.Thread.UncaughtExceptionHandler; + +public class ExceptionHandler implements UncaughtExceptionHandler { + static { + // Tells Java to let this class handle any uncaught exception + Thread.setDefaultUncaughtExceptionHandler(new ExceptionHandler()); + // Tells AWT to let this class handle any uncaught exception + System.setProperty("sun.awt.exception.handler", ExceptionHandler.class.getName()); + } + + /** {@inheritDoc} */ + @Override + public final void uncaughtException(final Thread t, final Throwable ex) { + BugReporter.reportException(ex); + } + + /** + * This Method is called by AWT when an error is thrown in the event + * dispatching thread and not caught. + * + * @param ex + * a {@link java.lang.Throwable} object. + */ + public final void handle(final Throwable ex) { + BugReporter.reportException(ex); + } +} diff --git a/forge-m-base/src/forge/screens/match/MatchController.java b/forge-m-base/src/forge/screens/match/FControl.java similarity index 81% rename from forge-m-base/src/forge/screens/match/MatchController.java rename to forge-m-base/src/forge/screens/match/FControl.java index 22aa5413160..1545f217955 100644 --- a/forge-m-base/src/forge/screens/match/MatchController.java +++ b/forge-m-base/src/forge/screens/match/FControl.java @@ -6,23 +6,20 @@ import java.util.List; import forge.Forge; import forge.game.Game; import forge.game.Match; +import forge.game.card.Card; import forge.game.player.LobbyPlayer; import forge.game.player.Player; import forge.model.FModel; import forge.utils.ForgePreferences.FPref; -public class MatchController { - private final MatchScreen view; +public class FControl { + private static Game game; + private static MatchScreen view; + private static List sortedPlayers; - private Game game; - private List sortedPlayers; - - public MatchController(MatchScreen view0) { + public static void startGame(final Match match0, final MatchScreen view0) { + game = match0.createGame(); view = view0; - } - - public final void startGameWithUi(final Match match) { - game = match.createGame(); /*if (game.getRules().getGameType() == GameType.Quest) { QuestController qc = Singletons.getModel().getQuest(); @@ -51,14 +48,14 @@ public class MatchController { });*/ } - public final void endCurrentGame() { - if (this.game == null) { return; } + public static void endCurrentGame() { + if (game == null) { return; } Forge.back(); - this.game = null; + game = null; } - public void initMatch(final List players, LobbyPlayer localPlayer) { + public static void initMatch(final List players, LobbyPlayer localPlayer) { // TODO fix for use with multiplayer final String[] indices = FModel.getPreferences().getPref(FPref.UI_AVATARS).split(","); @@ -94,7 +91,7 @@ public class MatchController { initHandViews(localPlayer); } - public void initHandViews(LobbyPlayer localPlayer) { + public static void initHandViews(LobbyPlayer localPlayer) { /*final List hands = new ArrayList(); int i = 0; @@ -115,7 +112,7 @@ public class MatchController { view.setHandViews(hands);*/ } - private List shiftPlayersPlaceLocalFirst(final List players, LobbyPlayer localPlayer) { + private static List shiftPlayersPlaceLocalFirst(final List players, LobbyPlayer localPlayer) { // get an arranged list so that the first local player is at index 0 List sortedPlayers = new ArrayList(players); int ixFirstHuman = -1; @@ -130,4 +127,8 @@ public class MatchController { } return sortedPlayers; } + + public static boolean mayShowCard(Card c) { + return true;// game == null || !gameHasHumanPlayer || c.canBeShownTo(getCurrentPlayer()); + } } diff --git a/forge-m-base/src/forge/screens/match/MatchScreen.java b/forge-m-base/src/forge/screens/match/MatchScreen.java index 7f914b279bd..4ac2567054e 100644 --- a/forge-m-base/src/forge/screens/match/MatchScreen.java +++ b/forge-m-base/src/forge/screens/match/MatchScreen.java @@ -18,7 +18,6 @@ public class MatchScreen extends FScreen { private static FSkinColor BORDER_COLOR = FSkinColor.get(Colors.CLR_BORDERS); private final Match match; - private final MatchController controller; private final Map playerPanels; //private final VLog log; private final VStack stack; @@ -29,7 +28,6 @@ public class MatchScreen extends FScreen { public MatchScreen(Match match0) { super(true, "Game 1 Turn 1", true); match = match0; - controller = new MatchController(this); playerPanels = new HashMap(); for (RegisteredPlayer player : match.getPlayers()) { @@ -43,7 +41,7 @@ public class MatchScreen extends FScreen { stack = add(new VStack()); prompt = add(new VPrompt()); - controller.startGameWithUi(match0); + FControl.startGame(match0, this); } @Override diff --git a/forge-m-base/src/forge/toolbox/FOptionPane.java b/forge-m-base/src/forge/toolbox/FOptionPane.java new file mode 100644 index 00000000000..524b53ea417 --- /dev/null +++ b/forge-m-base/src/forge/toolbox/FOptionPane.java @@ -0,0 +1,255 @@ +package forge.toolbox; + +import forge.assets.FSkinImage; + +public class FOptionPane extends FOverlay { + public static final FSkinImage QUESTION_ICON = FSkinImage.QUESTION; + public static final FSkinImage INFORMATION_ICON = FSkinImage.INFORMATION; + public static final FSkinImage WARNING_ICON = FSkinImage.WARNING; + public static final FSkinImage ERROR_ICON = FSkinImage.ERROR; + + public static void showMessageDialog(String message) { + showMessageDialog(message, "Forge", INFORMATION_ICON); + } + + public static void showMessageDialog(String message, String title) { + showMessageDialog(message, title, INFORMATION_ICON); + } + + public static void showErrorDialog(String message) { + showMessageDialog(message, "Forge", ERROR_ICON); + } + + public static void showErrorDialog(String message, String title) { + showMessageDialog(message, title, ERROR_ICON); + } + + public static void showMessageDialog(String message, String title, FSkinImage icon) { + showOptionDialog(message, title, icon, new String[] {"OK"}, 0); + } + + public static boolean showConfirmDialog(String message) { + return showConfirmDialog(message, "Forge"); + } + + public static boolean showConfirmDialog(String message, String title) { + return showConfirmDialog(message, title, "Yes", "No", true); + } + + public static boolean showConfirmDialog(String message, String title, boolean defaultYes) { + return showConfirmDialog(message, title, "Yes", "No", defaultYes); + } + + public static boolean showConfirmDialog(String message, String title, String yesButtonText, String noButtonText) { + return showConfirmDialog(message, title, yesButtonText, noButtonText, true); + } + + public static boolean showConfirmDialog(String message, String title, String yesButtonText, String noButtonText, boolean defaultYes) { + String[] options = {yesButtonText, noButtonText}; + int reply = FOptionPane.showOptionDialog(message, title, QUESTION_ICON, options, defaultYes ? 0 : 1); + return (reply == 0); + } + + public static int showOptionDialog(String message, String title, FSkinImage icon, String[] options) { + return showOptionDialog(message, title, icon, options, 0); + } + + public static int showOptionDialog(String message, String title, FSkinImage icon, String[] options, int defaultOption) { + final FOptionPane optionPane = new FOptionPane(message, title, icon, null, options, defaultOption); + optionPane.setVisible(true); + int dialogResult = optionPane.result; + optionPane.setVisible(false); + return dialogResult; + } + + public static String showInputDialog(String message, String title) { + return showInputDialog(message, title, null, "", null); + } + + public static String showInputDialog(String message, String title, FSkinImage icon) { + return showInputDialog(message, title, icon, "", null); + } + + public static String showInputDialog(String message, String title, FSkinImage icon, String initialInput) { + return showInputDialog(message, title, icon, initialInput, null); + } + + public static T showInputDialog(String message, String title, FSkinImage icon, T initialInput, T[] inputOptions) { + /*final JComponent inputField; + FTextField txtInput = null; + FComboBox cbInput = null; + if (inputOptions == null) { + txtInput = new FTextField.Builder().text(initialInput.toString()).build(); + inputField = txtInput; + } + else { + cbInput = new FComboBox(inputOptions); + cbInput.setSelectedItem(initialInput); + inputField = cbInput; + } + + final FOptionPane optionPane = new FOptionPane(message, title, icon, inputField, new String[] {"OK", "Cancel"}, -1); + optionPane.setDefaultFocus(inputField); + inputField.addKeyListener(new KeyAdapter() { //hook so pressing Enter on field accepts dialog + @Override + public void keyPressed(KeyEvent e) { + if (e.getKeyCode() == KeyEvent.VK_ENTER) { + optionPane.setResult(0); + } + } + }); + optionPane.setVisible(true); + int dialogResult = optionPane.result; + optionPane.dispose(); + if (dialogResult == 0) { + if (inputOptions == null) { + return (T)txtInput.getText(); + } + else { + return (T)cbInput.getSelectedItem(); + } + }*/ + return null; + } + + private int result = -1; //default result to -1, indicating dialog closed without choosing option + private final FButton[] buttons; + + public FOptionPane(String message, String title, FSkinImage icon, FDisplayObject displayObj, String[] options, int defaultOption) { + buttons = new FButton[options.length]; //TODO: Remove this line when below uncommented + /*this.setTitle(title); + + float padding = 10; + float x = padding; + float gapAboveButtons = padding * 3 / 2; + float gapBottom = displayObj == null ? gapAboveButtons: padding; + + if (icon != null) { + FLabel lblIcon = new FLabel.Builder().icon(icon).build(); + float labelWidth = icon.getWidth(); + this.add(lblIcon, "x " + (x - 3) + ", ay top, w " + labelWidth + ", h " + icon.getHeight() + ", gapbottom " + gapBottom); + x += labelWidth; + } + if (message != null) { + FTextArea prompt = new FTextArea(message); + prompt.setFont(FSkin.getFont(14)); + prompt.setAutoSize(true); + Dimension parentSize = JOptionPane.getRootFrame().getSize(); + prompt.setMaximumSize(new Dimension(parentSize.width / 2, parentSize.height - 100)); + this.add(prompt, "x " + x + ", ay top, wrap, gaptop " + (icon == null ? 0 : 7) + ", gapbottom " + gapBottom); + x = padding; + } + if (displayObj != null) { + this.add(displayObj, "x " + x + ", w 100%-" + (x + padding) + ", wrap, gapbottom " + gapAboveButtons); + } + + //determine size of buttons + int optionCount = options.length; + FButton btnMeasure = new FButton(); //use blank button to aid in measurement + FontMetrics metrics = JOptionPane.getRootFrame().getGraphics().getFontMetrics(btnMeasure.getFont()); + + int maxTextWidth = 0; + buttons = new FButton[optionCount]; + for (int i = 0; i < optionCount; i++) { + int textWidth = metrics.stringWidth(options[i]); + if (textWidth > maxTextWidth) { + maxTextWidth = textWidth; + } + buttons[i] = new FButton(options[i]); + } + + this.pack(); //resize dialog to fit component and title to help determine button layout + + int width = this.getWidth(); + int gapBetween = 3; + int buttonHeight = 26; + int buttonWidth = Math.max(maxTextWidth + btnMeasure.getMargin().left + btnMeasure.getMargin().right, 120); //account for margins and enfore minimum width + int dx = buttonWidth + gapBetween; + int totalButtonWidth = dx * optionCount - gapBetween; + final int lastOption = optionCount - 1; + + //add buttons + x = (width - totalButtonWidth) / 2; + if (x < padding) { + width = totalButtonWidth + 2 * padding; //increase width to make room for buttons + x = padding; + } + for (int i = 0; i < optionCount; i++) { + final int option = i; + final FButton btn = buttons[i]; + btn.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent arg0) { + FOptionPane.this.result = option; + FOptionPane.this.setVisible(false); + } + }); + btn.addKeyListener(new KeyAdapter() { //hook certain keys to move focus between buttons + @Override + public void keyPressed(KeyEvent e) { + switch (e.getKeyCode()) { + case KeyEvent.VK_LEFT: + if (option > 0) { + buttons[option - 1].requestFocusInWindow(); + } + break; + case KeyEvent.VK_RIGHT: + if (option < lastOption) { + buttons[option + 1].requestFocusInWindow(); + } + break; + case KeyEvent.VK_HOME: + if (option > 0) { + buttons[0].requestFocusInWindow(); + } + break; + case KeyEvent.VK_END: + if (option < lastOption) { + buttons[lastOption].requestFocusInWindow(); + } + break; + } + } + }); + if (option == defaultOption) { + this.setDefaultFocus(btn); + } + this.add(btn, "x " + x + ", w " + buttonWidth + ", h " + buttonHeight); + x += dx; + } + + this.setSize(width, this.getHeight() + buttonHeight); //resize dialog again to account for buttons +*/ } + + @Override + public void setVisible(boolean visible) { + if (this.isVisible() == visible) { return; } + + if (visible) { + result = -1; //default result to -1 when shown, indicating dialog closed without choosing option + } + super.setVisible(visible); + } + + public int getResult() { + return result; + } + + public void setResult(int result0) { + this.result = result0; + /*SwingUtilities.invokeLater(new Runnable() { //delay hiding so action can finish first + @Override + public void run() { + setVisible(false); + } + });*/ + } + + public boolean isButtonEnabled(int index) { + return buttons[index].isEnabled(); + } + + public void setButtonEnabled(int index, boolean enabled) { + buttons[index].setEnabled(enabled); + } +} diff --git a/forge-m-base/src/forge/utils/Constants.java b/forge-m-base/src/forge/utils/Constants.java index 999fa04b950..9a4789b83fb 100644 --- a/forge-m-base/src/forge/utils/Constants.java +++ b/forge-m-base/src/forge/utils/Constants.java @@ -17,12 +17,14 @@ */ package forge.utils; +import java.util.Collections; +import java.util.Map; + import com.badlogic.gdx.Gdx; import com.badlogic.gdx.Application.ApplicationType; public final class Constants { - public static final String PROFILE_FILE = "forge.profile.properties"; - public static final String PROFILE_TEMPLATE_FILE = PROFILE_FILE + ".example"; + private static final String PROFILE_FILE = "forge.profile.properties"; // data that is only in the program dir private static final String _ASSETS_ROOT = Gdx.app.getType() == ApplicationType.Desktop ? "bin/" : "assets/"; @@ -41,28 +43,40 @@ public final class Constants { private static final String _QUEST_DIR = _ASSETS_ROOT + "quest/"; public static final String TEXT_HOWTO_FILE = _ASSETS_ROOT + "howto.txt"; public static final String DRAFT_RANKINGS_FILE = _ASSETS_ROOT + "draft/rankings.txt"; - public static final String PRICES_BOOSTER_FILE = _QUEST_DIR + "booster-prices.txt"; - public static final String BAZAAR_FILE = _QUEST_DIR + "bazaar/index.xml"; public static final String CARD_DATA_DIR = _ASSETS_ROOT + "cardsfolder/"; public static final String EDITIONS_DIR = _ASSETS_ROOT + "editions/"; public static final String BLOCK_DATA_DIR = _ASSETS_ROOT + "blockdata/"; public static final String DECK_CUBE_DIR = _ASSETS_ROOT + "cube"; public static final String AI_PROFILE_DIR = _ASSETS_ROOT + "ai"; + public static final String NO_CARD_FILE = _ASSETS_ROOT + "defaults/no_card.jpg"; public static final String QUEST_WORLD_DIR = _QUEST_DIR + "world/"; public static final String QUEST_PRECON_DIR = _QUEST_DIR + "precons/"; + public static final String PRICES_BOOSTER_FILE = _QUEST_DIR + "booster-prices.txt"; + public static final String BAZAAR_FILE = _QUEST_DIR + "bazaar/index.xml"; public static final String CARD_DATA_PETS_DIR = _QUEST_DIR + "bazaar/"; public static final String DEFAULT_DUELS_DIR = _QUEST_DIR + "duels"; public static final String DEFAULT_CHALLENGES_DIR = _QUEST_DIR + "challenges"; // data tree roots - public static final String USER_DIR = "userData/"; - public static final String CACHE_DIR = "cache/"; - public static final int SERVER_PORT_NUMBER = 0; + public static final String USER_DIR; + public static final String CACHE_DIR; + public static final String CACHE_CARD_PICS_DIR; + public static final Map CACHE_CARD_PICS_SUBDIR; + public static final int SERVER_PORT_NUMBER; + static { + ForgeProfileProperties profileProps = new ForgeProfileProperties(PROFILE_FILE); + USER_DIR = profileProps.userDir; + CACHE_DIR = profileProps.cacheDir; + CACHE_CARD_PICS_DIR = profileProps.cardPicsDir; + CACHE_CARD_PICS_SUBDIR = Collections.unmodifiableMap(profileProps.cardPicsSubDir); + SERVER_PORT_NUMBER = profileProps.serverPort; + } // data that is only in the profile dirs public static final String USER_QUEST_DIR = USER_DIR + "quest/"; public static final String USER_PREFS_DIR = USER_DIR + "preferences/"; + public static final String USER_GUANTLET_DIR = USER_DIR + "gauntlet/"; public static final String LOG_FILE = USER_DIR + "forge.log"; public static final String DECK_BASE_DIR = USER_DIR + "decks/"; public static final String DECK_CONSTRUCTED_DIR = DECK_BASE_DIR + "constructed/"; @@ -72,7 +86,7 @@ public final class Constants { public static final String DECK_PLANE_DIR = DECK_BASE_DIR + "planar/"; public static final String DECK_COMMANDER_DIR = DECK_BASE_DIR + "commander/"; public static final String QUEST_SAVE_DIR = USER_QUEST_DIR + "saves/"; - public static final String MAIN_PREFS_FILE = USER_PREFS_DIR + "forge.preferences"; + public static final String MAIN_PREFS_FILE = USER_PREFS_DIR + "forge.m.preferences"; public static final String CARD_PREFS_FILE = USER_PREFS_DIR + "card.preferences"; public static final String DECK_PREFS_FILE = USER_PREFS_DIR + "deck.preferences"; public static final String QUEST_PREFS_FILE = USER_PREFS_DIR + "quest.preferences"; @@ -93,7 +107,9 @@ public final class Constants { public static final String[] PROFILE_DIRS = { USER_DIR, CACHE_DIR, + CACHE_CARD_PICS_DIR, USER_PREFS_DIR, + USER_GUANTLET_DIR, DB_DIR, DECK_CONSTRUCTED_DIR, DECK_DRAFT_DIR, diff --git a/forge-m-base/src/forge/utils/ForgeProfileProperties.java b/forge-m-base/src/forge/utils/ForgeProfileProperties.java new file mode 100644 index 00000000000..3e3a043e2f3 --- /dev/null +++ b/forge-m-base/src/forge/utils/ForgeProfileProperties.java @@ -0,0 +1,133 @@ +/* + * Forge: Play Magic: the Gathering. + * Copyright (C) 2011 Forge Team + * + * 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 . + */ +package forge.utils; + +import forge.util.FileSection; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.tuple.Pair; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.util.Map; +import java.util.Properties; + +/** + * Determines the user data and cache dirs, first looking at the specified file for overrides + * then falling back to platform-specific defaults. Resulting dir strings are guaranteed to end in a slash + * so they can be easily appended with further path elements. + */ +public class ForgeProfileProperties { + public final String userDir; + public final String cacheDir; + public final String cardPicsDir; + public final Map cardPicsSubDir; + public final int serverPort; + + private static final String _USER_DIR_KEY = "userDir"; + private static final String _CACHE_DIR_KEY = "cacheDir"; + private static final String _CARD_PICS_DIR_KEY = "cardPicsDir"; + private static final String _CARD_PICS_SUB_DIRS_KEY = "cardPicsSubDirs"; + + private static final String _SERVER_PORT = "serverPort"; + + public ForgeProfileProperties(String filename) { + Properties props = new Properties(); + File propFile = new File(filename); + try { + if (propFile.canRead()) { + props.load(new FileInputStream(propFile)); + } + } catch (IOException e) { + System.err.println("error while reading from profile properties file: " + filename); + } + + Pair defaults = _getDefaultDirs(); + userDir = _getDir(props, _USER_DIR_KEY, defaults.getLeft()); + cacheDir = _getDir(props, _CACHE_DIR_KEY, defaults.getRight()); + cardPicsDir = _getDir(props, _CARD_PICS_DIR_KEY, cacheDir + "pics/cards/"); + cardPicsSubDir = _getMap(props, _CARD_PICS_SUB_DIRS_KEY); + serverPort = _getInt(props, _SERVER_PORT, 0); + } + + private Map _getMap(Properties props, String propertyKey) { + String strMap = props.getProperty(propertyKey, "").trim(); + return FileSection.parseToMap(strMap, "->", "|"); + } + + private int _getInt(Properties props, String propertyKey, int defaultValue) { + String strValue = props.getProperty(propertyKey, "").trim(); + if ( StringUtils.isNotBlank(strValue) && StringUtils.isNumeric(strValue) ) + return Integer.parseInt(strValue); + return defaultValue; + } + + private static String _getDir(Properties props, String propertyKey, String defaultVal) { + String retDir = props.getProperty(propertyKey, defaultVal).trim(); + if (retDir.isEmpty()) { + // use default if dir is "defined" as an empty string in the properties file + retDir = defaultVal; + } + + // canonicalize + retDir = new File(retDir).getAbsolutePath(); + + // ensure path ends in a slash + if (File.separatorChar == retDir.charAt(retDir.length() - 1)) { + return retDir; + } + return retDir + File.separatorChar; + } + + // returns a pair + private static Pair _getDefaultDirs() { + String osName = System.getProperty("os.name"); + String homeDir = System.getProperty("user.home"); + + if (StringUtils.isEmpty(osName) || StringUtils.isEmpty(homeDir)) { + throw new RuntimeException("cannot determine OS and user home directory"); + } + + String fallbackDataDir = String.format("%s/.forge", homeDir); + + if (StringUtils.containsIgnoreCase(osName, "windows")) { + // the split between appdata and localappdata on windows is relatively recent. If + // localappdata is not defined, use appdata for both. and if appdata is not defined, + // fall back to a linux-style dot dir in the home directory + String appRoot = System.getenv().get("APPDATA"); + if (StringUtils.isEmpty(appRoot)) { + appRoot = fallbackDataDir; + } + String cacheRoot = System.getenv().get("LOCALAPPDATA"); + if (StringUtils.isEmpty(cacheRoot)) { + cacheRoot = appRoot; + } + // just use '/' everywhere instead of file.separator. it always works + // the cache dir is Forge/Cache instead of just Forge since appRoot and cacheRoot might be the + // same directory on windows and we need to distinguish them. + return Pair.of(String.format("%s/Forge", appRoot), + String.format("%s/Forge/Cache", cacheRoot)); + } else if (StringUtils.containsIgnoreCase(osName, "mac os x")) { + return Pair.of(String.format("%s/Library/Application Support/Forge", homeDir), + String.format("%s/Library/Caches/Forge", homeDir)); + } + + // Linux and everything else + return Pair.of(fallbackDataDir, String.format("%s/.cache/forge", homeDir)); + } +}