Adding temporary blueprint library loader, with a test battery to compare the output of the two for imminent development.

This commit is contained in:
Christian 'ketura' McCarty
2024-04-15 22:48:04 -05:00
parent 573270e7b6
commit a28af6b7b5
3 changed files with 670 additions and 0 deletions

View File

@@ -90,6 +90,14 @@ public class LotroCardBlueprintLibrary {
return Collections.unmodifiableMap(_allSets);
}
public Map<String, String> getAllMappings() {
return Collections.unmodifiableMap(_blueprintMapping);
}
public Map<String, Set<String>> getFullMappings() {
return Collections.unmodifiableMap(_fullBlueprintMapping);
}
public void reloadAllDefinitions() {
reloadSets();
reloadMappings();

View File

@@ -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<String, LotroCardBlueprint> _blueprints = new HashMap<>();
private final Map<String, String> _blueprintMapping = new HashMap<>();
private final Map<String, Set<String>> _fullBlueprintMapping = new HashMap<>();
private final Map<String, SetDefinition> _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<ICallback> _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<String, SetDefinition> getSetDefinitions() {
return Collections.unmodifiableMap(_allSets);
}
public Map<String, String> getAllMappings() {
return Collections.unmodifiableMap(_blueprintMapping);
}
public Map<String, Set<String>> 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<Map.Entry<String, JSONObject>> cardsInFile = cardsFile.entrySet();
for (Map.Entry<String, JSONObject> 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<String> 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<String> 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<String> list = _fullBlueprintMapping.get(from);
if (list == null) {
list = new HashSet<>();
_fullBlueprintMapping.put(from, list);
}
list.add(to);
}
public Map<String, LotroCardBlueprint> 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<String> 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<String, JSONDefs.ErrataInfo> errataMappings = null;
public Map<String, JSONDefs.ErrataInfo> 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);
}
}
}

View File

@@ -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<String> 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<String>();
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<String>();
//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<String>();
var setContentDiffs = new HashSet<String>();
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);
}
}