diff --git a/mod/src/main/java/basemod/patches/com/megacrit/cardcrawl/cards/AbstractCard/FixCNTokensNotWrappedCorrectly.java b/mod/src/main/java/basemod/patches/com/megacrit/cardcrawl/cards/AbstractCard/FixCNTokensNotWrappedCorrectly.java new file mode 100644 index 000000000..b64e8953a --- /dev/null +++ b/mod/src/main/java/basemod/patches/com/megacrit/cardcrawl/cards/AbstractCard/FixCNTokensNotWrappedCorrectly.java @@ -0,0 +1,101 @@ +package basemod.patches.com.megacrit.cardcrawl.cards.AbstractCard; + +import com.evacipated.cardcrawl.modthespire.lib.SpirePatch; +import com.evacipated.cardcrawl.modthespire.lib.SpireRawPatch; +import com.megacrit.cardcrawl.cards.AbstractCard; +import javassist.*; +import javassist.bytecode.*; +import javassist.convert.Transformer; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public class FixCNTokensNotWrappedCorrectly { + private static final Logger logger = LogManager.getLogger(FixCNTokensNotWrappedCorrectly.class); + + private static final String damage_var_id = " D "; + + /* + This makes the source code in initializeDescriptionCN correctly deal with !D!, !B! and !M!. + Now it would be " !D! ", " !B! ", " !M! " after initialization instead of bad D, !B!! or !M!! + */ + @SpirePatch(clz = AbstractCard.class, method = "initializeDescriptionCN") + public static class MakeSureTokenWrappedCorrectlyInCard { + private static boolean located = false; + private static int insertions = 0; + private static int timesLocated = 0; + + @SpireRawPatch + public static void MakeRaw(CtBehavior ctBehavior) throws CannotCompileException, NotFoundException { + CtClass stringClz = ctBehavior.getDeclaringClass().getClassPool().get(String.class.getName()); + + // surrounds the token !D! !B! and !M! + ctBehavior.instrument(new CodeConverter() {{ + transformers = new Transformer(transformers) { + @Override + public int transform(CtClass ctClass, int index, CodeIterator iterator, ConstPool constPool) throws BadBytecode { + if (timesLocated >= 3) { + return index; + } + int codeAtCurrIndex = iterator.byteAt(index); + if (timesLocated < 3 && codeAtCurrIndex == LDC) { + int ldcIndex = iterator.byteAt(index + 1); + String ldcVal = constPool.getStringInfo(ldcIndex); + if (!located) { + located = "!D!".equals(ldcVal) || "!B!".equals(ldcVal) || "!M!".equals(ldcVal); + } else { + // source code always checks !D! first + // so the first time located should land for !D! + // source code makes the word be like " D " which is so stupid for later matching + // here make it " !D! " + if (timesLocated <= 0) { + if (" D ".equals(ldcVal)) { + // javassist seems to refuse to add string constants with whitespace " " into constpool +// int ldcValConstIndex = constPool.addStringInfo(" !D! "); +// iterator.writeByte(ldcValConstIndex, index + 1); + Bytecode bc = new Bytecode(constPool); + bc.addInvokestatic(FixCNTokensNotWrappedCorrectly.class.getName(), "GetIdentifiedVarWord", Descriptor.ofMethod(stringClz, new CtClass[]{stringClz})); + // insert after ldc and its index value + iterator.insertAt(index + 2, bc.get()); + insertions++; + } + } else { + // source code makes the word be like " !B!! " + // we need to make it " !B! " which is more correct + if ("! ".equals(ldcVal)) { +// int ldcValConstIndex = constPool.addStringInfo(" "); +// iterator.writeByte(ldcValConstIndex, index + 1); + Bytecode bc = new Bytecode(constPool); + bc.addInvokestatic(FixCNTokensNotWrappedCorrectly.class.getName(), "GetIdentifiedVarWord", Descriptor.ofMethod(stringClz, new CtClass[]{stringClz})); + // insert after ldc and its index value + iterator.insertAt(index + 2, bc.get()); + insertions++; + } + } + + // only need to do two replacement each time located + if (insertions > 0 && insertions % 2 == 0) { + located = false; + timesLocated++; + insertions = 0; + } + } + } + return index; + } + }; + }}); + } + } + + public static String GetIdentifiedVarWord(String identifier) { + // should be only two kinds of identifier: " D " and "! " + switch (identifier) { + case damage_var_id: + return " !D! "; + case "! ": + return " "; + default: + return identifier; + } + } +} diff --git a/mod/src/main/java/basemod/patches/com/megacrit/cardcrawl/cards/AbstractCard/FixDescriptionWidthCustomDynamicVariableCN.java b/mod/src/main/java/basemod/patches/com/megacrit/cardcrawl/cards/AbstractCard/FixDescriptionWidthCustomDynamicVariableCN.java index 864633e7a..2ae13a248 100644 --- a/mod/src/main/java/basemod/patches/com/megacrit/cardcrawl/cards/AbstractCard/FixDescriptionWidthCustomDynamicVariableCN.java +++ b/mod/src/main/java/basemod/patches/com/megacrit/cardcrawl/cards/AbstractCard/FixDescriptionWidthCustomDynamicVariableCN.java @@ -24,6 +24,7 @@ public static void Insert(AbstractCard __instance, @ByRef String[] word, @ByRef StringBuilder currentLine, @ByRef int[] numLines, float CN_DESC_BOX_WIDTH) { + // edit: changed the extra "!" appended after the variable to " " float MAGIC_NUMBER_LENGTH = 20.0F * Settings.scale ; if (word[0].startsWith("!")) { // GlyphLayout gl = new GlyphLayout(FontHelper.cardDescFont_N, "!M!"); @@ -32,9 +33,9 @@ public static void Insert(AbstractCard __instance, @ByRef String[] word, @ByRef __instance.description.add(new DescriptionLine(currentLine.toString(), currentWidth[0])); currentLine.setLength(0); currentWidth[0] = MAGIC_NUMBER_LENGTH; - currentLine.append(" ").append(word[0]).append("! "); + currentLine.append(" ").append(word[0]).append(" "); } else { - currentLine.append(" ").append(word[0]).append("! "); + currentLine.append(" ").append(word[0]).append(" "); currentWidth[0] += MAGIC_NUMBER_LENGTH; } word[0] = ""; diff --git a/mod/src/main/java/basemod/patches/com/megacrit/cardcrawl/cards/AbstractCard/FixUnwrappedCNTokensWronglyRender.java b/mod/src/main/java/basemod/patches/com/megacrit/cardcrawl/cards/AbstractCard/FixUnwrappedCNTokensWronglyRender.java new file mode 100644 index 000000000..ad83a9235 --- /dev/null +++ b/mod/src/main/java/basemod/patches/com/megacrit/cardcrawl/cards/AbstractCard/FixUnwrappedCNTokensWronglyRender.java @@ -0,0 +1,208 @@ +package basemod.patches.com.megacrit.cardcrawl.cards.AbstractCard; + +/* +This is where we fix a huge mess of the source code to make it not render unformatted tokens in CN-like desc. +"formatted token" refers to token wrapped with "!", such as " !B! " and " !D! ". +These formatted tokens should be well handled in CustomDynamicVariableTokenizeCN +RenderCustomDynamicVariableCN fixes rendering custom variables including !D!, !B! and !M!, +but source code still searches through the tokens and checks unformatted tokens like "B" and "D" + */ + +import com.evacipated.cardcrawl.modthespire.lib.SpirePatch; +import com.evacipated.cardcrawl.modthespire.lib.SpireRawPatch; +import com.megacrit.cardcrawl.cards.AbstractCard; +import javassist.*; +import javassist.bytecode.*; +import javassist.convert.Transformer; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public class FixUnwrappedCNTokensWronglyRender { + private static final Logger logger = LogManager.getLogger(FixUnwrappedCNTokensWronglyRender.class); + + @SpirePatch(clz = AbstractCard.class, method = "renderDescriptionCN") + public static class FormatOnlySurroundedToken { + + private static boolean badCodeLocated = false; + private static int timesLocated = 0; + private static int firstLocated = -1; + private static int localVarTmpIndex = -1; + private static int localVarUpdateTmpIndex = -1; + private static int localVarJIndex = -1; + + private static final int ascii_D = 68; + private static final int ascii_M = 77; + + @SpireRawPatch + public static void MakeRaw(CtBehavior ctBehavior) throws CannotCompileException, NotFoundException { + // make sure only the surrounded tokens can be formatted + CodeAttribute ca = ctBehavior.getMethodInfo().getCodeAttribute(); + LocalVariableAttribute localVarTable = (LocalVariableAttribute) ca.getAttribute(LocalVariableAttribute.tag); + // the old bad code is + // if (tmp.chatAt(j) == 'D' || (tmp.charAt(j) == 'B' && !tmp.contains("[B]")) || tmp.charAt(j) == 'M') { // do bad things } + // skip the whole bad logic to prevent it doing wrong thing + // there are 2 places where the bad code lies in the source code + // thankfully their logic are mostly the same, easy to locate + + if (localVarTmpIndex == -1 || localVarUpdateTmpIndex == -1 || localVarJIndex == -1) { + for (int i = 0; i < localVarTable.tableLength(); i++) { + String varName = localVarTable.variableName(i); + if ("tmp".equals(varName)) { + localVarTmpIndex = localVarTable.index(i); + } + if ("updateTmp".equals(varName)) { + localVarUpdateTmpIndex = localVarTable.index(i); + } + if ("j".equals(varName)) { + localVarJIndex = localVarTable.index(i); + } + } + } + + ctBehavior.instrument(new CodeConverter() {{ + transformers = new CodeReplacement(transformers); + }}); + } + + private static class CodeReplacement extends Transformer { + + public CodeReplacement(Transformer t) { + super(t); + } + + @Override + public int transform(CtClass ctClass, final int index, CodeIterator iterator, ConstPool constPool) throws BadBytecode { + if (timesLocated >= 2) { + return index; + } + CtClass stringClz; + try { + stringClz = ctClass.getClassPool().get(String.class.getName()); + } catch (NotFoundException e) { + throw new RuntimeException(e); + } + int codeAtCurrIndex = iterator.byteAt(index); + int skipStartingIndex = -1; + if (codeAtCurrIndex == BIPUSH && !badCodeLocated) { + int byteVal = iterator.byteAt(index + 1); + // the bad codes checks 'D' first so it is a head marker + if (byteVal == ascii_D) { + if (index < firstLocated) { + return index; + } + iterator.setMark(index); + // goes up, find the very head + int reverseIndex = index; + boolean locatedHead = false; + while (reverseIndex > 0) { + int reverseCode = iterator.byteAt(--reverseIndex); + // the bad logic loads "tmp" first using aload + if (reverseCode != ALOAD) continue; + int aloadValIndex = iterator.byteAt(reverseIndex + 1); + if (aloadValIndex == localVarTmpIndex) { + locatedHead = true; + // set the starting position of new logic + skipStartingIndex = reverseIndex; + break; + } + } + iterator.move(iterator.getMark()); + if (locatedHead) { + iterator.setMark(iterator.lookAhead()); + // goes down to check 'M' and if_icmpne + boolean locatedM = false; + boolean locatedIf = false; + while (iterator.hasNext() && !locatedIf) { + int nextPos = iterator.next(); + int nextCode = iterator.byteAt(nextPos); + if (locatedM) { + // after finding possible 'M', checks if its next is if + if (nextCode != IF_ICMPNE) continue; + locatedIf = true; + } + if (nextCode == BIPUSH) { + int nextByteVal = iterator.byteAt(nextPos + 1); + if (nextByteVal == ascii_M) + locatedM = true; + } + } + badCodeLocated = locatedIf; + iterator.move(iterator.getMark()); + } + } + } + if (badCodeLocated) { + Bytecode bc = new Bytecode(constPool); + bc.addInvokestatic(FormatOnlySurroundedToken.class.getName(), "FixingDBM", Descriptor.ofMethod(CtClass.booleanType, new CtClass[0])); + bc.add(Opcode.IFNE); + // leave for ifne, the index is to be located later + bc.addIndex(Opcode.NOP); + iterator.insertAt(skipStartingIndex, bc.get()); + // now goes down again to find a goto (the break in the source code) + // need to add new logic after the goto + boolean locatedM = false; + boolean locatedIf = false; + boolean locatedBreak = false; + int gotoPos = -1; + while (iterator.hasNext() && !locatedBreak) { + int nextPos = iterator.next(); + int nextCode = iterator.byteAt(nextPos); + if (locatedIf) { + if (nextCode != GOTO) continue; + locatedBreak = true; + gotoPos = nextPos; +// logger.info("found goto at {}", gotoPos); + } + if (locatedM) { + if (nextCode != IF_ICMPNE) continue; + locatedIf = true; + } + if (nextCode == BIPUSH) { + int nextByteVal = iterator.byteAt(nextPos + 1); + if (nextByteVal == ascii_M) + locatedM = true; + } + } + // two operators for goto + int destination = gotoPos + 3; + // add new logic + bc = new Bytecode(constPool); + // load "this" ref + bc.addAload(0); + bc.addAload(localVarTmpIndex); + bc.addIload(localVarJIndex); + bc.addInvokestatic(FormatOnlySurroundedToken.class.getName(), "GetCorrectWord", Descriptor.ofMethod(stringClz, new CtClass[] {ctClass, stringClz, CtClass.intType})); + bc.addAstore(localVarUpdateTmpIndex); + int skipEndLocation = iterator.insertAt(destination, bc.get()); + iterator.move(gotoPos); + iterator.writeByte(Opcode.NOP, gotoPos + 1); + iterator.writeByte(Opcode.NOP, gotoPos + 2); + iterator.write16bit(skipEndLocation - gotoPos, gotoPos + 1); + iterator.move(skipStartingIndex); + while (iterator.hasNext() && iterator.byteAt(iterator.lookAhead()) != Opcode.IFNE) + iterator.next(); + // correct ifne offset to new logic position + iterator.write16bit(destination - iterator.lookAhead(), iterator.lookAhead() + 1); + timesLocated++; + badCodeLocated = false; + firstLocated = gotoPos; + } + return index; + } + } + + + public static String GetCorrectWord(AbstractCard card, String tmp, int j) { + String text = tmp; + // formatted tokens, whether well-formatted or not, should be already converted into string values of the corresponding variables + // so there shouldn't exist tokens like that but normal unformatted letters D, B and M. + // Still, it's best to check if the word contains formatted tokens + tmp = RenderCustomDynamicVariableCN.MatchVariablesAndReplace(card, text); + return tmp; + } + + public static boolean FixingDBM() { + return true; + } + } +} \ No newline at end of file diff --git a/mod/src/main/java/basemod/patches/com/megacrit/cardcrawl/cards/AbstractCard/RenderCustomDynamicVariableCN.java b/mod/src/main/java/basemod/patches/com/megacrit/cardcrawl/cards/AbstractCard/RenderCustomDynamicVariableCN.java index 841fba909..0903f5e74 100644 --- a/mod/src/main/java/basemod/patches/com/megacrit/cardcrawl/cards/AbstractCard/RenderCustomDynamicVariableCN.java +++ b/mod/src/main/java/basemod/patches/com/megacrit/cardcrawl/cards/AbstractCard/RenderCustomDynamicVariableCN.java @@ -8,9 +8,12 @@ import com.evacipated.cardcrawl.modthespire.lib.*; import com.evacipated.cardcrawl.modthespire.patcher.PatchingException; import com.megacrit.cardcrawl.cards.AbstractCard; +import com.megacrit.cardcrawl.cards.red.Strike_Red; import javassist.CannotCompileException; import javassist.CtBehavior; +import org.apache.logging.log4j.util.Strings; +import java.util.function.Function; import java.util.regex.Pattern; @SpirePatch( @@ -19,41 +22,26 @@ ) public class RenderCustomDynamicVariableCN { + public static final String VARIABLE_REGEX = "!(.+?)!"; + @SpireInsertPatch( locator=Locator.class, localvars={"tmp"} ) public static void Insert(AbstractCard __instance, SpriteBatch sb, @ByRef String[] tmp) { - if (tmp[0].startsWith("$") || tmp[0].equals("D")) { - String key = tmp[0]; - Pattern pattern = Pattern.compile("\\$(.+)\\$\\$"); - java.util.regex.Matcher matcher = pattern.matcher(key); - if (matcher.find()) { - key = matcher.group(1); - } - - DynamicVariable dv = BaseMod.cardDynamicVariableMap.get(key); - if (dv != null) { - if (dv.isModified(__instance)) { - if (dv.value(__instance) >= dv.modifiedBaseValue(__instance)) { - tmp[0] = "[#" + dv.getIncreasedValueColor() + "]" + dv.value(__instance) + "[]"; - } else { - tmp[0] = "[#" + dv.getDecreasedValueColor() + "]" + dv.value(__instance) + "[]"; - } - } else { - Color textColor = ReflectionHacks.getPrivate(__instance, AbstractCard.class, "textColor"); - Color dvColor = dv.getNormalColor(); - float oldAlpha = dvColor.a; - if (textColor != null) { - dvColor.a = textColor.a; - } - tmp[0] = "[#" + dvColor + "]" + dv.modifiedBaseValue(__instance) + "[]"; - dvColor.a = oldAlpha; - } - } - } + // Well-formatted variables in CN desc should be like "造成 !modid:var! 伤害。" + // in which two whitespaces separate the variable from the other texts. + // And these well-formatted ones should be well tokenized into individual tokens in CustomDynamicVariableTokenizeCN. + // And "mal-formatted" variables are like "造成!modid:var!伤害。". + // It can be noted that these mal-formatted variables are not separated from the other texts. + // One problem with these mal-formatted variables is they can really come in piles in a sentence (or a token), + // such as "造成!modid:var1!伤害,然后获得!modid:var2!点格挡并抽!M!张牌。" + // which means regex match may come up with a lot of groups, and we need to replace these groups one by one + + String text = tmp[0]; + tmp[0] = MatchVariablesAndReplace(__instance, text); } private static class Locator extends SpireInsertLocator @@ -64,4 +52,62 @@ public int[] Locate(CtBehavior ctMethodToPatch) throws CannotCompileException, P return LineFinder.findInOrder(ctMethodToPatch, finalMatcher); } } + + public static String MatchVariablesAndReplace(AbstractCard card, String text) { + // I made a poll in a major Chinese modding community which showed that + // around 64% of Chinese modders preferred a strict match instead of a loose one. + // That means for each token passed here, we need to check if it starts with "!", + // filtering out all the tokens that start with other texts + + // This simple bool is kept here for some time when it might need to switch to other case + boolean strictSideWon = true; + if (strictSideWon && !text.startsWith("!")) + return text; + Pattern pattern = Pattern.compile(VARIABLE_REGEX); + return stepReplace(pattern, text, m -> { + String varKey = m.group(1); + DynamicVariable dv = BaseMod.cardDynamicVariableMap.get(varKey); + if (dv != null) { + return GetValueOfVariable(card, dv); + } else { + return m.group(0); + } + }); + } + + public static String GetValueOfVariable(AbstractCard card, DynamicVariable dv) { + String result = null; + if (dv != null) { + if (dv.isModified(card)) { + if (dv.value(card) >= dv.modifiedBaseValue(card)) { + result = "[#" + dv.getIncreasedValueColor() + "]" + dv.value(card) + "[]"; + } else { + result = "[#" + dv.getDecreasedValueColor() + "]" + dv.value(card) + "[]"; + } + } else { + Color textColor = ReflectionHacks.getPrivate(card, AbstractCard.class, "textColor"); + Color dvColor = dv.getNormalColor(); + float oldAlpha = dvColor.a; + if (textColor != null) { + dvColor.a = textColor.a; + } + result = "[#" + dvColor + "]" + dv.modifiedBaseValue(card) + "[]"; + dvColor.a = oldAlpha; + } + } + return result; + } + + private static String stepReplace(Pattern pattern, String text, Function mapper) { + java.util.regex.Matcher matcher = pattern.matcher(text); + StringBuffer sb = new StringBuffer(); + while (matcher.find()) { + String replacement = mapper.apply(matcher); + if (replacement != null) { + matcher.appendReplacement(sb, replacement); + } + } + matcher.appendTail(sb); + return sb.toString(); + } } diff --git a/mod/src/main/java/basemod/patches/com/megacrit/cardcrawl/cards/DescriptionLine/CustomDynamicVariableTokenizeCN.java b/mod/src/main/java/basemod/patches/com/megacrit/cardcrawl/cards/DescriptionLine/CustomDynamicVariableTokenizeCN.java index 326a3c941..8a44013ff 100644 --- a/mod/src/main/java/basemod/patches/com/megacrit/cardcrawl/cards/DescriptionLine/CustomDynamicVariableTokenizeCN.java +++ b/mod/src/main/java/basemod/patches/com/megacrit/cardcrawl/cards/DescriptionLine/CustomDynamicVariableTokenizeCN.java @@ -1,14 +1,17 @@ package basemod.patches.com.megacrit.cardcrawl.cards.DescriptionLine; -import basemod.BaseMod; -import basemod.abstracts.DynamicVariable; import com.evacipated.cardcrawl.modthespire.lib.*; -import com.evacipated.cardcrawl.modthespire.patcher.PatchingException; import com.megacrit.cardcrawl.cards.DescriptionLine; -import javassist.CannotCompileException; -import javassist.CtBehavior; -import java.util.regex.Pattern; +/* +Instead of replacing "!" with "$" in CN language to prevent the bad logic of the source code, +it is better to make it more close to good logic. So I commented the insert patch to stop it from doing the replacement. +The key reason why CN modders can't use normal letters DBM in their card desc is that in initializeDescriptionCN, +the source code in one hand removes the format symbol "!" that wraps the damage variable !D! so it becomes a normal D, +which makes it easy to be confused with truly normal D. +And the other is that the source code appends extra format symbol to !B! and !M! +making them be !B!! and !M!!. This should be fixed in FixCNTokensNotWrappedCorrectly. +*/ @SpirePatch( clz=DescriptionLine.class, @@ -16,34 +19,43 @@ ) public class CustomDynamicVariableTokenizeCN { - @SpireInsertPatch( - locator=Locator.class, - localvars={"tokenized", "i"} - ) - public static void Insert(String desc, String[] tokenized, int i) - { - if (tokenized[i].startsWith("!")) { - String key = tokenized[i]; - - Pattern pattern = Pattern.compile("!(.+)!!"); - java.util.regex.Matcher matcher = pattern.matcher(key); - if (matcher.find()) { - key = matcher.group(1); - } - - DynamicVariable dv = BaseMod.cardDynamicVariableMap.get(key); - if (dv != null) { - tokenized[i] = tokenized[i].replace("!", "$"); - } - } + // postfix the vanilla method to change its return value + @SpirePostfixPatch + public static String[] RetokenizeCN(String[] tokens, String desc) { + tokens = desc.split("\\s+"); + // no need to do any replacement so that variables like !D!, !B! and !M! and other custom variables stay the same + return tokens; } + + +// @SpireInsertPatch( +// locator=Locator.class, +// localvars={"tokenized", "i"} +// ) +// public static void Insert(String desc, String[] tokenized, int i) +// { +// if (tokenized[i].startsWith("!")) { +// String key = tokenized[i]; +// +// Pattern pattern = Pattern.compile("!(.+)!!"); +// java.util.regex.Matcher matcher = pattern.matcher(key); +// if (matcher.find()) { +// key = matcher.group(1); +// } +// +// DynamicVariable dv = BaseMod.cardDynamicVariableMap.get(key); +// if (dv != null) { +// tokenized[i] = tokenized[i].replace("!", "$"); +// } +// } +// } - private static class Locator extends SpireInsertLocator - { - public int[] Locate(CtBehavior ctMethodToPatch) throws CannotCompileException, PatchingException - { - Matcher finalMatcher = new Matcher.MethodCallMatcher(String.class, "replace"); - return LineFinder.findInOrder(ctMethodToPatch, finalMatcher); - } - } +// private static class Locator extends SpireInsertLocator +// { +// public int[] Locate(CtBehavior ctMethodToPatch) throws CannotCompileException, PatchingException +// { +// Matcher finalMatcher = new Matcher.MethodCallMatcher(String.class, "replace"); +// return LineFinder.findInOrder(ctMethodToPatch, finalMatcher); +// } +// } } diff --git a/mod/src/main/java/basemod/patches/com/megacrit/cardcrawl/screens/SingleCardViewPopup/FixUnwrappedCNTokensWronglyRenderSCV.java b/mod/src/main/java/basemod/patches/com/megacrit/cardcrawl/screens/SingleCardViewPopup/FixUnwrappedCNTokensWronglyRenderSCV.java new file mode 100644 index 000000000..dce356506 --- /dev/null +++ b/mod/src/main/java/basemod/patches/com/megacrit/cardcrawl/screens/SingleCardViewPopup/FixUnwrappedCNTokensWronglyRenderSCV.java @@ -0,0 +1,215 @@ +package basemod.patches.com.megacrit.cardcrawl.screens.SingleCardViewPopup; + +import basemod.ReflectionHacks; +import basemod.patches.com.megacrit.cardcrawl.cards.AbstractCard.RenderCustomDynamicVariableCN; +import com.evacipated.cardcrawl.modthespire.lib.SpirePatch; +import com.evacipated.cardcrawl.modthespire.lib.SpireRawPatch; +import com.megacrit.cardcrawl.cards.AbstractCard; +import com.megacrit.cardcrawl.screens.SingleCardViewPopup; +import javassist.*; +import javassist.bytecode.*; +import javassist.convert.Transformer; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public class FixUnwrappedCNTokensWronglyRenderSCV { + private static final Logger logger = LogManager.getLogger(FixUnwrappedCNTokensWronglyRenderSCV.class); + + @SpirePatch(clz = SingleCardViewPopup.class, method = "renderDescriptionCN") + public static class FormatOnlySurroundedToken { + + private static boolean badCodeLocated = false; + private static int timesLocated = 0; + private static int firstLocated = -1; + private static int localVarTmpIndex = -1; + private static int localVarUpdateTmpIndex = -1; + private static int localVarJIndex = -1; + + private static int ascii_D = 68; + private static int ascii_M = 77; + + @SpireRawPatch + public static void MakeRaw(CtBehavior ctBehavior) throws CannotCompileException, NotFoundException { + // make sure only the surrounded tokens can be formatted + CodeAttribute ca = ctBehavior.getMethodInfo().getCodeAttribute(); + LocalVariableAttribute localVarTable = (LocalVariableAttribute) ca.getAttribute(LocalVariableAttribute.tag); + // the old bad code is + // if (tmp.chatAt(j) == 'D' || (tmp.charAt(j) == 'B' && !tmp.contains("[B]")) || tmp.charAt(j) == 'M') { // do bad things } + // skip the whole bad logic to prevent it doing wrong thing + // there are 2 places where the bad code lies in the source code + // thankfully their logic are mostly the same, easy to locate + + if (localVarTmpIndex == -1 || localVarUpdateTmpIndex == -1 || localVarJIndex == -1) { + for (int i = 0; i < localVarTable.tableLength(); i++) { + String varName = localVarTable.variableName(i); + if ("tmp".equals(varName)) { + localVarTmpIndex = localVarTable.index(i); + } + if ("updateTmp".equals(varName)) { + localVarUpdateTmpIndex = localVarTable.index(i); + } + if ("j".equals(varName)) { + localVarJIndex = localVarTable.index(i); + } + } + } + + ctBehavior.instrument(new CodeConverter() {{ + transformers = new CodeReplacement(transformers); + }}); + } + + private static class CodeReplacement extends Transformer { + + public CodeReplacement(Transformer t) { + super(t); + } + + @Override + public int transform(CtClass ctClass, final int index, CodeIterator iterator, ConstPool constPool) throws BadBytecode { + if (timesLocated >= 2) { + return index; + } + CtClass stringClz; + try { + stringClz = ctClass.getClassPool().get(String.class.getName()); + } catch (NotFoundException e) { + throw new RuntimeException(e); + } + int codeAtCurrIndex = iterator.byteAt(index); + int skipStartingIndex = -1; + if (codeAtCurrIndex == BIPUSH && !badCodeLocated) { + int byteVal = iterator.byteAt(index + 1); + // the bad codes checks 'D' first so it is a head marker + if (byteVal == ascii_D) { + if (index < firstLocated) { + return index; + } + iterator.setMark(index); + // goes up, find the very head + int reverseIndex = index; + boolean locatedHead = false; + while (reverseIndex > 0) { + int reverseCode = iterator.byteAt(--reverseIndex); + // the bad logic loads "tmp" first using aload + if (reverseCode != ALOAD) continue; + int aloadValIndex = iterator.byteAt(reverseIndex + 1); + if (aloadValIndex == localVarTmpIndex) { + locatedHead = true; + // set the starting position of new logic + skipStartingIndex = reverseIndex; + break; + } + } + iterator.move(iterator.getMark()); + if (locatedHead) { + iterator.setMark(iterator.lookAhead()); + // goes down to check 'M' and if_icmpne + boolean locatedM = false; + boolean locatedIf = false; + while (iterator.hasNext() && !locatedIf) { + int nextPos = iterator.next(); + int nextCode = iterator.byteAt(nextPos); + if (locatedM) { + // after finding possible 'M', checks if its next is if + if (nextCode != IF_ICMPNE) continue; + locatedIf = true; + } + if (nextCode == BIPUSH) { + int nextByteVal = iterator.byteAt(nextPos + 1); + if (nextByteVal == ascii_M) + locatedM = true; + } + } + badCodeLocated = locatedIf; + iterator.move(iterator.getMark()); + } + } + } + if (badCodeLocated) { + Bytecode bc = new Bytecode(constPool); + bc.addInvokestatic(FormatOnlySurroundedToken.class.getName(), "FixingDBM", Descriptor.ofMethod(CtClass.booleanType, new CtClass[0])); + bc.add(Opcode.IFNE); + // leave for ifne, the index is to be located later + bc.addIndex(Opcode.NOP); + iterator.insertAt(skipStartingIndex, bc.get()); + // now goes down again to find a goto (the break in the source code) + // need to add new logic after the goto + boolean locatedM = false; + boolean locatedIf = false; + boolean locatedBreak = false; + int gotoPos = -1; + while (iterator.hasNext() && !locatedBreak) { + int nextPos = iterator.next(); + int nextCode = iterator.byteAt(nextPos); + if (locatedIf) { + if (nextCode != GOTO) continue; + locatedBreak = true; + gotoPos = nextPos; + } + if (locatedM) { + if (nextCode != IF_ICMPNE) continue; + locatedIf = true; + } + if (nextCode == BIPUSH) { + int nextByteVal = iterator.byteAt(nextPos + 1); + if (nextByteVal == ascii_M) + locatedM = true; + } + } + // two operators for goto + int destination = gotoPos + 3; + // add new logic + bc = new Bytecode(constPool); + // load "this" ref + bc.addAload(0); + bc.addAload(localVarTmpIndex); + bc.addIload(localVarJIndex); + bc.addInvokestatic(FormatOnlySurroundedToken.class.getName(), "GetCorrectWord", Descriptor.ofMethod(stringClz, new CtClass[] {ctClass, stringClz, CtClass.intType})); + bc.addAstore(localVarUpdateTmpIndex); + int skipEndLocation = iterator.insertAt(destination, bc.get()); + iterator.move(gotoPos); + iterator.writeByte(Opcode.NOP, gotoPos + 1); + iterator.writeByte(Opcode.NOP, gotoPos + 2); + iterator.write16bit(skipEndLocation - gotoPos, gotoPos + 1); + iterator.move(skipStartingIndex); + while (iterator.hasNext() && iterator.byteAt(iterator.lookAhead()) != Opcode.IFNE) + iterator.next(); + // correct ifne offset to new logic position + iterator.write16bit(destination - iterator.lookAhead(), iterator.lookAhead() + 1); + timesLocated++; + badCodeLocated = false; + firstLocated = gotoPos; + } + return index; + } + } + + // Thanks to Casey Yano, the god of the spire, for he keeps the bad codes almost the same as the ones in AbstractCard + // So simply changing param AbstractCard to SCV is okay + public static String GetCorrectWord(SingleCardViewPopup scv, String tmp, int j) { + String text = tmp; + try { + AbstractCard card = ReflectionHacks.getPrivate(scv, SingleCardViewPopup.class, "card"); + // formatted tokens, whether well-formatted or not, should be converted into string values of the corresponding variables + // so there shouldn't exist tokens like that but normal unformatted letters D, B and M. + // Still, it's best to check if the word contains formatted tokens + tmp = RenderCustomDynamicVariableCN.MatchVariablesAndReplace(card, text); + } catch (Exception e) { + logger.info("Failed to get correct word for {}: {}", tmp, e.getCause()); + } + return tmp; + } + + public static boolean FixingDBM() { + return true; + } + + private static int paramIndex = 1; + + private static String getParamName(LocalVariableAttribute table, String key) { + int index = (table != null ? table.tableLength() : 0) + paramIndex++; + return "_param_" + index + "_" + key; + } + } +} \ No newline at end of file diff --git a/mod/src/main/java/basemod/patches/com/megacrit/cardcrawl/screens/SingleCardViewPopup/RenderCustomDynamicVariableCN.java b/mod/src/main/java/basemod/patches/com/megacrit/cardcrawl/screens/SingleCardViewPopup/RenderCustomDynamicVariableCN.java index b31f3db99..abf6c73fa 100644 --- a/mod/src/main/java/basemod/patches/com/megacrit/cardcrawl/screens/SingleCardViewPopup/RenderCustomDynamicVariableCN.java +++ b/mod/src/main/java/basemod/patches/com/megacrit/cardcrawl/screens/SingleCardViewPopup/RenderCustomDynamicVariableCN.java @@ -1,7 +1,5 @@ package basemod.patches.com.megacrit.cardcrawl.screens.SingleCardViewPopup; -import basemod.BaseMod; -import basemod.abstracts.DynamicVariable; import com.badlogic.gdx.graphics.g2d.SpriteBatch; import com.evacipated.cardcrawl.modthespire.lib.*; import com.evacipated.cardcrawl.modthespire.patcher.PatchingException; @@ -10,7 +8,7 @@ import javassist.CannotCompileException; import javassist.CtBehavior; -import java.util.regex.Pattern; +import static basemod.patches.com.megacrit.cardcrawl.cards.AbstractCard.RenderCustomDynamicVariableCN.MatchVariablesAndReplace; @SpirePatch( clz=SingleCardViewPopup.class, @@ -24,29 +22,10 @@ public class RenderCustomDynamicVariableCN ) public static void Insert(SingleCardViewPopup __instance, SpriteBatch sb, AbstractCard card, @ByRef String[] tmp) { - if (tmp[0].startsWith("$") || tmp[0].equals("D")) { - String key = tmp[0]; - - Pattern pattern = Pattern.compile("\\$(.+)\\$\\$"); - java.util.regex.Matcher matcher = pattern.matcher(key); - if (matcher.find()) { - key = matcher.group(1); - } - - DynamicVariable dv = BaseMod.cardDynamicVariableMap.get(key); - if (dv != null) { - if (dv.isModified(card)) { - if (dv.value(card) >= dv.modifiedBaseValue(card)) { - tmp[0] = "[#" + dv.getIncreasedValueColor().toString() + "]" + Integer.toString(dv.value(card)) + "[]"; - } else { - tmp[0] = "[#" + dv.getDecreasedValueColor().toString() + "]" + Integer.toString(dv.value(card)) + "[]"; - } - } else { - //cardmods affect base variables - tmp[0] = "[#" + dv.getNormalColor().toString() + "]" + Integer.toString(dv.modifiedBaseValue(card)) + "[]"; - } - } - } + // same logic as render in single card + // for more details, see basemod.patches.com.megacrit.cardcrawl.cards.AbstractCard.RenderCustomDynamicVariableCN + String text = tmp[0]; + tmp[0] = MatchVariablesAndReplace(card, text); } private static class Locator extends SpireInsertLocator