Gametext loading in hjson cards, tweaks to card loading

- Added game text, lore, and promo text processors for hjson card loading
- Tweaked title hjson field loading to deprecate the * in titles for uniqueness
- Added an ID field to the base blueprint so that blueprint references are aware of what their own ID is
- Added population of the blueprint ID on creation for both hjson and java cards
- Fixed bad version of a dependency (was using a RC)
This commit is contained in:
Christian 'ketura' McCarty
2024-04-15 22:45:43 -05:00
parent 942a1698ab
commit 573270e7b6
12 changed files with 293 additions and 44 deletions

View File

@@ -12,8 +12,8 @@
<dependencies>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava-collections</artifactId>
<version>r03</version>
<artifactId>guava</artifactId>
<version>33.1.0-jre</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>

View File

@@ -0,0 +1,100 @@
package com.gempukku.lotro.common;
import java.text.Normalizer;
public class GameText {
public static String ConvertTextToHTML(String text) {
String html = GameText.ConvertNanDeckNotation(text);
html = GameText.ConvertAnnotatedHTMLToFormattedHTML(html);
html = GameText.ConvertPlainQuotesToSmartQuotes(html);
html = Names.AddAccentsToPlainText(html);
return html;
}
public static String ConvertNanDeckNotation(String text) {
return text
//Replacing a single backslash and other line breaks
.replaceAll("\\\\", "<br>")
.replaceAll("\n", "<br>")
.replaceAll("\\n", "<br>")
//non-breaking space
.replaceAll("_", "&nbsp;")
//em dash helper
.replaceAll("---", "")
//en dash helper
.replaceAll("--", "")
//en dash instead of hyphen in front of negative numbers
.replaceAll("-(\\d)", "$1");
}
public static String ConvertAnnotatedHTMLToFormattedHTML(String text) {
return text
.replaceAll("<helper>", "<i>")
.replaceAll("</helper>", "</i>")
.replaceAll("<keyword>", "<b>")
.replaceAll("</keyword>", "</b>")
.replaceAll("<phase>", "<b>")
.replaceAll("</phase>", "</b>")
.replaceAll("<br/>", "<br>");
//TODO: symbol replacement
}
public static String ConvertPlainQuotesToSmartQuotes(String text) {
return text
//Quote characters at the beginning and end of the string are unambiguous
.replaceAll("^'", "")
.replaceAll("'$", "")
.replaceAll("^\"", "")
.replaceAll("\"$", "")
//If a space is on one side, also unambiguous
.replaceAll(" '", " ")
.replaceAll("' ", " ")
.replaceAll(" \"", "")
.replaceAll("\" ", "")
//When next to punctuation, it must be an end quote
.replaceAll("([.,?!/])'", "$1")
.replaceAll("([.,?!/])\"", "$1”")
//If we've figured out one of the smart quotes and it's right next
// to a dumb quote, we can use that to figure out the other
.replaceAll("\"", "“‘")
.replaceAll("\"", "”’")
.replaceAll("\"", "‘“")
.replaceAll("\"", "’”")
.replaceAll("'“", "‘“")
.replaceAll("'”", "’”")
.replaceAll("“'", "“‘")
.replaceAll("”'", "”’")
//Possessives (the s' case is already handled above)
.replaceAll("'s", "s")
//If any pair of dumb quotes has evaded the above, we will assume they
// are open or closed depending on order
.replaceAll("\"'", "“‘")
.replaceAll("'\"", "’”")
//Any remaining single quotes must be in a contraction like I'm
.replaceAll("'", "")
//Any remaining double quotes are just asking for it
.replaceAll("\"", "");
}
public static String ConvertSmartQuotesToPlainQuotes(String text) {
return text
.replaceAll("", "'")
.replaceAll("", "'")
.replaceAll("", "\"")
.replaceAll("", "\"");
}
public static String SanitizeHTMLToSearchText(String text) {
String output = Normalizer.normalize(text, Normalizer.Form.NFD)
//Removes any unicode "combining mark", aka diacritics
.replaceAll("\\p{M}", "");
output = ConvertSmartQuotesToPlainQuotes(output);
return output
.replaceAll("</?b>", "")
.replaceAll("</?i>", "")
.replaceAll("\\s*<br/?>\\s*", " ");
}
}

View File

@@ -25,6 +25,7 @@ public class Names {
.replaceAll("", "'")
.replaceAll("", "\"")
.replaceAll("", "\"")
//Removes any unicode "combining mark", aka diacritics
.replaceAll("\\p{M}", "")
.replaceAll(" ", "")
.replaceAll("_", "")
@@ -37,6 +38,73 @@ public class Names {
.replaceAll("", "'")
.replaceAll("", "\"")
.replaceAll("", "\"")
//Removes any unicode "combining mark", aka diacritics
.replaceAll("\\p{M}", "");
}
public static String AddAccentsToPlainText(String text) {
return text
.replaceAll("Anarion", "Anárion")
.replaceAll("Anduril", "Andúril")
.replaceAll("Annun", "Annûn")
.replaceAll("Attea", "Attëa")
.replaceAll("Barad-dur", "Barad-dûr")
.replaceAll("Baruk khazad", "Baruk Khazâd")
.replaceAll("Cantea", "Cantëa")
.replaceAll("Cirdan", "Círdan")
.replaceAll("Curunir", "Curunír")
.replaceAll("Deagol", "Déagol")
.replaceAll("Deor", "Déor")
.replaceAll("Dinendal", "Dínendal")
.replaceAll("Dunadan", "Dúnadan")
.replaceAll("Dunedain", "Dúnedain")
.replaceAll("Enquea", "Enquëa")
.replaceAll("Eomer", "Éomer")
.replaceAll("Eomund", "Éomund")
.replaceAll("Eored", "Éored")
.replaceAll("Eothain", "Éothain")
.replaceAll("Eowyn", "Éowyn")
.replaceAll("Erethon", "Erethón")
.replaceAll("Fror", "Frór")
.replaceAll("Ghan-buri-ghan", "Ghan-bûri-ghan")
.replaceAll("Gloin", "Glóin")
.replaceAll("Grima", "Gríma")
.replaceAll("Grishnakh", "Grishnákh")
.replaceAll("Guthlaf", "Guthláf")
.replaceAll("Guthwine", "Gúthwinë")
.replaceAll("Hama", "Háma")
.replaceAll("Jarnsmid", "Járnsmid")
.replaceAll("Khazad ai-menu", "Khazâd ai-mênu")
.replaceAll("Khazad-dum", "Khazad-dûm")
.replaceAll("Lathspell", "Láthspell")
.replaceAll("Leod", "Léod")
.replaceAll("Leofric", "Léofric")
.replaceAll("Leowyn", "Léowyn")
.replaceAll("Lorien", "Lórien")
.replaceAll("Lothlorien", "Lothlórien")
.replaceAll("Luthien", "Lúthien")
.replaceAll("Mauhur", "Mauhúr")
.replaceAll("Miruvore", "Miruvóre")
.replaceAll("Mumak", "Mûmak")
.replaceAll("Mumakil", "Mûmakil")
.replaceAll("Namarie", "Namárië")
.replaceAll("Nazgul", "Nazgûl")
.replaceAll("Nertea", "Nertëa")
.replaceAll("Numenor", "Númenor")
.replaceAll("Numenorean", "Númenorean")
.replaceAll("Otsea", "Otsëa")
.replaceAll("Palantir", "Palantír")
.replaceAll("Rhun", "Rhûn")
.replaceAll("Rumil", "Rúmil")
.replaceAll("Simbelmyne", "Simbelmynë")
.replaceAll("Smeagol", "Sméagol")
.replaceAll("Theoden", "Théoden")
.replaceAll("Theodred", "Théodred")
.replaceAll("Thonnas", "Thónnas")
.replaceAll("Thror", "Thrór")
.replaceAll("Toldea", "Toldëa")
.replaceAll("Udun", "Udûn")
.replaceAll("Ugluk", "Uglúk")
.replaceAll("Ulaire", "Úlairë");
}
}

View File

@@ -17,6 +17,7 @@ import com.gempukku.lotro.logic.timing.EffectResult;
import java.util.*;
public class BuiltLotroCardBlueprint implements LotroCardBlueprint {
private String id;
private String title;
private String sanitizedTitle;
private String subtitle;
@@ -38,6 +39,12 @@ public class BuiltLotroCardBlueprint implements LotroCardBlueprint {
private SitesBlock allyHomeBlock;
private int[] allyHomeSites = new int[0];
private String gameText;
private String formattedGameText;
private String loreText;
private String promoText;
private List<Requirement> requirements;
private List<FilterableSource> targetFilters;
@@ -84,6 +91,10 @@ public class BuiltLotroCardBlueprint implements LotroCardBlueprint {
// Building methods
public void setBlueprintId(String id) {
this.id = id;
}
public void setAllyHomeSites(SitesBlock block, int[] numbers) {
this.allyHomeBlock = block;
this.allyHomeSites = numbers;
@@ -270,6 +281,19 @@ public class BuiltLotroCardBlueprint implements LotroCardBlueprint {
this.subtitle = subtitle;
}
public void setGameText(String text) {
this.formattedGameText = GameText.ConvertTextToHTML(text.trim());
this.gameText = GameText.SanitizeHTMLToSearchText(formattedGameText);
}
public void setLore(String text) {
this.loreText = GameText.ConvertTextToHTML(text.trim());
}
public void setPromoText(String text) {
this.promoText = GameText.ConvertTextToHTML(text.trim());
}
public void setUnique(boolean unique) {
this.unique = unique;
}
@@ -332,6 +356,19 @@ public class BuiltLotroCardBlueprint implements LotroCardBlueprint {
// Implemented methods
@Override
public String getId() {
return id;
}
@Override
public void setId(String id) {
if(this.id != null)
throw new UnsupportedOperationException("Id for this blueprint has already been set");
this.id = id;
}
@Override
public Side getSide() {
return side;

View File

@@ -45,9 +45,9 @@ public class LotroCardBlueprintBuilder implements CardGenerationEnvironment {
fieldProcessors.put("target", new TargetFieldProcessor());
fieldProcessors.put("effects", new EffectFieldProcessor());
fieldProcessors.put("gametext", new NullProcessor());
fieldProcessors.put("lore", new NullProcessor());
fieldProcessors.put("promotext", new NullProcessor());
fieldProcessors.put("gametext", new GameTextFieldProcessor());
fieldProcessors.put("lore", new LoreFieldProcessor());
fieldProcessors.put("promotext", new PromoTextFieldProcessor());
//Soon! But not yet
fieldProcessors.put("cardinfo", new NullProcessor());

View File

@@ -0,0 +1,15 @@
package com.gempukku.lotro.cards.build.field;
import com.gempukku.lotro.cards.build.BuiltLotroCardBlueprint;
import com.gempukku.lotro.cards.build.CardGenerationEnvironment;
import com.gempukku.lotro.cards.build.FieldProcessor;
import com.gempukku.lotro.cards.build.InvalidCardDefinitionException;
public class GameTextFieldProcessor implements FieldProcessor {
@Override
public void processField(String key, Object value, BuiltLotroCardBlueprint blueprint, CardGenerationEnvironment environment) throws InvalidCardDefinitionException {
final String[] textLines = FieldUtils.getStringArray(value, key);
String gametext = String.join("<br>", textLines);
blueprint.setGameText(gametext);
}
}

View File

@@ -0,0 +1,15 @@
package com.gempukku.lotro.cards.build.field;
import com.gempukku.lotro.cards.build.BuiltLotroCardBlueprint;
import com.gempukku.lotro.cards.build.CardGenerationEnvironment;
import com.gempukku.lotro.cards.build.FieldProcessor;
import com.gempukku.lotro.cards.build.InvalidCardDefinitionException;
public class LoreFieldProcessor implements FieldProcessor {
@Override
public void processField(String key, Object value, BuiltLotroCardBlueprint blueprint, CardGenerationEnvironment environment) throws InvalidCardDefinitionException {
final String[] textLines = FieldUtils.getStringArray(value, key);
String lore = String.join("<br>", textLines);
blueprint.setLore(lore);
}
}

View File

@@ -0,0 +1,15 @@
package com.gempukku.lotro.cards.build.field;
import com.gempukku.lotro.cards.build.BuiltLotroCardBlueprint;
import com.gempukku.lotro.cards.build.CardGenerationEnvironment;
import com.gempukku.lotro.cards.build.FieldProcessor;
import com.gempukku.lotro.cards.build.InvalidCardDefinitionException;
public class PromoTextFieldProcessor implements FieldProcessor {
@Override
public void processField(String key, Object value, BuiltLotroCardBlueprint blueprint, CardGenerationEnvironment environment) throws InvalidCardDefinitionException {
final String[] textLines = FieldUtils.getStringArray(value, key);
String promoText = String.join("<br>", textLines);
blueprint.setPromoText(promoText);
}
}

View File

@@ -8,15 +8,10 @@ import com.gempukku.lotro.cards.build.InvalidCardDefinitionException;
public class TitleFieldProcessor implements FieldProcessor {
@Override
public void processField(String key, Object value, BuiltLotroCardBlueprint blueprint, CardGenerationEnvironment environment) throws InvalidCardDefinitionException {
boolean unique = false;
String title = FieldUtils.getString(value, key);
//Deprecated, don't do this. Use the "unique" field instead.
if (title.startsWith("*")) {
unique = true;
title = title.substring(1);
}
if (unique)
blueprint.setUnique(true);
if (title.startsWith("*"))
throw new InvalidCardDefinitionException("Do not use '*' to indicate uniqueness. Use the 'unique' boolean field instead.");
blueprint.setTitle(title);
}
}

View File

@@ -16,6 +16,15 @@ public interface LotroCardBlueprint {
LEFT, RIGHT
}
default LotroCardBlueprint getParent() {
return this;
}
String getId();
//This isn't ideal, but there's 2,000 java cards that already exist.
// Adding the argument to their constructors would be a chore.
void setId(String id);
Side getSide();
CardType getCardType();

View File

@@ -200,16 +200,19 @@ public class LotroCardBlueprintLibrary {
final JSONObject cardsFile = (JSONObject) parser.parse(json);
final Set<Map.Entry<String, JSONObject>> cardsInFile = cardsFile.entrySet();
for (Map.Entry<String, JSONObject> cardEntry : cardsInFile) {
String blueprint = cardEntry.getKey();
if (validateNew)
if (_blueprints.containsKey(blueprint))
logger.error(blueprint + " - Replacing existing card definition!");
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 LotroCardBlueprint lotroCardBlueprint = cardBlueprintBuilder.buildFromJson(cardDefinition);
_blueprints.put(blueprint, lotroCardBlueprint);
final var lotroCardBlueprint = cardBlueprintBuilder.buildFromJson(cardDefinition);
lotroCardBlueprint.setId(blueprintId);
_blueprints.put(blueprintId, lotroCardBlueprint);
} catch (InvalidCardDefinitionException exp) {
logger.error("Unable to load card " + blueprint, exp);
logger.error("Unable to load card " + blueprintId +
" from " + file.getAbsolutePath(), exp);
}
}
} catch (FileNotFoundException exp) {
@@ -238,6 +241,7 @@ public class LotroCardBlueprintLibrary {
try {
// Ensure it's loaded
LotroCardBlueprint blueprint = findJavaBlueprint(blueprintId);
blueprint.setId(blueprintId);
_blueprints.put(blueprintId, blueprint);
} catch (CardNotFoundException exp) {
throw new RuntimeException("Unable to start the server, due to invalid (missing) card definition - " + blueprintId);
@@ -442,30 +446,6 @@ public class LotroCardBlueprintLibrary {
return String.valueOf(id);
}
private void determineNeedsLoadingFlag(JSONObject setDefinition, Set<String> flags) {
Boolean needsLoading = (Boolean) setDefinition.get("needsLoading");
if (needsLoading == null)
needsLoading = true;
if (needsLoading)
flags.add("needsLoading");
}
private void determineMerchantableFlag(JSONObject setDefinition, Set<String> flags) {
Boolean merchantable = (Boolean) setDefinition.get("merchantable");
if (merchantable == null)
merchantable = true;
if (merchantable)
flags.add("merchantable");
}
private void determineOriginalSetFlag(JSONObject setDefinition, Set<String> flags) {
Boolean originalSet = (Boolean) setDefinition.get("originalSet");
if (originalSet == null)
originalSet = true;
if (originalSet)
flags.add("originalSet");
}
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 {

View File

@@ -18,6 +18,8 @@ import java.util.Map;
import java.util.Set;
public abstract class AbstractLotroCardBlueprint implements LotroCardBlueprint {
private String _id;
private final int _twilightCost;
private final String _name;
private final String _sanitizedName;
@@ -89,6 +91,19 @@ public abstract class AbstractLotroCardBlueprint implements LotroCardBlueprint {
return _cardType;
}
@Override
public final String getId() {
return _id;
}
@Override
public void setId(String id) {
if(_id != null)
throw new UnsupportedOperationException("Id for this blueprint has already been set");
_id = id;
}
@Override
public final Side getSide() {
return _side;