Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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!");
Expand All @@ -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] = "";
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
Loading