diff --git a/gemp-lotr/gemp-lotr-logic/src/main/java/com/gempukku/lotro/game/LotroCardBlueprintLibrary.java b/gemp-lotr/gemp-lotr-logic/src/main/java/com/gempukku/lotro/game/LotroCardBlueprintLibrary.java index 352280a3b..07182798e 100644 --- a/gemp-lotr/gemp-lotr-logic/src/main/java/com/gempukku/lotro/game/LotroCardBlueprintLibrary.java +++ b/gemp-lotr/gemp-lotr-logic/src/main/java/com/gempukku/lotro/game/LotroCardBlueprintLibrary.java @@ -90,6 +90,14 @@ public class LotroCardBlueprintLibrary { return Collections.unmodifiableMap(_allSets); } + public Map getAllMappings() { + return Collections.unmodifiableMap(_blueprintMapping); + } + + public Map> getFullMappings() { + return Collections.unmodifiableMap(_fullBlueprintMapping); + } + public void reloadAllDefinitions() { reloadSets(); reloadMappings(); diff --git a/gemp-lotr/gemp-lotr-logic/src/main/java/com/gempukku/lotro/game/LotroCardBlueprintLibrary2.java b/gemp-lotr/gemp-lotr-logic/src/main/java/com/gempukku/lotro/game/LotroCardBlueprintLibrary2.java new file mode 100644 index 000000000..0049d5475 --- /dev/null +++ b/gemp-lotr/gemp-lotr-logic/src/main/java/com/gempukku/lotro/game/LotroCardBlueprintLibrary2.java @@ -0,0 +1,479 @@ +package com.gempukku.lotro.game; + +import com.gempukku.lotro.cards.build.InvalidCardDefinitionException; +import com.gempukku.lotro.cards.build.LotroCardBlueprintBuilder; +import com.gempukku.lotro.common.AppConfig; +import com.gempukku.lotro.common.JSONDefs; +import com.gempukku.lotro.game.packs.DefaultSetDefinition; +import com.gempukku.lotro.game.packs.SetDefinition; +import com.gempukku.lotro.logic.GameUtils; +import com.gempukku.util.JsonUtils; +import org.apache.commons.io.IOUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.hjson.JsonValue; +import org.json.simple.JSONObject; +import org.json.simple.parser.JSONParser; +import org.json.simple.parser.ParseException; + +import java.io.*; +import java.lang.reflect.InvocationTargetException; +import java.nio.charset.StandardCharsets; +import java.util.*; +import java.util.concurrent.Semaphore; + +public class LotroCardBlueprintLibrary2 { + private static final Logger logger = LogManager.getLogger(LotroCardBlueprintLibrary2.class); + + private final String[] _packageNames = + new String[]{ + "", ".dwarven", ".dunland", ".elven", ".fallenRealms", ".gandalf", ".gollum", ".gondor", ".isengard", ".men", ".orc", + ".raider", ".rohan", ".moria", ".wraith", ".sauron", ".shire", ".site", ".uruk_hai", + + //Additional Hobbit Draft packages + ".esgaroth", ".gundabad", ".smaug", ".spider", ".troll" + }; + private final Map _blueprints = new HashMap<>(); + private final Map _blueprintMapping = new HashMap<>(); + private final Map> _fullBlueprintMapping = new HashMap<>(); + private final Map _allSets = new LinkedHashMap<>(); + + private final LotroCardBlueprintBuilder cardBlueprintBuilder = new LotroCardBlueprintBuilder(); + + private final Semaphore collectionReady = new Semaphore(1); + private final File _cardPath; + private final File _mappingsPath; + private final File _setDefsPath; + private final File _raritiesFolder; + + private final List _refreshCallbacks = new ArrayList<>(); + + public LotroCardBlueprintLibrary2() { + this(AppConfig.getCardsPath(), AppConfig.getMappingsPath(), AppConfig.getSetDefinitionsPath(), AppConfig.getResourceFile("rarities")); + } + + public LotroCardBlueprintLibrary2(File cardsPath, File mappingsPath, File setDefinitionPath, File raritiesFolder) { + _cardPath = cardsPath; + _mappingsPath = mappingsPath; + _setDefsPath = setDefinitionPath; + _raritiesFolder = raritiesFolder; + logger.info("Locking blueprint library in constructor"); + //This will be released after the library has been init'd; until then all functional uses should block + collectionReady.acquireUninterruptibly(); + logger.info("Unlocking blueprint library in constructor"); + + loadSets(); + loadMappings(); + loadCards(_cardPath, true); + cacheAllJavaBlueprints(); + collectionReady.release(); + } + + public boolean SubscribeToRefreshes(ICallback callback) { + if(_refreshCallbacks.contains(callback)) + return false; + + _refreshCallbacks.add(callback); + + return true; + } + + public boolean UnsubscribeFromRefreshes(ICallback callback) { + if(!_refreshCallbacks.contains(callback)) + return false; + + _refreshCallbacks.remove(callback); + + return true; + } + + public Map getSetDefinitions() { + return Collections.unmodifiableMap(_allSets); + } + + public Map getAllMappings() { + return Collections.unmodifiableMap(_blueprintMapping); + } + + public Map> getFullMappings() { + return Collections.unmodifiableMap(_fullBlueprintMapping); + } + + public void reloadAllDefinitions() { + reloadSets(); + reloadMappings(); + reloadCards(); + errataMappings = null; + getErrata(); + + for(var callback : _refreshCallbacks) { + callback.Invoke(); + } + } + + private void reloadSets() { + try { + collectionReady.acquire(); + loadSets(); + collectionReady.release(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + + private void reloadMappings() { + try { + collectionReady.acquire(); + loadMappings(); + collectionReady.release(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + + private void reloadCards() { + try { + collectionReady.acquire(); + loadCards(_cardPath, false); + collectionReady.release(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + + private void loadSets() { + try { + final InputStreamReader reader = new InputStreamReader(new FileInputStream(_setDefsPath), StandardCharsets.UTF_8); + try { + var setDefs = JsonUtils.ConvertArray(reader, JSONDefs.Set.class); + + for (JSONDefs.Set def : setDefs) { + if (def == null) + continue; + + var set = new DefaultSetDefinition(def); + readSetRarityFile(set, set.getSetId(), def.rarityFile); + _allSets.put(set.getSetId(), set); + } + + } finally { + IOUtils.closeQuietly(reader); + } + } catch (IOException exp) { + throw new RuntimeException("Unable to read card rarities: " + exp); + } catch (Exception exp) { + throw new RuntimeException("Unable to parse setConfig.hjson file: " + exp); + } + } + + private void loadMappings() { + try { + try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(new FileInputStream(_mappingsPath), StandardCharsets.UTF_8))) { + String line; + + _blueprintMapping.clear(); + _fullBlueprintMapping.clear(); + + while ((line = bufferedReader.readLine()) != null) { + if (!line.startsWith("#")) { + String[] split = line.split(","); + _blueprintMapping.put(split[0], split[1]); + addAlternatives(split[0], split[1]); + } + } + } + } catch (IOException exp) { + throw new RuntimeException("Problem loading blueprintMapping.txt", exp); + } + } + + private void loadCards(File path, boolean initial) { + if (path.isFile()) { + loadCardsFromFile(path, initial); + } + else if (path.isDirectory()) { + for (File file : path.listFiles()) { + loadCards(file, initial); + } + } + } + + private void loadCardsFromFile(File file, boolean validateNew) { + if (!JsonUtils.IsValidHjsonFile(file)) + return; + + JSONParser parser = new JSONParser(); + try (Reader reader = new InputStreamReader(new FileInputStream(file), StandardCharsets.UTF_8)) { + //This will read both json and hjson, producing standard json + String json = JsonValue.readHjson(reader).toString(); + final JSONObject cardsFile = (JSONObject) parser.parse(json); + final Set> cardsInFile = cardsFile.entrySet(); + for (Map.Entry cardEntry : cardsInFile) { + String blueprintId = cardEntry.getKey(); + if (validateNew && _blueprints.containsKey(blueprintId)) { + logger.error(blueprintId + " from " + + file.getAbsolutePath() + " - Replacing existing card definition!"); + } + final JSONObject cardDefinition = cardEntry.getValue(); + try { + final var lotroCardBlueprint = cardBlueprintBuilder.buildFromJson(cardDefinition); + lotroCardBlueprint.setId(blueprintId); + _blueprints.put(blueprintId, lotroCardBlueprint); + } catch (InvalidCardDefinitionException exp) { + logger.error("Unable to load card " + blueprintId + + " from " + file.getAbsolutePath(), exp); + } + } + } catch (FileNotFoundException exp) { + logger.error("Failed to find file " + file.getAbsolutePath(), exp); + } catch (IOException exp) { + logger.error("Error while loading file " + file.getAbsolutePath(), exp); + } catch (ParseException exp) { + logger.error("Failed to parse file " + file.getAbsolutePath(), exp); + } + catch (Exception exp) { + logger.error("Unexpected error while parsing file " + file.getAbsolutePath(), exp); + } + logger.debug("Loaded JSON card file " + file.getName()); + } + + private void cacheAllJavaBlueprints() { + for (var setDef : _allSets.values()) { + if(!setDef.NeedsLoading()) + continue; + + logger.debug("Loading Java cards for set " + setDef.getSetId()); + final Set allCards = setDef.getAllCards(); + for (String blueprintId : allCards) { + if (getBaseBlueprintId(blueprintId).equals(blueprintId)) { + if (!_blueprints.containsKey(blueprintId)) { + try { + // Ensure it's loaded + LotroCardBlueprint blueprint = findJavaBlueprint(blueprintId); + _blueprints.put(blueprintId, blueprint); + } catch (CardNotFoundException exp) { + throw new RuntimeException("Unable to start the server, due to invalid (missing) card definition - " + blueprintId); + } + } + } + } + } + } + + public String getBaseBlueprintId(String blueprintId) { + blueprintId = stripBlueprintModifiers(blueprintId); + String base = _blueprintMapping.get(blueprintId); + if (base != null) + return base; + return blueprintId; + } + + private void addAlternatives(String newBlueprint, String existingBlueprint) { + Set existingAlternates = _fullBlueprintMapping.get(existingBlueprint); + if (existingAlternates != null) { + for (String existingAlternate : existingAlternates) { + addAlternative(newBlueprint, existingAlternate); + addAlternative(existingAlternate, newBlueprint); + } + } + addAlternative(newBlueprint, existingBlueprint); + addAlternative(existingBlueprint, newBlueprint); + } + + private void addAlternative(String from, String to) { + Set list = _fullBlueprintMapping.get(from); + if (list == null) { + list = new HashSet<>(); + _fullBlueprintMapping.put(from, list); + } + list.add(to); + } + + public Map getBaseCards() { + try { + collectionReady.acquire(); + var data = Collections.unmodifiableMap(_blueprints); + collectionReady.release(); + return data; + } catch (InterruptedException exp) { + throw new RuntimeException("LotroCardBlueprintLibrary.getBaseCard() interrupted: ", exp); + } + } + + public Set getAllAlternates(String blueprintId) { + try { + collectionReady.acquire(); + var data = _fullBlueprintMapping.get(blueprintId); + collectionReady.release(); + return data; + } catch (InterruptedException exp) { + throw new RuntimeException("LotroCardBlueprintLibrary.getAllAlternates() interrupted: ", exp); + } + } + + private Map errataMappings = null; + public Map getErrata() { + try { + if(errataMappings == null) { + collectionReady.acquire(); + errataMappings = new HashMap<>(); + for (String id : _blueprints.keySet()) { + var parts = id.split("_"); + int setID = Integer.parseInt(parts[0]); + String cardID = parts[1]; + JSONDefs.ErrataInfo card = null; + String base = id; + if(setID >= 50 && setID <= 69) { + base = "" + (setID - 50) + "_" + cardID; + } + else if(setID >= 70 && setID <= 89) { + base = "" + (setID - 70) + "_" + cardID; + } + else if(setID >= 150 && setID <= 199) { + base = "" + (setID - 50) + "_" + cardID; + } + else + continue; + + if(errataMappings.containsKey(base)) { + card = errataMappings.get(base); + } + else { + var basecard = _blueprints.get(base); + + //This should only really happen when errata IDs are made + //that do not line up with their official counterparts, such + //as when making multiple errata candidates. + if(basecard == null) + continue; + card = new JSONDefs.ErrataInfo(); + card.BaseID = base; + card.Name = GameUtils.getFullName(basecard); + card.LinkText = GameUtils.getDeluxeCardLink(id, basecard); + card.ErrataIDs = new HashMap<>(); + errataMappings.put(base, card); + + } + + card.ErrataIDs.put(JSONDefs.ErrataInfo.PC_Errata, id); + } + + collectionReady.release(); + } + return errataMappings; + } catch (InterruptedException exp) { + throw new RuntimeException("LotroCardBlueprintLibrary.getErrata() interrupted: ", exp); + } + } + + public boolean hasAlternateInSet(String blueprintId, int setNo) { + try { + collectionReady.acquire(); + var alternatives = _fullBlueprintMapping.get(blueprintId); + collectionReady.release(); + + if (alternatives != null) + for (String alternative : alternatives) + if (alternative.startsWith(setNo + "_")) + return true; + + return false; + } catch (InterruptedException exp) { + throw new RuntimeException("LotroCardBlueprintLibrary.hasAlternateInSet() interrupted: ", exp); + } + } + + public LotroCardBlueprint getLotroCardBlueprint(String blueprintId) throws CardNotFoundException { + blueprintId = stripBlueprintModifiers(blueprintId); + LotroCardBlueprint bp = null; + + try { + collectionReady.acquire(); + if (_blueprints.containsKey(blueprintId)) { + bp = _blueprints.get(blueprintId); + } + collectionReady.release(); + + if(bp != null) + return bp; + + return findJavaBlueprint(blueprintId); + } catch (InterruptedException exp) { + throw new RuntimeException("LotroCardBlueprintLibrary.getLotroCardBlueprint() interrupted: ", exp); + } + } + + public String stripBlueprintModifiers(String blueprintId) { + if (blueprintId.endsWith("*")) + blueprintId = blueprintId.substring(0, blueprintId.length() - 1); + if (blueprintId.endsWith("T")) + blueprintId = blueprintId.substring(0, blueprintId.length() - 1); + return blueprintId; + } + + private LotroCardBlueprint findJavaBlueprint(String blueprintId) throws CardNotFoundException { + if (_blueprintMapping.containsKey(blueprintId)) + return getLotroCardBlueprint(_blueprintMapping.get(blueprintId)); + + String[] blueprintParts = blueprintId.split("_"); + + String setNumber = blueprintParts[0]; + String cardNumber = blueprintParts[1]; + + for (String packageName : _packageNames) { + LotroCardBlueprint blueprint; + try { + blueprint = tryLoadingFromPackage(packageName, setNumber, cardNumber); + } catch (IllegalAccessException | InstantiationException | NoSuchMethodException e) { + throw new CardNotFoundException(blueprintId); + } + if (blueprint != null) + return blueprint; + } + + throw new CardNotFoundException(blueprintId); + } + + private LotroCardBlueprint tryLoadingFromPackage(String packageName, String setNumber, String cardNumber) throws IllegalAccessException, InstantiationException, NoSuchMethodException { + try { + Class clazz = Class.forName("com.gempukku.lotro.cards.set" + setNumber + packageName + ".Card" + setNumber + "_" + normalizeId(cardNumber)); + return (LotroCardBlueprint) clazz.getDeclaredConstructor().newInstance(); + } catch (ClassNotFoundException | InvocationTargetException e) { + // Ignore + return null; + } + } + + private String normalizeId(String blueprintPart) { + int id = Integer.parseInt(blueprintPart); + if (id < 10) + return "00" + id; + else if (id < 100) + return "0" + id; + else + return String.valueOf(id); + } + + private void readSetRarityFile(DefaultSetDefinition rarity, String setNo, String rarityFile) throws IOException { + BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(new FileInputStream(new File(_raritiesFolder, rarityFile)), StandardCharsets.UTF_8)); + try { + String line; + + while ((line = bufferedReader.readLine()) != null) { + String blueprintId = setNo + "_" + line.substring(setNo.length() + 1); + if (line.endsWith("T")) { + if (!line.startsWith(setNo)) + throw new IllegalStateException("Seems the rarity is for some other set"); + rarity.addTengwarCard(blueprintId); + } else { + if (!line.startsWith(setNo)) + throw new IllegalStateException("Seems the rarity is for some other set"); + String cardRarity = line.substring(setNo.length(), setNo.length() + 1); + rarity.addCard(blueprintId, cardRarity); + } + } + } finally { + IOUtils.closeQuietly(bufferedReader); + } + } +} diff --git a/gemp-lotr/gemp-lotr-server/src/test/java/com/gempukku/lotro/library/LotroCardBlueprintLibraryTests.java b/gemp-lotr/gemp-lotr-server/src/test/java/com/gempukku/lotro/library/LotroCardBlueprintLibraryTests.java index 33f094324..525f39b61 100644 --- a/gemp-lotr/gemp-lotr-server/src/test/java/com/gempukku/lotro/library/LotroCardBlueprintLibraryTests.java +++ b/gemp-lotr/gemp-lotr-server/src/test/java/com/gempukku/lotro/library/LotroCardBlueprintLibraryTests.java @@ -4,11 +4,16 @@ import com.gempukku.lotro.at.AbstractAtTest; import com.gempukku.lotro.common.CardType; import com.gempukku.lotro.game.CardNotFoundException; import com.gempukku.lotro.game.LotroCardBlueprint; +import com.gempukku.lotro.game.LotroCardBlueprintLibrary2; import com.gempukku.lotro.logic.GameUtils; import org.junit.Test; import java.util.HashMap; +import java.util.HashSet; import java.util.Map; +import java.util.Set; + +import static org.junit.Assert.*; public class LotroCardBlueprintLibraryTests extends AbstractAtTest { @Test @@ -38,4 +43,182 @@ public class LotroCardBlueprintLibraryTests extends AbstractAtTest { } } + + private void RecordDiff(Set diffs, String message) { + diffs.add(message); + System.out.println(message); + } + + @Test + public void NewLibraryMapsSameAsOld() { + var oldLib = _cardLibrary; + var newLib = new LotroCardBlueprintLibrary2(); + + //_blueprints export, should be 3,878 card representations, + // including base cards, reprints, errata, etc. + var oldBlueprints = oldLib.getBaseCards(); + var newBlueprints = newLib.getBaseCards(); + assertEquals(oldBlueprints.size(), newBlueprints.size()); + + System.out.println("\n========\n"); + + //These are unlikely to differ between the two as we are not altering card loading. + //Thus these are asserts to halt the testing process if we are discovered to have + // violated that assumption. + for(var bp : oldBlueprints.keySet()) { + assertTrue("Blueprint found in the old library but not the new: " + bp, + newBlueprints.containsKey(bp)); + } + + for(var bp : newBlueprints.keySet()) { + assertTrue("Blueprint found in the new library but not the old: " + bp, + oldBlueprints.containsKey(bp)); + } + + System.out.println("\n========\n"); + + //_blueprintMapping, should be 288 one-way mappings of reprints and + // AIs, as contained in the blueprintMappings.txt file + //This might be obsolete, as the point of this field is to represent that file. + var oldMappings = oldLib.getAllMappings(); + var newMappings = newLib.getAllMappings(); + assertEquals(oldMappings.size(), newMappings.size()); + + //These mappings however are likely to fluctuate as development occurs. Thus + // we will instead track the differences and display them, rather than asserting on them. + var mapDiffs = new HashSet(); + + for(var bp : oldMappings.keySet()) { + if(!newMappings.containsKey(bp)) { + RecordDiff(mapDiffs, + "Mapping in old lib but not new: " + bp + " > " + oldMappings.get(bp)); + } + } + + for(var bp : newMappings.keySet()) { + if(!oldMappings.containsKey(bp)) { + RecordDiff(mapDiffs, + "Mapping in new lib but not old: " + bp + " > " + newMappings.get(bp)); + } + } + + + System.out.println("\n========\n"); + + //_fullBlueprintMapping, should be 545 bi-directional associations + // between cards and their reprints/errata/AIs + var oldFullMapping = oldLib.getFullMappings(); + var newFullMapping = newLib.getFullMappings(); + assertEquals(oldFullMapping.size(), newFullMapping.size()); + + //Even if the _mappings is eventually discarded as irrelevant, we will probably + // always need a way of discovering associations between equivalent cards. + var fullMapDiffs = new HashSet(); + + //First check that no mappings are missing in the new one + for(var bp : oldFullMapping.keySet()) { + if(!newFullMapping.containsKey(bp)) { + RecordDiff(fullMapDiffs, + "Full mapping in old lib but not new: " + bp + " > " + + String.join(", ", oldFullMapping.get(bp))); + } + else { + var newmaps = newFullMapping.get(bp); + for(var mappedBP : oldFullMapping.get(bp)) { + if(!newmaps.contains(mappedBP)) { + RecordDiff(fullMapDiffs, + "Full map entry in old lib but not new: " + bp + " > " + mappedBP); + } + } + } + } + + //Then check that no additional mappings were added to the new + for(var bp : newFullMapping.keySet()) { + if(!oldFullMapping.containsKey(bp)) { + RecordDiff(fullMapDiffs, + "Full mapping in new lib but not old: " + bp + " > " + + String.join(", ", newFullMapping.get(bp))); + } + else { + var oldmaps = oldFullMapping.get(bp); + for(var mappedBP : newFullMapping.get(bp)) { + if(!oldmaps.contains(mappedBP)) { + RecordDiff(fullMapDiffs, + "Full map entry in new lib but not old: " + bp + " > " + mappedBP); + } + } + } + } + + System.out.println("\n========\n"); + + //_allSets, should be 59 sets as loaded from the setConfig.hjson file + // (which should really be a folder search). Unlike the mappings map, + // this only starts as a representation of that file and gains additional + // data, including the rarities nonsense. + var oldSets = oldLib.getSetDefinitions(); + var newSets = newLib.getSetDefinitions(); + assertEquals(oldFullMapping.size(), newFullMapping.size()); + + var setDiffs = new HashSet(); + var setContentDiffs = new HashSet(); + + for(var setId : oldSets.keySet()) { + if(!newSets.containsKey(setId)) { + RecordDiff(setDiffs, + "Set in old lib but not new: " + setId ); + } + else { + var oldset = oldSets.get(setId); + var newset = newSets.get(setId); + + var oldcards = oldset.getAllCards(); + var newcards = newset.getAllCards(); + + for(var bp : oldcards) { + if(!newcards.contains(bp)) { + RecordDiff(setContentDiffs, + "Card in old set " + setId + " but not new: " + bp); + } + } + } + } + + for(var setId : newSets.keySet()) { + if(!oldSets.containsKey(setId)) { + RecordDiff(setDiffs, + "Set in new lib but not old: " + setId); + } + else { + var oldset = oldSets.get(setId); + var newset = newSets.get(setId); + + var oldcards = oldset.getAllCards(); + var newcards = newset.getAllCards(); + + for(var bp : newcards) { + if(!oldcards.contains(bp)) { + RecordDiff(setContentDiffs, + "Card in new set " + setId + " but not old: " + bp); + } + } + } + } + + + //Results + + System.out.println("\n\nLibrary Comparison results:\n\n========\n"); + System.out.println(oldBlueprints.size() - newBlueprints.size() + " total differences in card blueprints loaded."); + System.out.println(mapDiffs.size() + " total differences in the bare mappings."); + System.out.println(fullMapDiffs.size() + " total differences in the full mappings."); + System.out.println(setDiffs.size() + " total differences in sets loaded."); + System.out.println(setContentDiffs.size() + " total differences in set contents."); + System.out.println("\n========"); + + int total = mapDiffs.size() + fullMapDiffs.size() + setDiffs.size() + setContentDiffs.size(); + System.out.println("\nGrand total of differences: " + total); + assertEquals(0, total); + } }