├── src └── main │ ├── resources │ ├── Icon.png │ └── ModTheSpire.json │ └── java │ └── communicationmod │ ├── OnStateChangeSubscriber.java │ ├── EndOfTurnAction.java │ ├── patches │ ├── EnableEndTurnPatch.java │ ├── CardCrawlGamePatch.java │ ├── SetDialogOptionPatch.java │ ├── GameActionManagerTopPatch.java │ ├── GameActionManagerBottomPatch.java │ ├── AbstractRoomEndTurnPatch.java │ ├── HandCardSelectScreenPatch.java │ ├── GridCardSelectScreenPatch.java │ ├── RoomEventDialogPatch.java │ ├── CampfireLiftEffectPatch.java │ ├── GenericEventDialogPatch.java │ ├── UpdateBodyTextPatch.java │ ├── InputActionPatch.java │ ├── CombatRewardScreenPatch.java │ ├── DungeonMapPatch.java │ ├── MapRoomNodeHoverPatch.java │ ├── MerchantPatch.java │ ├── GridCardSelectScreenUpdatePatch.java │ ├── CampfireDigEffectPatch.java │ ├── CampfireSleepEffectPatch.java │ ├── CampfireTokeEffectPatch.java │ ├── CampfireSmithEffectPatch.java │ ├── CampfireRecallEffectPatch.java │ ├── ShopScreenPatch.java │ ├── CardRewardScreenPatch.java │ ├── AbstractRelicUpdatePatch.java │ └── GremlinMatchGamePatch.java │ ├── DataWriter.java │ ├── InvalidCommandException.java │ ├── DataReader.java │ ├── GameStateListener.java │ ├── CommunicationMod.java │ ├── CommandExecutor.java │ ├── ChoiceScreenUtils.java │ └── GameStateConverter.java ├── .gitignore ├── LICENSE ├── CHANGELOG.md ├── pom.xml └── README.md /src/main/resources/Icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ForgottenArbiter/CommunicationMod/HEAD/src/main/resources/Icon.png -------------------------------------------------------------------------------- /src/main/java/communicationmod/OnStateChangeSubscriber.java: -------------------------------------------------------------------------------- 1 | package communicationmod; 2 | 3 | import basemod.interfaces.ISubscriber; 4 | 5 | public interface OnStateChangeSubscriber extends ISubscriber { 6 | void receiveOnStateChange(); 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/communicationmod/EndOfTurnAction.java: -------------------------------------------------------------------------------- 1 | package communicationmod; 2 | 3 | import com.megacrit.cardcrawl.actions.AbstractGameAction; 4 | 5 | public class EndOfTurnAction extends AbstractGameAction { 6 | public void update() { 7 | GameStateListener.signalTurnEnd(); 8 | this.isDone = true; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/main/resources/ModTheSpire.json: -------------------------------------------------------------------------------- 1 | { 2 | "modid": "${project.artifactId}", 3 | "name": "${project.name}", 4 | "author_list": ["Forgotten Arbiter"], 5 | "description": "${project.description}", 6 | "version": "${project.version}", 7 | "sts_version": "${SlayTheSpire.version}", 8 | "mts_version": "${ModTheSpire.version}", 9 | "dependencies": ["basemod"] 10 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled class file 2 | *.class 3 | 4 | # Log file 5 | *.log 6 | 7 | # Mobile Tools for Java (J2ME) 8 | .mtj.tmp/ 9 | 10 | # Package Files # 11 | *.jar 12 | *.war 13 | *.ear 14 | *.zip 15 | *.tar.gz 16 | *.rar 17 | 18 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 19 | hs_err_pid* 20 | 21 | # Eclipse 22 | .classpath 23 | .project 24 | .settings/ 25 | 26 | # IntelliJ 27 | .idea/ 28 | out/ 29 | CommunicationMod.iml 30 | 31 | # maven 32 | target/ 33 | dependency-reduced-pom.xml -------------------------------------------------------------------------------- /src/main/java/communicationmod/patches/EnableEndTurnPatch.java: -------------------------------------------------------------------------------- 1 | package communicationmod.patches; 2 | 3 | import com.evacipated.cardcrawl.modthespire.lib.SpirePatch; 4 | import com.megacrit.cardcrawl.actions.common.EnableEndTurnButtonAction; 5 | import communicationmod.GameStateListener; 6 | 7 | @SpirePatch( 8 | clz = EnableEndTurnButtonAction.class, 9 | method = "update" 10 | ) 11 | public class EnableEndTurnPatch { 12 | public static void Postfix(EnableEndTurnButtonAction _instance) { 13 | GameStateListener.signalTurnStart(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/communicationmod/patches/CardCrawlGamePatch.java: -------------------------------------------------------------------------------- 1 | package communicationmod.patches; 2 | 3 | import com.evacipated.cardcrawl.modthespire.lib.SpirePatch; 4 | import com.evacipated.cardcrawl.modthespire.lib.SpirePostfixPatch; 5 | import com.megacrit.cardcrawl.core.CardCrawlGame; 6 | import communicationmod.CommunicationMod; 7 | 8 | @SpirePatch( 9 | clz=CardCrawlGame.class, 10 | method="dispose" 11 | ) 12 | public class CardCrawlGamePatch { 13 | 14 | public static void Prefix(CardCrawlGame _instance) { 15 | CommunicationMod.dispose(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/communicationmod/patches/SetDialogOptionPatch.java: -------------------------------------------------------------------------------- 1 | package communicationmod.patches; 2 | 3 | import com.evacipated.cardcrawl.modthespire.lib.SpirePatch; 4 | import com.megacrit.cardcrawl.events.GenericEventDialog; 5 | import communicationmod.GameStateListener; 6 | 7 | @SpirePatch( 8 | clz= GenericEventDialog.class, 9 | method="setDialogOption", 10 | paramtypez = {String.class} 11 | ) 12 | public class SetDialogOptionPatch { 13 | public static void Postfix(GenericEventDialog _instance, String _arg) { 14 | GameStateListener.registerStateChange(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/communicationmod/patches/GameActionManagerTopPatch.java: -------------------------------------------------------------------------------- 1 | package communicationmod.patches; 2 | 3 | import com.evacipated.cardcrawl.modthespire.lib.SpirePatch; 4 | import com.megacrit.cardcrawl.actions.AbstractGameAction; 5 | import com.megacrit.cardcrawl.actions.GameActionManager; 6 | import communicationmod.GameStateListener; 7 | 8 | @SpirePatch( 9 | clz= GameActionManager.class, 10 | method="addToTop" 11 | ) 12 | public class GameActionManagerTopPatch { 13 | public static void Postfix(GameActionManager _instance, AbstractGameAction _arg) { 14 | GameStateListener.registerStateChange(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/communicationmod/patches/GameActionManagerBottomPatch.java: -------------------------------------------------------------------------------- 1 | package communicationmod.patches; 2 | 3 | import com.evacipated.cardcrawl.modthespire.lib.SpirePatch; 4 | import com.megacrit.cardcrawl.actions.AbstractGameAction; 5 | import com.megacrit.cardcrawl.actions.GameActionManager; 6 | import communicationmod.GameStateListener; 7 | 8 | @SpirePatch( 9 | clz= GameActionManager.class, 10 | method="addToBottom" 11 | ) 12 | public class GameActionManagerBottomPatch { 13 | public static void Postfix(GameActionManager _instance, AbstractGameAction _arg) { 14 | GameStateListener.registerStateChange(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/communicationmod/patches/AbstractRoomEndTurnPatch.java: -------------------------------------------------------------------------------- 1 | package communicationmod.patches; 2 | 3 | import com.evacipated.cardcrawl.modthespire.lib.SpirePatch; 4 | import com.megacrit.cardcrawl.dungeons.AbstractDungeon; 5 | import com.megacrit.cardcrawl.rooms.AbstractRoom; 6 | import communicationmod.EndOfTurnAction; 7 | 8 | public class AbstractRoomEndTurnPatch { 9 | 10 | @SpirePatch( 11 | clz= AbstractRoom.class, 12 | method="endTurn" 13 | ) 14 | public static class EndTurnPatch { 15 | public static void Postfix(AbstractRoom _instance) { 16 | AbstractDungeon.actionManager.addToBottom(new EndOfTurnAction()); 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/communicationmod/patches/HandCardSelectScreenPatch.java: -------------------------------------------------------------------------------- 1 | package communicationmod.patches; 2 | 3 | 4 | import com.evacipated.cardcrawl.modthespire.lib.SpirePatch; 5 | import com.megacrit.cardcrawl.core.Settings; 6 | import com.megacrit.cardcrawl.screens.select.HandCardSelectScreen; 7 | import communicationmod.GameStateListener; 8 | 9 | @SpirePatch( 10 | clz= HandCardSelectScreen.class, 11 | method="selectHoveredCard" 12 | ) 13 | public class HandCardSelectScreenPatch { 14 | 15 | public static void Postfix(HandCardSelectScreen _instance) { 16 | // If the card selection was going to trigger a screen close due to the quick card select option, don't register the state change. 17 | if (!(Settings.FAST_HAND_CONF && _instance.numCardsToSelect == 1 && _instance.selectedCards.size() == 1 && !_instance.canPickZero)) { 18 | GameStateListener.registerStateChange(); 19 | 20 | } 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/communicationmod/patches/GridCardSelectScreenPatch.java: -------------------------------------------------------------------------------- 1 | package communicationmod.patches; 2 | 3 | import basemod.ReflectionHacks; 4 | import com.evacipated.cardcrawl.modthespire.lib.SpirePatch; 5 | import com.megacrit.cardcrawl.cards.AbstractCard; 6 | import com.megacrit.cardcrawl.screens.select.GridCardSelectScreen; 7 | 8 | @SpirePatch( 9 | clz = GridCardSelectScreen.class, 10 | method = "updateCardPositionsAndHoverLogic" 11 | ) 12 | public class GridCardSelectScreenPatch { 13 | 14 | public static AbstractCard hoverCard; 15 | public static boolean replaceHoverCard = false; 16 | 17 | public static void Postfix(GridCardSelectScreen _instance) { 18 | if(replaceHoverCard) { 19 | ReflectionHacks.setPrivate(_instance, GridCardSelectScreen.class, "hoveredCard", hoverCard); 20 | hoverCard.hb.hovered = true; 21 | hoverCard.hb.clicked = true; 22 | replaceHoverCard = false; 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 ForgottenArbiter 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/main/java/communicationmod/patches/RoomEventDialogPatch.java: -------------------------------------------------------------------------------- 1 | package communicationmod.patches; 2 | 3 | import com.evacipated.cardcrawl.modthespire.lib.*; 4 | import com.evacipated.cardcrawl.modthespire.patcher.PatchingException; 5 | import com.megacrit.cardcrawl.events.RoomEventDialog; 6 | import communicationmod.GameStateListener; 7 | import javassist.CannotCompileException; 8 | import javassist.CtBehavior; 9 | 10 | import java.util.ArrayList; 11 | 12 | @SpirePatch( 13 | clz= RoomEventDialog.class, 14 | method="update" 15 | ) 16 | 17 | public class RoomEventDialogPatch { 18 | 19 | @SpireInsertPatch( 20 | locator=Locator.class 21 | ) 22 | public static void Insert(RoomEventDialog _instance) { 23 | GameStateListener.registerStateChange(); 24 | } 25 | 26 | private static class Locator extends SpireInsertLocator { 27 | public int[] Locate(CtBehavior ctMethodToPatch) throws CannotCompileException, PatchingException { 28 | Matcher matcher = new Matcher.FieldAccessMatcher(RoomEventDialog.class, "selectedOption"); 29 | return LineFinder.findInOrder(ctMethodToPatch, new ArrayList(), matcher); 30 | } 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/communicationmod/patches/CampfireLiftEffectPatch.java: -------------------------------------------------------------------------------- 1 | package communicationmod.patches; 2 | 3 | import com.evacipated.cardcrawl.modthespire.lib.*; 4 | import com.evacipated.cardcrawl.modthespire.patcher.PatchingException; 5 | import com.megacrit.cardcrawl.vfx.campfire.CampfireLiftEffect; 6 | import communicationmod.GameStateListener; 7 | import javassist.CannotCompileException; 8 | import javassist.CtBehavior; 9 | 10 | import java.util.ArrayList; 11 | 12 | @SpirePatch( 13 | clz= CampfireLiftEffect.class, 14 | method="update" 15 | ) 16 | public class CampfireLiftEffectPatch { 17 | 18 | @SpireInsertPatch( 19 | locator=Locator.class 20 | ) 21 | public static void Insert(CampfireLiftEffect _instance) { 22 | GameStateListener.resumeStateUpdate(); 23 | } 24 | 25 | private static class Locator extends SpireInsertLocator { 26 | public int[] Locate(CtBehavior ctMethodToPatch) throws CannotCompileException, PatchingException { 27 | Matcher matcher = new Matcher.FieldAccessMatcher(CampfireLiftEffect.class, "isDone"); 28 | return LineFinder.findInOrder(ctMethodToPatch, new ArrayList(), matcher); 29 | } 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/communicationmod/patches/GenericEventDialogPatch.java: -------------------------------------------------------------------------------- 1 | package communicationmod.patches; 2 | 3 | import com.evacipated.cardcrawl.modthespire.lib.*; 4 | import com.evacipated.cardcrawl.modthespire.patcher.PatchingException; 5 | import com.megacrit.cardcrawl.events.GenericEventDialog; 6 | import communicationmod.GameStateListener; 7 | import javassist.CannotCompileException; 8 | import javassist.CtBehavior; 9 | 10 | import java.util.ArrayList; 11 | 12 | @SpirePatch( 13 | clz= GenericEventDialog.class, 14 | method="update" 15 | ) 16 | 17 | public class GenericEventDialogPatch { 18 | 19 | @SpireInsertPatch( 20 | locator=Locator.class 21 | ) 22 | public static void Insert(GenericEventDialog _instance) { 23 | GameStateListener.registerStateChange(); 24 | } 25 | 26 | private static class Locator extends SpireInsertLocator { 27 | public int[] Locate(CtBehavior ctMethodToPatch) throws CannotCompileException, PatchingException { 28 | Matcher matcher = new Matcher.FieldAccessMatcher(GenericEventDialog.class, "selectedOption"); 29 | return LineFinder.findInOrder(ctMethodToPatch, new ArrayList(), matcher); 30 | } 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/communicationmod/patches/UpdateBodyTextPatch.java: -------------------------------------------------------------------------------- 1 | package communicationmod.patches; 2 | 3 | import com.evacipated.cardcrawl.modthespire.lib.SpirePatch; 4 | import com.megacrit.cardcrawl.events.GenericEventDialog; 5 | import com.megacrit.cardcrawl.events.RoomEventDialog; 6 | import com.megacrit.cardcrawl.ui.DialogWord; 7 | 8 | public class UpdateBodyTextPatch { 9 | 10 | public static String bodyText = ""; 11 | 12 | @SpirePatch( 13 | clz= RoomEventDialog.class, 14 | method = "updateBodyText", 15 | paramtypez = {String.class, DialogWord.AppearEffect.class} 16 | ) 17 | public static class RoomEventPatch { 18 | public static void Prefix(RoomEventDialog _instance, String text, DialogWord.AppearEffect ae) { 19 | bodyText = text; 20 | } 21 | } 22 | 23 | @SpirePatch( 24 | clz= GenericEventDialog.class, 25 | method = "updateBodyText", 26 | paramtypez = {String.class, DialogWord.AppearEffect.class} 27 | ) 28 | public static class ImageEventPatch { 29 | public static void Prefix(GenericEventDialog _instance, String text, DialogWord.AppearEffect ae) { 30 | bodyText = text; 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/communicationmod/patches/InputActionPatch.java: -------------------------------------------------------------------------------- 1 | package communicationmod.patches; 2 | 3 | import com.evacipated.cardcrawl.modthespire.lib.SpirePatch; 4 | import com.evacipated.cardcrawl.modthespire.lib.SpireReturn; 5 | import com.megacrit.cardcrawl.helpers.input.InputAction; 6 | 7 | public class InputActionPatch { 8 | 9 | public static boolean doKeypress = false; 10 | public static int key = 0; 11 | 12 | @SpirePatch( 13 | clz= InputAction.class, 14 | method="isJustPressed" 15 | ) 16 | public static class JustPressedPatch { 17 | 18 | public static SpireReturn Prefix(InputAction _instance, int ___keycode) { 19 | if (doKeypress && ___keycode == key) { 20 | return SpireReturn.Return(true); 21 | } else { 22 | return SpireReturn.Continue(); 23 | } 24 | } 25 | 26 | } 27 | 28 | @SpirePatch( 29 | clz=InputAction.class, 30 | method="isPressed" 31 | ) 32 | public static class PressedPatch { 33 | 34 | public static SpireReturn Prefix(InputAction _instance, int ___keycode) { 35 | if (doKeypress && ___keycode == key) { 36 | return SpireReturn.Return(true); 37 | } else { 38 | return SpireReturn.Continue(); 39 | } 40 | } 41 | 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/communicationmod/patches/CombatRewardScreenPatch.java: -------------------------------------------------------------------------------- 1 | package communicationmod.patches; 2 | 3 | import com.megacrit.cardcrawl.rewards.RewardItem; 4 | import com.megacrit.cardcrawl.screens.CombatRewardScreen; 5 | import com.evacipated.cardcrawl.modthespire.lib.*; 6 | import com.evacipated.cardcrawl.modthespire.patcher.PatchingException; 7 | import communicationmod.GameStateListener; 8 | import javassist.CannotCompileException; 9 | import javassist.CtBehavior; 10 | 11 | import java.util.ArrayList; 12 | 13 | @SpirePatch( 14 | clz= CombatRewardScreen.class, 15 | method="rewardViewUpdate" 16 | ) 17 | public class CombatRewardScreenPatch { 18 | 19 | 20 | @SpireInsertPatch( 21 | locator=Locator.class 22 | ) 23 | public static void Insert(CombatRewardScreen _instance) { 24 | // This will deal with linked relics / keys 25 | for(RewardItem reward : _instance.rewards) { 26 | if (reward.isDone) { 27 | return; 28 | } 29 | } 30 | GameStateListener.registerStateChange(); 31 | } 32 | 33 | private static class Locator extends SpireInsertLocator { 34 | public int[] Locate(CtBehavior ctMethodToPatch) throws CannotCompileException, PatchingException { 35 | Matcher matcher = new Matcher.MethodCallMatcher(CombatRewardScreen.class, "setLabel"); 36 | return LineFinder.findInOrder(ctMethodToPatch, new ArrayList(), matcher); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/communicationmod/patches/DungeonMapPatch.java: -------------------------------------------------------------------------------- 1 | package communicationmod.patches; 2 | 3 | import com.evacipated.cardcrawl.modthespire.lib.*; 4 | import com.evacipated.cardcrawl.modthespire.patcher.PatchingException; 5 | import com.megacrit.cardcrawl.helpers.Hitbox; 6 | import com.megacrit.cardcrawl.helpers.input.InputHelper; 7 | import com.megacrit.cardcrawl.map.DungeonMap; 8 | import javassist.CannotCompileException; 9 | import javassist.CtBehavior; 10 | 11 | import java.util.ArrayList; 12 | 13 | @SpirePatch( 14 | clz=DungeonMap.class, 15 | method="update" 16 | ) 17 | public class DungeonMapPatch { 18 | 19 | public static boolean doBossHover = false; 20 | 21 | @SpireInsertPatch( 22 | locator=Locator.class 23 | ) 24 | public static void Insert(DungeonMap _instance) { 25 | 26 | if(doBossHover) { 27 | _instance.bossHb.hovered = true; 28 | InputHelper.justClickedLeft = true; 29 | doBossHover = false; 30 | } 31 | } 32 | 33 | private static class Locator extends SpireInsertLocator { 34 | public int[] Locate(CtBehavior ctMethodToPatch) throws CannotCompileException, PatchingException { 35 | Matcher matcher = new Matcher.MethodCallMatcher(Hitbox.class, "update"); 36 | int[] results = LineFinder.findInOrder(ctMethodToPatch, new ArrayList(), matcher); 37 | results[0] += 1; 38 | return results; 39 | } 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/communicationmod/patches/MapRoomNodeHoverPatch.java: -------------------------------------------------------------------------------- 1 | package communicationmod.patches; 2 | 3 | import com.evacipated.cardcrawl.modthespire.lib.*; 4 | import com.evacipated.cardcrawl.modthespire.patcher.PatchingException; 5 | import com.megacrit.cardcrawl.helpers.Hitbox; 6 | import com.megacrit.cardcrawl.map.MapRoomNode; 7 | import javassist.CannotCompileException; 8 | import javassist.CtBehavior; 9 | 10 | import java.util.ArrayList; 11 | import java.util.Map; 12 | 13 | @SpirePatch( 14 | clz= MapRoomNode.class, 15 | method="update" 16 | ) 17 | public class MapRoomNodeHoverPatch { 18 | 19 | public static MapRoomNode hoverNode; 20 | public static boolean doHover = false; 21 | 22 | @SpireInsertPatch( 23 | locator=Locator.class 24 | ) 25 | public static void Insert(MapRoomNode _instance) { 26 | 27 | if(doHover) { 28 | if(hoverNode == _instance) { 29 | _instance.hb.hovered = true; 30 | doHover = false; 31 | } else { 32 | _instance.hb.hovered = false; 33 | } 34 | } 35 | } 36 | 37 | private static class Locator extends SpireInsertLocator { 38 | public int[] Locate(CtBehavior ctMethodToPatch) throws CannotCompileException, PatchingException { 39 | Matcher matcher = new Matcher.MethodCallMatcher(Hitbox.class, "update"); 40 | int[] results = LineFinder.findInOrder(ctMethodToPatch, new ArrayList(), matcher); 41 | results[0] += 1; 42 | return results; 43 | } 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/communicationmod/DataWriter.java: -------------------------------------------------------------------------------- 1 | package communicationmod; 2 | 3 | import org.apache.logging.log4j.LogManager; 4 | import org.apache.logging.log4j.Logger; 5 | 6 | import java.io.IOException; 7 | import java.io.OutputStream; 8 | import java.util.concurrent.BlockingQueue; 9 | 10 | public class DataWriter implements Runnable { 11 | 12 | private final BlockingQueue queue; 13 | private final OutputStream stream; 14 | private boolean verbose; 15 | private static final Logger logger = LogManager.getLogger(DataWriter.class.getName()); 16 | 17 | public DataWriter(BlockingQueue queue, OutputStream stream, boolean verbose) { 18 | this.queue = queue; 19 | this.stream = stream; 20 | this.verbose = verbose; 21 | } 22 | 23 | public void run() { 24 | String message = ""; 25 | while (!Thread.currentThread().isInterrupted()) { 26 | try { 27 | message = this.queue.take(); 28 | if (verbose) { 29 | logger.info("Sending message: " + message); 30 | } 31 | stream.write(message.getBytes()); 32 | stream.write('\n'); 33 | stream.flush(); 34 | } catch (InterruptedException e) { 35 | logger.info("Communications writing thread interrupted."); 36 | Thread.currentThread().interrupt(); 37 | } catch (IOException e) { 38 | logger.error("Message could not be sent to child process: " + message); 39 | e.printStackTrace(); 40 | } 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/communicationmod/patches/MerchantPatch.java: -------------------------------------------------------------------------------- 1 | package communicationmod.patches; 2 | 3 | import com.evacipated.cardcrawl.modthespire.lib.*; 4 | import com.evacipated.cardcrawl.modthespire.patcher.PatchingException; 5 | import com.megacrit.cardcrawl.helpers.Hitbox; 6 | import com.megacrit.cardcrawl.helpers.input.InputHelper; 7 | import com.megacrit.cardcrawl.shop.Merchant; 8 | import javassist.CannotCompileException; 9 | import javassist.CtBehavior; 10 | 11 | import java.util.ArrayList; 12 | 13 | public class MerchantPatch { 14 | 15 | public static boolean visitMerchant = false; 16 | 17 | @SpirePatch( 18 | clz=Merchant.class, 19 | method="update" 20 | ) 21 | public static class MerchantUpdatePatch { 22 | 23 | @SpireInsertPatch( 24 | locator=Locator.class 25 | ) 26 | public static void Insert(Merchant _instance) { 27 | if(visitMerchant) { 28 | _instance.hb.hovered = true; 29 | InputHelper.justClickedLeft = true; 30 | visitMerchant = false; 31 | } 32 | 33 | } 34 | 35 | private static class Locator extends SpireInsertLocator { 36 | public int[] Locate(CtBehavior ctMethodToPatch) throws CannotCompileException, PatchingException { 37 | Matcher matcher = new Matcher.MethodCallMatcher(Hitbox.class, "update"); 38 | int[] results = LineFinder.findInOrder(ctMethodToPatch, new ArrayList(), matcher); 39 | results[0] += 1; 40 | return results; 41 | } 42 | } 43 | 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/communicationmod/patches/GridCardSelectScreenUpdatePatch.java: -------------------------------------------------------------------------------- 1 | package communicationmod.patches; 2 | 3 | import com.evacipated.cardcrawl.modthespire.lib.*; 4 | import com.evacipated.cardcrawl.modthespire.patcher.PatchingException; 5 | import com.megacrit.cardcrawl.helpers.Hitbox; 6 | import com.megacrit.cardcrawl.screens.select.GridCardSelectScreen; 7 | import communicationmod.GameStateListener; 8 | import javassist.CannotCompileException; 9 | import javassist.CtBehavior; 10 | 11 | import java.util.ArrayList; 12 | 13 | import static com.evacipated.cardcrawl.modthespire.lib.LineFinder.findAllInOrder; 14 | 15 | @SpirePatch( 16 | clz= GridCardSelectScreen.class, 17 | method="update" 18 | ) 19 | public class GridCardSelectScreenUpdatePatch { 20 | 21 | @SpireInsertPatch( 22 | locator=Locator.class 23 | ) 24 | public static void Insert(GridCardSelectScreen _instance) { 25 | GameStateListener.registerStateChange(); 26 | } 27 | 28 | private static class Locator extends SpireInsertLocator { 29 | public int[] Locate(CtBehavior ctMethodToPatch) throws CannotCompileException, PatchingException { 30 | Matcher matcher = new Matcher.FieldAccessMatcher(Hitbox.class, "clicked"); 31 | int[] matches = LineFinder.findAllInOrder(ctMethodToPatch, new ArrayList(), matcher); 32 | int[] selectedMatches = new int[matches.length/2]; 33 | for(int i = 0; i < matches.length; i++) { 34 | if(i % 2 == 1) { 35 | selectedMatches[i/2] = matches[i]; // Take every other access to hb.clicked, as the others are in if statements 36 | } 37 | } 38 | return selectedMatches; 39 | } 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/communicationmod/patches/CampfireDigEffectPatch.java: -------------------------------------------------------------------------------- 1 | package communicationmod.patches; 2 | 3 | import com.evacipated.cardcrawl.modthespire.lib.*; 4 | import com.evacipated.cardcrawl.modthespire.patcher.PatchingException; 5 | import com.megacrit.cardcrawl.metrics.MetricData; 6 | import com.megacrit.cardcrawl.vfx.campfire.CampfireDigEffect; 7 | import communicationmod.GameStateListener; 8 | import javassist.CannotCompileException; 9 | import javassist.CtBehavior; 10 | 11 | import java.util.ArrayList; 12 | 13 | public class CampfireDigEffectPatch { 14 | 15 | @SpireInsertPatch( 16 | locator=LocatorAfter.class 17 | ) 18 | public static void After(CampfireDigEffect _instance) { 19 | GameStateListener.resumeStateUpdate(); 20 | } 21 | 22 | private static class LocatorAfter extends SpireInsertLocator { 23 | public int[] Locate(CtBehavior ctMethodToPatch) throws CannotCompileException, PatchingException { 24 | Matcher matcher = new Matcher.FieldAccessMatcher(CampfireDigEffect.class, "isDone"); 25 | return LineFinder.findInOrder(ctMethodToPatch, new ArrayList(), matcher); 26 | } 27 | } 28 | 29 | @SpireInsertPatch( 30 | locator=LocatorBefore.class 31 | ) 32 | public static void Before(CampfireDigEffect _instance) { 33 | GameStateListener.blockStateUpdate(); 34 | } 35 | 36 | private static class LocatorBefore extends SpireInsertLocator { 37 | public int[] Locate(CtBehavior ctMethodToPatch) throws CannotCompileException, PatchingException { 38 | Matcher matcher = new Matcher.MethodCallMatcher(MetricData.class, "addCampfireChoiceData"); 39 | return LineFinder.findInOrder(ctMethodToPatch, new ArrayList(), matcher); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/communicationmod/InvalidCommandException.java: -------------------------------------------------------------------------------- 1 | package communicationmod; 2 | 3 | public class InvalidCommandException extends Exception { 4 | 5 | private String[] command; 6 | private InvalidCommandFormat format; 7 | private String message = ""; 8 | 9 | public InvalidCommandException(String[] command, InvalidCommandFormat format, String message) { 10 | super(); 11 | this.command = command; 12 | this.format = format; 13 | this.message = message; 14 | } 15 | 16 | public InvalidCommandException(String[] command, InvalidCommandFormat format) { 17 | super(); 18 | this.command = command; 19 | this.format = format; 20 | } 21 | 22 | public InvalidCommandException(String message) { 23 | super(); 24 | this.message = message; 25 | this.format = InvalidCommandFormat.SIMPLE; 26 | this.command = new String[1]; 27 | this.command[0] = ""; 28 | } 29 | 30 | public String getMessage() { 31 | String wholeCommand = String.join(" ", this.command); 32 | switch (this.format) { 33 | case OUT_OF_BOUNDS: 34 | return String.format("Index %s out of bounds in command \"%s\"", this.message, wholeCommand); 35 | case MISSING_ARGUMENT: 36 | return String.format("Argument missing in command \"%s\".%s", wholeCommand, this.message); 37 | case INVALID_ARGUMENT: 38 | return String.format("Invalid argument %s in command \"%s\".", this.message, wholeCommand); 39 | default: 40 | return this.message; 41 | } 42 | } 43 | 44 | public enum InvalidCommandFormat { 45 | OUT_OF_BOUNDS, 46 | MISSING_ARGUMENT, 47 | INVALID_ARGUMENT, 48 | SIMPLE 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/communicationmod/patches/CampfireSleepEffectPatch.java: -------------------------------------------------------------------------------- 1 | package communicationmod.patches; 2 | 3 | import com.evacipated.cardcrawl.modthespire.lib.*; 4 | import com.evacipated.cardcrawl.modthespire.patcher.PatchingException; 5 | import com.megacrit.cardcrawl.metrics.MetricData; 6 | import com.megacrit.cardcrawl.vfx.campfire.CampfireSleepEffect; 7 | import communicationmod.GameStateListener; 8 | import javassist.CannotCompileException; 9 | import javassist.CtBehavior; 10 | 11 | import java.util.ArrayList; 12 | 13 | public class CampfireSleepEffectPatch { 14 | 15 | @SpireInsertPatch( 16 | locator=LocatorAfter.class 17 | ) 18 | public static void After(CampfireSleepEffect _instance) { 19 | GameStateListener.resumeStateUpdate(); 20 | } 21 | 22 | private static class LocatorAfter extends SpireInsertLocator { 23 | public int[] Locate(CtBehavior ctMethodToPatch) throws CannotCompileException, PatchingException { 24 | Matcher matcher = new Matcher.FieldAccessMatcher(CampfireSleepEffect.class, "isDone"); 25 | return LineFinder.findInOrder(ctMethodToPatch, new ArrayList(), matcher); 26 | } 27 | } 28 | 29 | @SpireInsertPatch( 30 | locator=LocatorBefore.class 31 | ) 32 | public static void Before(CampfireSleepEffect _instance) { 33 | GameStateListener.blockStateUpdate(); 34 | } 35 | 36 | private static class LocatorBefore extends SpireInsertLocator { 37 | public int[] Locate(CtBehavior ctMethodToPatch) throws CannotCompileException, PatchingException { 38 | Matcher matcher = new Matcher.MethodCallMatcher(CampfireSleepEffect.class, "playSleepJingle"); 39 | return LineFinder.findInOrder(ctMethodToPatch, new ArrayList(), matcher); 40 | } 41 | } 42 | } 43 | 44 | -------------------------------------------------------------------------------- /src/main/java/communicationmod/patches/CampfireTokeEffectPatch.java: -------------------------------------------------------------------------------- 1 | package communicationmod.patches; 2 | 3 | import com.evacipated.cardcrawl.modthespire.lib.*; 4 | import com.evacipated.cardcrawl.modthespire.patcher.PatchingException; 5 | import com.megacrit.cardcrawl.metrics.MetricData; 6 | import com.megacrit.cardcrawl.vfx.campfire.CampfireTokeEffect; 7 | import communicationmod.GameStateListener; 8 | import javassist.CannotCompileException; 9 | import javassist.CtBehavior; 10 | 11 | import java.util.ArrayList; 12 | 13 | @SpirePatch( 14 | clz= CampfireTokeEffect.class, 15 | method="update" 16 | ) 17 | public class CampfireTokeEffectPatch { 18 | 19 | @SpireInsertPatch( 20 | locator=LocatorAfter.class 21 | ) 22 | public static void After(CampfireTokeEffect _instance) { 23 | GameStateListener.resumeStateUpdate(); 24 | } 25 | 26 | private static class LocatorAfter extends SpireInsertLocator { 27 | public int[] Locate(CtBehavior ctMethodToPatch) throws CannotCompileException, PatchingException { 28 | Matcher matcher = new Matcher.FieldAccessMatcher(CampfireTokeEffect.class, "isDone"); 29 | return LineFinder.findInOrder(ctMethodToPatch, new ArrayList(), matcher); 30 | } 31 | } 32 | 33 | @SpireInsertPatch( 34 | locator=LocatorBefore.class 35 | ) 36 | public static void Before(CampfireTokeEffect _instance) { 37 | GameStateListener.blockStateUpdate(); 38 | } 39 | 40 | private static class LocatorBefore extends SpireInsertLocator { 41 | public int[] Locate(CtBehavior ctMethodToPatch) throws CannotCompileException, PatchingException { 42 | Matcher matcher = new Matcher.MethodCallMatcher(MetricData.class, "addCampfireChoiceData"); 43 | return LineFinder.findInOrder(ctMethodToPatch, new ArrayList(), matcher); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/communicationmod/patches/CampfireSmithEffectPatch.java: -------------------------------------------------------------------------------- 1 | package communicationmod.patches; 2 | 3 | import com.evacipated.cardcrawl.modthespire.lib.*; 4 | import com.evacipated.cardcrawl.modthespire.patcher.PatchingException; 5 | import com.megacrit.cardcrawl.metrics.MetricData; 6 | import com.megacrit.cardcrawl.vfx.campfire.CampfireSmithEffect; 7 | import communicationmod.GameStateListener; 8 | import javassist.CannotCompileException; 9 | import javassist.CtBehavior; 10 | 11 | import java.util.ArrayList; 12 | 13 | @SpirePatch( 14 | clz= CampfireSmithEffect.class, 15 | method="update" 16 | ) 17 | public class CampfireSmithEffectPatch { 18 | 19 | @SpireInsertPatch( 20 | locator=LocatorAfter.class 21 | ) 22 | public static void After(CampfireSmithEffect _instance) { 23 | GameStateListener.resumeStateUpdate(); 24 | } 25 | 26 | private static class LocatorAfter extends SpireInsertLocator { 27 | public int[] Locate(CtBehavior ctMethodToPatch) throws CannotCompileException, PatchingException { 28 | Matcher matcher = new Matcher.FieldAccessMatcher(CampfireSmithEffect.class, "isDone"); 29 | return LineFinder.findInOrder(ctMethodToPatch, new ArrayList(), matcher); 30 | } 31 | } 32 | 33 | @SpireInsertPatch( 34 | locator=LocatorBefore.class 35 | ) 36 | public static void Before(CampfireSmithEffect _instance) { 37 | GameStateListener.blockStateUpdate(); 38 | } 39 | 40 | private static class LocatorBefore extends SpireInsertLocator { 41 | public int[] Locate(CtBehavior ctMethodToPatch) throws CannotCompileException, PatchingException { 42 | Matcher matcher = new Matcher.MethodCallMatcher(MetricData.class, "addCampfireChoiceData"); 43 | return LineFinder.findInOrder(ctMethodToPatch, new ArrayList(), matcher); 44 | } 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/communicationmod/patches/CampfireRecallEffectPatch.java: -------------------------------------------------------------------------------- 1 | package communicationmod.patches; 2 | 3 | import com.evacipated.cardcrawl.modthespire.lib.*; 4 | import com.evacipated.cardcrawl.modthespire.patcher.PatchingException; 5 | import com.megacrit.cardcrawl.metrics.MetricData; 6 | import com.megacrit.cardcrawl.vfx.campfire.CampfireRecallEffect; 7 | import communicationmod.GameStateListener; 8 | import javassist.CannotCompileException; 9 | import javassist.CtBehavior; 10 | 11 | import java.util.ArrayList; 12 | 13 | @SpirePatch( 14 | clz= CampfireRecallEffect.class, 15 | method="update" 16 | ) 17 | public class CampfireRecallEffectPatch { 18 | 19 | @SpireInsertPatch( 20 | locator=LocatorAfter.class 21 | ) 22 | public static void After(CampfireRecallEffect _instance) { 23 | GameStateListener.resumeStateUpdate(); 24 | } 25 | 26 | private static class LocatorAfter extends SpireInsertLocator { 27 | public int[] Locate(CtBehavior ctMethodToPatch) throws CannotCompileException, PatchingException { 28 | Matcher matcher = new Matcher.FieldAccessMatcher(CampfireRecallEffect.class, "isDone"); 29 | return LineFinder.findInOrder(ctMethodToPatch, new ArrayList(), matcher); 30 | } 31 | } 32 | 33 | @SpireInsertPatch( 34 | locator=LocatorBefore.class 35 | ) 36 | public static void Before(CampfireRecallEffect _instance) { 37 | GameStateListener.blockStateUpdate(); 38 | } 39 | 40 | private static class LocatorBefore extends SpireInsertLocator { 41 | public int[] Locate(CtBehavior ctMethodToPatch) throws CannotCompileException, PatchingException { 42 | Matcher matcher = new Matcher.MethodCallMatcher(MetricData.class, "addCampfireChoiceData"); 43 | return LineFinder.findInOrder(ctMethodToPatch, new ArrayList(), matcher); 44 | } 45 | } 46 | 47 | } 48 | 49 | -------------------------------------------------------------------------------- /src/main/java/communicationmod/DataReader.java: -------------------------------------------------------------------------------- 1 | package communicationmod; 2 | 3 | import org.apache.logging.log4j.LogManager; 4 | import org.apache.logging.log4j.Logger; 5 | 6 | import java.io.IOException; 7 | import java.io.InputStream; 8 | import java.util.concurrent.BlockingQueue; 9 | 10 | public class DataReader implements Runnable{ 11 | 12 | private final BlockingQueue queue; 13 | private final InputStream stream; 14 | private static final Logger logger = LogManager.getLogger(DataReader.class.getName()); 15 | private boolean verbose; 16 | 17 | public DataReader (BlockingQueue queue, InputStream stream, boolean verbose) { 18 | this.queue = queue; 19 | this.stream = stream; 20 | this.verbose = verbose; 21 | } 22 | 23 | public void run() { 24 | while (!Thread.currentThread().isInterrupted()) { 25 | StringBuilder inputBuffer = new StringBuilder(); 26 | try { 27 | while (true) { 28 | int nextChar = this.stream.read(); 29 | if (nextChar == -1) { 30 | continue; 31 | } else if (nextChar == 0 || nextChar == '\n') { 32 | break; 33 | } 34 | inputBuffer.append((char) nextChar); 35 | } 36 | if (inputBuffer.length() > 0) { 37 | if (verbose) { 38 | logger.info("Received message: " + inputBuffer.toString()); 39 | } 40 | queue.put(inputBuffer.toString()); 41 | } 42 | } catch(IOException e){ 43 | logger.error("Message could not be received from child process. Shutting down reading thread."); 44 | Thread.currentThread().interrupt(); 45 | } catch (InterruptedException e) { 46 | logger.info("Communications reading thread interrupted."); 47 | Thread.currentThread().interrupt(); 48 | } 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/communicationmod/patches/ShopScreenPatch.java: -------------------------------------------------------------------------------- 1 | package communicationmod.patches; 2 | 3 | import basemod.ReflectionHacks; 4 | import com.evacipated.cardcrawl.modthespire.lib.*; 5 | import com.evacipated.cardcrawl.modthespire.patcher.PatchingException; 6 | import com.megacrit.cardcrawl.cards.AbstractCard; 7 | import com.megacrit.cardcrawl.shop.ShopScreen; 8 | import communicationmod.GameStateListener; 9 | import javassist.CannotCompileException; 10 | import javassist.CtBehavior; 11 | 12 | import java.util.ArrayList; 13 | 14 | public class ShopScreenPatch { 15 | 16 | public static boolean doHover = false; 17 | public static AbstractCard hoverCard; 18 | 19 | 20 | @SpirePatch( 21 | clz = ShopScreen.class, 22 | method = "purgeCard" 23 | ) 24 | public static class PurgeCardPatch { 25 | 26 | public static void Postfix() { 27 | GameStateListener.resumeStateUpdate(); // Needed to wait for the rest of the logic to complete after card was selected. 28 | } 29 | 30 | } 31 | 32 | 33 | @SpirePatch( 34 | clz=ShopScreen.class, 35 | method = "update" 36 | ) 37 | public static class HoverCardPatch { 38 | 39 | @SuppressWarnings("unchecked") 40 | @SpireInsertPatch( 41 | locator=Locator.class 42 | ) 43 | public static void Insert(ShopScreen _instance) { 44 | if(doHover) { 45 | ArrayList coloredCards = (ArrayList) ReflectionHacks.getPrivate(_instance, ShopScreen.class, "coloredCards"); 46 | ArrayList colorlessCards = (ArrayList) ReflectionHacks.getPrivate(_instance, ShopScreen.class, "colorlessCards"); 47 | for(AbstractCard card : coloredCards) { 48 | card.hb.hovered = card == hoverCard; 49 | } 50 | for(AbstractCard card : colorlessCards) { 51 | card.hb.hovered = card == hoverCard; 52 | } 53 | doHover = false; 54 | } 55 | } 56 | 57 | private static class Locator extends SpireInsertLocator { 58 | public int[] Locate(CtBehavior ctMethodToPatch) throws CannotCompileException, PatchingException { 59 | Matcher matcher = new Matcher.MethodCallMatcher(ShopScreen.class, "updateHand"); 60 | return LineFinder.findInOrder(ctMethodToPatch, new ArrayList(), matcher); 61 | } 62 | } 63 | 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## Changelog ## 2 | 3 | #### v1.2.1 #### 4 | * Fix an issue where the external process was not sent state 5 | 6 | #### v1.2.0 #### 7 | * Shade in gson in a way that avoids mod conflicts 8 | * Add java hooks for using Communication Mod as a mod dependency 9 | 10 | #### v1.1.0 #### 11 | * Added mod config panel toggle for verbosity option 12 | * Added ethereal to cards in the game state 13 | * Added number of times damaged to the game state 14 | * Fixed a bug where the wrong act boss was sometimes displayed 15 | 16 | #### v1.0.4 #### 17 | * Added low verbosity option (set verbose=false in config file) 18 | * Fixed compatibility with v2.2 of Slay the Spire 19 | 20 | #### v1.0.3 #### 21 | * Match and Keep! no longer hijacks the cursor 22 | * Match and Keep! no longer hangs when using SuperFastMode 23 | * Improved Match and Keep! event option descriptions and ordering 24 | * Do not crash if a start command is sent before Communication Mod is ready 25 | 26 | #### v1.0.2 #### 27 | * Added BaseMod as a mod dependency (so now you can't load them in the wrong order) 28 | 29 | #### v1.0.1 #### 30 | * Fixed crash when closing the boss chest 31 | * Fixed a bug causing the cancel command to do nothing once in a shop when the external program was launched from the mods menu 32 | 33 | #### v1.0.0 #### 34 | * Added key command 35 | * Added click command 36 | * Added wait command 37 | * Fixed compatibility with version 2.0 of StS 38 | 39 | #### v0.8.0 #### 40 | * Added card_in_play to the game state 41 | * Added the turn number to the game state 42 | * Added the number of cards discarded this turn to the game state 43 | * Added the monsters' last two move ids to the game state 44 | * Fixed crash with StS version 1.1 45 | * Fixed a bug where max energy would be transmitted instead of current energy 46 | 47 | #### v0.7.0 #### 48 | * Added Limbo to the game state, which is used for various cards such as Havoc 49 | * Added a number of new fields to specific powers which did not have all of their state captured 50 | 51 | #### v0.6.0 #### 52 | * Added "act_boss" to the game state, indicating the first boss to be fought in the current Act 53 | * Made Communication Mod compatible with Slay the Spire v1.1 54 | 55 | #### v0.5.0 #### 56 | * Added "any_number" to the grid select screen state, indicating whether any number of cards can be selected 57 | * Fixed "choose boss" rarely failing to select the boss node 58 | * Communication Mod now waits for more rest actions to finish before becoming ready, fixing a number of related issues 59 | 60 | #### v0.4.0 #### 61 | * Initial public release -------------------------------------------------------------------------------- /src/main/java/communicationmod/patches/CardRewardScreenPatch.java: -------------------------------------------------------------------------------- 1 | package communicationmod.patches; 2 | 3 | import com.evacipated.cardcrawl.modthespire.lib.*; 4 | import com.evacipated.cardcrawl.modthespire.patcher.PatchingException; 5 | import com.megacrit.cardcrawl.cards.AbstractCard; 6 | import com.megacrit.cardcrawl.screens.CardRewardScreen; 7 | import javassist.CannotCompileException; 8 | import javassist.CtBehavior; 9 | 10 | import java.util.ArrayList; 11 | 12 | public class CardRewardScreenPatch { 13 | 14 | public static boolean doHover = false; 15 | public static AbstractCard hoverCard; 16 | 17 | @SpirePatch( 18 | clz=CardRewardScreen.class, 19 | method = "cardSelectUpdate" 20 | ) 21 | public static class HoverCardPatch { 22 | 23 | @SpireInsertPatch( 24 | locator=Locator.class, 25 | localvars = {"c"} 26 | ) 27 | public static void Insert(CardRewardScreen _instance, AbstractCard c) { 28 | if(doHover) { 29 | if(c.equals(hoverCard)) { 30 | hoverCard.hb.hovered = true; 31 | } else { 32 | c.hb.hovered = false; 33 | } 34 | } 35 | } 36 | 37 | private static class Locator extends SpireInsertLocator { 38 | public int[] Locate(CtBehavior ctMethodToPatch) throws CannotCompileException, PatchingException { 39 | Matcher matcher = new Matcher.MethodCallMatcher(AbstractCard.class, "updateHoverLogic"); 40 | int[] match = LineFinder.findInOrder(ctMethodToPatch, new ArrayList(), matcher); 41 | match[0] += 1; 42 | return match; 43 | } 44 | } 45 | 46 | } 47 | 48 | @SpirePatch( 49 | clz=CardRewardScreen.class, 50 | method = "cardSelectUpdate" 51 | ) 52 | public static class AcquireCardPatch { 53 | 54 | @SpireInsertPatch( 55 | locator=Locator.class 56 | ) 57 | public static void Insert(CardRewardScreen _instance) { 58 | doHover = false; 59 | } 60 | 61 | private static class Locator extends SpireInsertLocator { 62 | public int[] Locate(CtBehavior ctMethodToPatch) throws CannotCompileException, PatchingException { 63 | Matcher matcher = new Matcher.FieldAccessMatcher(CardRewardScreen.class, "skipButton"); 64 | return LineFinder.findInOrder(ctMethodToPatch, new ArrayList(), matcher); 65 | } 66 | } 67 | 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/main/java/communicationmod/patches/AbstractRelicUpdatePatch.java: -------------------------------------------------------------------------------- 1 | package communicationmod.patches; 2 | 3 | import com.megacrit.cardcrawl.helpers.Hitbox; 4 | import com.megacrit.cardcrawl.relics.AbstractRelic; 5 | import com.evacipated.cardcrawl.modthespire.lib.*; 6 | import com.evacipated.cardcrawl.modthespire.patcher.PatchingException; 7 | import communicationmod.GameStateListener; 8 | import javassist.CannotCompileException; 9 | import javassist.CtBehavior; 10 | 11 | import java.util.ArrayList; 12 | 13 | @SpirePatch( 14 | clz= AbstractRelic.class, 15 | method="update" 16 | ) 17 | public class AbstractRelicUpdatePatch { 18 | 19 | public static AbstractRelic hoverRelic; 20 | public static boolean doHover = false; 21 | 22 | @SpireInsertPatch( 23 | locator=ObtainedLocator.class 24 | ) 25 | public static void BlockStateChange(AbstractRelic _instance) { 26 | // A relic's equip code isn't actually called until it reaches the top of the screen. 27 | // To avoid problems, we cannot report a state update until this happens. 28 | if(_instance.isObtained) { 29 | GameStateListener.blockStateUpdate(); 30 | } 31 | } 32 | 33 | private static class ObtainedLocator extends SpireInsertLocator { 34 | public int[] Locate(CtBehavior ctMethodToPatch) throws CannotCompileException, PatchingException { 35 | Matcher matcher = new Matcher.FieldAccessMatcher(AbstractRelic.class, "isObtained"); 36 | int[] results = LineFinder.findInOrder(ctMethodToPatch, new ArrayList(), matcher); 37 | results[0] += 1; 38 | return results; 39 | } 40 | } 41 | 42 | @SpireInsertPatch( 43 | locator=EquipLocator.class 44 | ) 45 | public static void ResumeStateChange(AbstractRelic _instance) { 46 | GameStateListener.resumeStateUpdate(); 47 | } 48 | 49 | private static class EquipLocator extends SpireInsertLocator { 50 | public int[] Locate(CtBehavior ctMethodToPatch) throws CannotCompileException, PatchingException { 51 | Matcher matcher = new Matcher.MethodCallMatcher(AbstractRelic.class, "onEquip"); 52 | return LineFinder.findInOrder(ctMethodToPatch, new ArrayList(), matcher); 53 | } 54 | } 55 | 56 | @SpireInsertPatch( 57 | locator=HitboxLocator.class 58 | ) 59 | public static void DoHitboxHover(AbstractRelic _instance) { 60 | if(doHover) { 61 | if(hoverRelic == _instance) { 62 | _instance.hb.hovered = true; 63 | _instance.hb.clicked = true; 64 | doHover = false; 65 | } else { 66 | _instance.hb.hovered = false; 67 | } 68 | } 69 | } 70 | 71 | private static class HitboxLocator extends SpireInsertLocator { 72 | public int[] Locate(CtBehavior ctMethodToPatch) throws CannotCompileException, PatchingException { 73 | Matcher matcher = new Matcher.MethodCallMatcher(Hitbox.class, "update"); 74 | int[] results = LineFinder.findInOrder(ctMethodToPatch, new ArrayList(), matcher); 75 | results[0] += 1; 76 | return results; 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 4.0.0 5 | 6 | autoplay 7 | CommunicationMod 8 | 1.2.1 9 | jar 10 | Communication Mod 11 | Used to help external programs communicate with Slay the Spire 12 | 13 | 14 | 1.8 15 | 1.8 16 | 11-30-2020 17 | 3.18.1 18 | 5.27.0 19 | 20 | 21 | 22 | 23 | com.megacrit.cardcrawl 24 | slaythespire 25 | ${SlayTheSpire.version} 26 | system 27 | ${basedir}/../lib/desktop-1.0.jar 28 | 29 | 30 | com.evacipated.cardcrawl 31 | ModTheSpire 32 | ${ModTheSpire.version} 33 | system 34 | ${basedir}/../lib/ModTheSpire.jar 35 | 36 | 37 | com.evacipated.cardcrawl 38 | BaseMod 39 | ${BaseMod.version} 40 | system 41 | ${basedir}/../lib/BaseMod.jar 42 | 43 | 44 | com.google.code.gson 45 | gson 46 | 2.8.9 47 | 48 | 49 | 50 | 51 | CommunicationMod 52 | 53 | 54 | org.apache.maven.plugins 55 | maven-shade-plugin 56 | 2.4.2 57 | 58 | 59 | CommunicationMod 60 | package 61 | 62 | shade 63 | 64 | 65 | true 66 | 67 | 68 | autoplay:CommunicationMod 69 | 70 | 71 | 72 | 73 | com.google.gson 74 | com.autoplay.gson 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | org.apache.maven.plugins 84 | maven-antrun-plugin 85 | 1.8 86 | 87 | 88 | package 89 | 90 | 91 | 92 | 93 | 94 | 95 | run 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | src/main/resources 105 | 106 | 107 | src/main/resources 108 | false 109 | 110 | ModTheSpire.json 111 | 112 | 113 | 114 | src/main/resources 115 | true 116 | 117 | ModTheSpire.json 118 | 119 | 120 | 121 | 122 | 123 | 124 | -------------------------------------------------------------------------------- /src/main/java/communicationmod/patches/GremlinMatchGamePatch.java: -------------------------------------------------------------------------------- 1 | package communicationmod.patches; 2 | 3 | import basemod.ReflectionHacks; 4 | import com.evacipated.cardcrawl.modthespire.lib.*; 5 | import com.evacipated.cardcrawl.modthespire.patcher.PatchingException; 6 | import com.megacrit.cardcrawl.cards.AbstractCard; 7 | import com.megacrit.cardcrawl.cards.CardGroup; 8 | import com.megacrit.cardcrawl.events.shrines.GremlinMatchGame; 9 | import com.megacrit.cardcrawl.helpers.Hitbox; 10 | import com.megacrit.cardcrawl.helpers.input.InputHelper; 11 | import communicationmod.GameStateListener; 12 | import javassist.CannotCompileException; 13 | import javassist.CtBehavior; 14 | 15 | import java.util.*; 16 | 17 | public class GremlinMatchGamePatch { 18 | 19 | public static HashMap cardPositions; 20 | public static CardGroup cards; 21 | public static Set revealedCards; 22 | 23 | public static ArrayList getOrderedCards() { 24 | ArrayList returnedCards = new ArrayList<>(cards.group); 25 | returnedCards.sort(Comparator.comparingInt(c -> cardPositions.get(c.uuid))); 26 | returnedCards.removeIf(c -> !c.isFlipped); 27 | return returnedCards; 28 | } 29 | 30 | @SpirePatch( 31 | clz=GremlinMatchGame.class, 32 | method=SpirePatch.CONSTRUCTOR 33 | ) 34 | public static class InitializeCardsPatch { 35 | 36 | public static void Postfix(GremlinMatchGame _instance) { 37 | cards = (CardGroup) ReflectionHacks.getPrivate(_instance, GremlinMatchGame.class, "cards"); 38 | revealedCards = new HashSet<>(); 39 | // If 0 is top left and 11 is bottom right, the positions of the cards in the result array are: 40 | // [0, 5, 10, 3, 4, 9, 2, 7, 8, 1, 6, 11]. We want to store the initial positions for easy reference. 41 | // Cards can be removed from the card group, so it is easier to just calculate them at the start. 42 | cardPositions = new HashMap<>(); 43 | for(int i = 0; i < 12; i++) { 44 | AbstractCard currentCard = cards.group.get(i); 45 | int target_x = i % 4; 46 | int target_y = i % 3; 47 | int position = target_x + 4 * target_y; 48 | cardPositions.put(currentCard.uuid, position); 49 | } 50 | } 51 | } 52 | 53 | @SpirePatch( 54 | clz=GremlinMatchGame.class, 55 | method="updateMatchGameLogic" 56 | ) 57 | public static class HoverCardPatch { 58 | 59 | public static boolean doHover = false; 60 | public static AbstractCard hoverCard = null; 61 | 62 | @SpireInsertPatch( 63 | locator=Locator.class, 64 | localvars = {"c"} 65 | ) 66 | public static void Insert(GremlinMatchGame _instance, AbstractCard c) { 67 | if (doHover) { 68 | if (c.equals(hoverCard)) { 69 | c.hb.hovered = true; 70 | InputHelper.justClickedLeft = true; 71 | doHover = false; 72 | } else { 73 | c.hb.hovered = false; 74 | } 75 | } 76 | } 77 | 78 | private static class Locator extends SpireInsertLocator { 79 | public int[] Locate(CtBehavior ctMethodToPatch) throws CannotCompileException, PatchingException { 80 | Matcher matcher = new Matcher.MethodCallMatcher(Hitbox.class, "update"); 81 | int[] result = LineFinder.findInOrder(ctMethodToPatch, new ArrayList(), matcher); 82 | result[0] += 1; 83 | return result; 84 | } 85 | } 86 | 87 | 88 | } 89 | 90 | @SpirePatch( 91 | clz=GremlinMatchGame.class, 92 | method="updateMatchGameLogic" 93 | ) 94 | public static class WaitForCardFlipPatch { 95 | 96 | @SpireInsertPatch( 97 | locator=Locator.class 98 | ) 99 | public static void Insert(GremlinMatchGame _instance) { 100 | // We have to wait for the cards to flip or everything goes wrong. Nothing else detects this change. 101 | int attemptCount = (int) ReflectionHacks.getPrivate(_instance, GremlinMatchGame.class, "attemptCount"); 102 | if(attemptCount > 0) { 103 | GameStateListener.registerStateChange(); 104 | } 105 | } 106 | 107 | private static class Locator extends SpireInsertLocator { 108 | public int[] Locate(CtBehavior ctMethodToPatch) throws CannotCompileException, PatchingException { 109 | Matcher matcher = new Matcher.FieldAccessMatcher(GremlinMatchGame.class, "attemptCount"); 110 | int[] result = LineFinder.findInOrder(ctMethodToPatch, new ArrayList(), matcher); 111 | result[0] += 1; 112 | return result; 113 | } 114 | } 115 | 116 | } 117 | 118 | @SpirePatch( 119 | clz=GremlinMatchGame.class, 120 | method="updateMatchGameLogic" 121 | ) 122 | public static class CardIdentificationPatch { 123 | 124 | @SpireInsertPatch( 125 | locator=Locator.class, 126 | localvars = {"c"} 127 | ) 128 | public static void Insert(GremlinMatchGame _instance, AbstractCard c) { 129 | revealedCards.add(c.uuid); 130 | } 131 | 132 | private static class Locator extends SpireInsertLocator { 133 | public int[] Locate(CtBehavior ctMethodToPatch) throws CannotCompileException, PatchingException { 134 | Matcher matcher = new Matcher.FieldAccessMatcher(AbstractCard.class, "isFlipped"); 135 | int[] matches = LineFinder.findAllInOrder(ctMethodToPatch, new ArrayList(), matcher); 136 | return Arrays.copyOfRange(matches, 1, 2); 137 | } 138 | } 139 | } 140 | 141 | @SpirePatch( 142 | clz=GremlinMatchGame.class, 143 | method="updateMatchGameLogic" 144 | ) 145 | public static class RegisterFirstFlipPatch{ 146 | 147 | @SpireInsertPatch( 148 | locator=Locator.class 149 | ) 150 | public static void Insert(GremlinMatchGame _instance) { 151 | GameStateListener.registerStateChange(); 152 | } 153 | 154 | /* 155 | This locator tries to find the line this.chosenCard = this.hoveredCard, which indicates the first flip of a match 156 | */ 157 | private static class Locator extends SpireInsertLocator { 158 | public int[] Locate(CtBehavior ctMethodToPatch) throws CannotCompileException, PatchingException { 159 | Matcher chosenMatcher = new Matcher.FieldAccessMatcher(GremlinMatchGame.class, "chosenCard"); 160 | Matcher hoveredMatcher = new Matcher.FieldAccessMatcher(GremlinMatchGame.class, "hoveredCard"); 161 | int[] chosenMatches = LineFinder.findAllInOrder(ctMethodToPatch, new ArrayList(), chosenMatcher); 162 | int[] hoveredMatches = LineFinder.findAllInOrder(ctMethodToPatch, new ArrayList(), hoveredMatcher); 163 | // Not the most computationally efficient way to do this, but it should only be run once anyway. 164 | for (int waitMatch : chosenMatches) { 165 | for (int gameDoneMatch : hoveredMatches) { 166 | if (waitMatch == gameDoneMatch) { 167 | int[] match = new int[1]; 168 | match[0] = waitMatch; 169 | return match; 170 | } 171 | } 172 | } 173 | throw new PatchingException("Could not find patching location for RegisterFirstFlipPatch in GremlinMatchGame."); 174 | } 175 | } 176 | 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /src/main/java/communicationmod/GameStateListener.java: -------------------------------------------------------------------------------- 1 | package communicationmod; 2 | 3 | import com.megacrit.cardcrawl.actions.GameActionManager; 4 | import com.megacrit.cardcrawl.core.CardCrawlGame; 5 | import com.megacrit.cardcrawl.dungeons.AbstractDungeon; 6 | import com.megacrit.cardcrawl.neow.NeowRoom; 7 | import com.megacrit.cardcrawl.rooms.AbstractRoom; 8 | import com.megacrit.cardcrawl.rooms.EventRoom; 9 | import com.megacrit.cardcrawl.rooms.VictoryRoom; 10 | 11 | public class GameStateListener { 12 | private static AbstractDungeon.CurrentScreen previousScreen = null; 13 | private static boolean previousScreenUp = false; 14 | private static AbstractRoom.RoomPhase previousPhase = null; 15 | private static boolean previousGridSelectConfirmUp = false; 16 | private static int previousGold = 99; 17 | private static boolean externalChange = false; 18 | private static boolean myTurn = false; 19 | private static boolean blocked = false; 20 | private static boolean waitingForCommand = false; 21 | private static boolean hasPresentedOutOfGameState = false; 22 | private static boolean waitOneUpdate = false; 23 | private static int timeout = 0; 24 | 25 | /** 26 | * Used to indicate that something (in game logic, not external command) has been done that will change the game state, 27 | * and hasStateChanged() should indicate a state change when the state next becomes stable. 28 | */ 29 | public static void registerStateChange() { 30 | externalChange = true; 31 | waitingForCommand = false; 32 | } 33 | 34 | /** 35 | * Used to tell hasStateChanged() to indicate a state change after a specified number of frames. 36 | * @param newTimeout The number of frames to wait 37 | */ 38 | public static void setTimeout(int newTimeout) { 39 | timeout = newTimeout; 40 | } 41 | 42 | /** 43 | * Used to indicate that an external command has been executed 44 | */ 45 | public static void registerCommandExecution() { 46 | waitingForCommand = false; 47 | } 48 | 49 | /** 50 | * Prevents hasStateChanged() from indicating a state change until resumeStateUpdate() is called. 51 | */ 52 | public static void blockStateUpdate() { 53 | blocked = true; 54 | } 55 | 56 | /** 57 | * Removes the block instantiated by blockStateChanged() 58 | */ 59 | public static void resumeStateUpdate() { 60 | blocked = false; 61 | } 62 | 63 | /** 64 | * Used by a patch in the game to signal the start of your turn. We do not care about state changes 65 | * when it is not our turn in combat, as we cannot take action until then. 66 | */ 67 | public static void signalTurnStart() { 68 | myTurn = true; 69 | } 70 | 71 | /** 72 | * Used by patches in the game to signal the end of your turn (or the end of combat). 73 | */ 74 | public static void signalTurnEnd() { 75 | myTurn = false; 76 | } 77 | 78 | /** 79 | * Resets all state detection variables for the start of a new run. 80 | */ 81 | public static void resetStateVariables() { 82 | previousScreen = null; 83 | previousScreenUp = false; 84 | previousPhase = null; 85 | previousGridSelectConfirmUp = false; 86 | previousGold = 99; 87 | externalChange = false; 88 | myTurn = false; 89 | blocked = false; 90 | waitingForCommand = false; 91 | waitOneUpdate = false; 92 | } 93 | 94 | /** 95 | * Detects whether the game state is stable and we are ready to receive a command from the user. 96 | * 97 | * @return whether the state is stable 98 | */ 99 | private static boolean hasDungeonStateChanged() { 100 | if (blocked) { 101 | return false; 102 | } 103 | hasPresentedOutOfGameState = false; 104 | AbstractDungeon.CurrentScreen newScreen = AbstractDungeon.screen; 105 | boolean newScreenUp = AbstractDungeon.isScreenUp; 106 | AbstractRoom.RoomPhase newPhase = AbstractDungeon.getCurrRoom().phase; 107 | boolean inCombat = (newPhase == AbstractRoom.RoomPhase.COMBAT); 108 | // Lots of stuff can happen while the dungeon is fading out, but nothing that requires input from the user. 109 | if (AbstractDungeon.isFadingOut || AbstractDungeon.isFadingIn) { 110 | return false; 111 | } 112 | // This check happens before the rest since dying can happen in combat and messes with the other cases. 113 | if (newScreen == AbstractDungeon.CurrentScreen.DEATH && newScreen != previousScreen) { 114 | return true; 115 | } 116 | // These screens have no interaction available. 117 | if (newScreen == AbstractDungeon.CurrentScreen.DOOR_UNLOCK || newScreen == AbstractDungeon.CurrentScreen.NO_INTERACT) { 118 | return false; 119 | } 120 | // We are not ready to receive commands when it is not our turn, except for some pesky screens 121 | if (inCombat && (!myTurn || AbstractDungeon.getMonsters().areMonstersBasicallyDead())) { 122 | if (!newScreenUp) { 123 | return false; 124 | } 125 | } 126 | // In event rooms, we need to wait for the event wait timer to reach 0 before we can accurately assess its state. 127 | AbstractRoom currentRoom = AbstractDungeon.getCurrRoom(); 128 | if ((currentRoom instanceof EventRoom 129 | || currentRoom instanceof NeowRoom 130 | || (currentRoom instanceof VictoryRoom && ((VictoryRoom) currentRoom).eType == VictoryRoom.EventType.HEART)) 131 | && AbstractDungeon.getCurrRoom().event.waitTimer != 0.0F) { 132 | return false; 133 | } 134 | // The state has always changed in some way when one of these variables is different. 135 | // However, the state may not be finished changing, so we need to do some additional checks. 136 | if (newScreen != previousScreen || newScreenUp != previousScreenUp || newPhase != previousPhase) { 137 | if (inCombat) { 138 | // In combat, newScreenUp being true indicates an action that requires our immediate attention. 139 | if (newScreenUp) { 140 | return true; 141 | } 142 | // In combat, if no screen is up, we should wait for all actions to complete before indicating a state change. 143 | else if (AbstractDungeon.actionManager.phase.equals(GameActionManager.Phase.WAITING_ON_USER) 144 | && AbstractDungeon.actionManager.cardQueue.isEmpty() 145 | && AbstractDungeon.actionManager.actions.isEmpty()) { 146 | return true; 147 | } 148 | 149 | // Out of combat, we want to wait one update cycle, as some screen transitions trigger further updates. 150 | } else { 151 | waitOneUpdate = true; 152 | previousScreenUp = newScreenUp; 153 | previousScreen = newScreen; 154 | previousPhase = newPhase; 155 | return false; 156 | } 157 | } else if (waitOneUpdate) { 158 | waitOneUpdate = false; 159 | return true; 160 | } 161 | // We are assuming that commands are only being submitted through our interface. Some actions that require 162 | // our attention, like retaining a card, occur after the end turn is queued, but the previous cases 163 | // cover those actions. We would like to avoid registering other state changes after the end turn 164 | // command but before the game actually ends your turn. 165 | if (inCombat && AbstractDungeon.player.endTurnQueued) { 166 | return false; 167 | } 168 | // If some other code registered a state change through registerStateChange(), or if we notice a state 169 | // change through the gold amount changing, we still need to wait until all actions are finished 170 | // resolving to claim a stable state and ask for a new command. 171 | if ((externalChange || previousGold != AbstractDungeon.player.gold) 172 | && AbstractDungeon.actionManager.phase.equals(GameActionManager.Phase.WAITING_ON_USER) 173 | && AbstractDungeon.actionManager.preTurnActions.isEmpty() 174 | && AbstractDungeon.actionManager.actions.isEmpty() 175 | && AbstractDungeon.actionManager.cardQueue.isEmpty()) { 176 | return true; 177 | } 178 | // In a grid select screen, if a confirm screen comes up or goes away, it doesn't change any other state. 179 | if (newScreen == AbstractDungeon.CurrentScreen.GRID) { 180 | boolean newGridSelectConfirmUp = AbstractDungeon.gridSelectScreen.confirmScreenUp; 181 | if (previousScreen == AbstractDungeon.CurrentScreen.GRID && newGridSelectConfirmUp != previousGridSelectConfirmUp) { 182 | return true; 183 | } 184 | } 185 | // Sometimes, we need to register an external change in combat while an action is resolving which brings 186 | // the screen up. Because the screen did not change, this is not covered by other cases. 187 | if (externalChange && inCombat && newScreenUp) { 188 | return true; 189 | } 190 | if (timeout > 0) { 191 | timeout -= 1; 192 | if(timeout == 0) { 193 | return true; 194 | } 195 | } 196 | return false; 197 | } 198 | 199 | /** 200 | * Detects whether the state of the game menu has changed. Right now, this only occurs when you first enter the 201 | * menu, either after starting Slay the Spire for the first time, or after ending a game and returning to the menu. 202 | * 203 | * @return Whether the main menu has just been entered. 204 | */ 205 | public static boolean checkForMenuStateChange() { 206 | boolean stateChange = false; 207 | if (!hasPresentedOutOfGameState && CardCrawlGame.mode == CardCrawlGame.GameMode.CHAR_SELECT && CardCrawlGame.mainMenuScreen != null) { 208 | stateChange = true; 209 | hasPresentedOutOfGameState = true; 210 | } 211 | if (stateChange) { 212 | externalChange = false; 213 | waitingForCommand = true; 214 | } 215 | return stateChange; 216 | } 217 | 218 | /** 219 | * Detects a state change in AbstractDungeon, and updates all of the local variables used to detect 220 | * changes in the dungeon state. Sets waitingForCommand = true if a state change was registered since 221 | * the last command was sent. 222 | * 223 | * @return Whether a dungeon state change was detected 224 | */ 225 | public static boolean checkForDungeonStateChange() { 226 | boolean stateChange = false; 227 | if (CommandExecutor.isInDungeon()) { 228 | stateChange = hasDungeonStateChanged(); 229 | if (stateChange) { 230 | externalChange = false; 231 | waitingForCommand = true; 232 | previousPhase = AbstractDungeon.getCurrRoom().phase; 233 | previousScreen = AbstractDungeon.screen; 234 | previousScreenUp = AbstractDungeon.isScreenUp; 235 | previousGold = AbstractDungeon.player.gold; 236 | previousGridSelectConfirmUp = AbstractDungeon.gridSelectScreen.confirmScreenUp; 237 | timeout = 0; 238 | } 239 | } else { 240 | myTurn = false; 241 | } 242 | return stateChange; 243 | } 244 | 245 | public static boolean isWaitingForCommand() { 246 | return waitingForCommand; 247 | } 248 | } -------------------------------------------------------------------------------- /src/main/java/communicationmod/CommunicationMod.java: -------------------------------------------------------------------------------- 1 | package communicationmod; 2 | 3 | import basemod.*; 4 | import basemod.interfaces.PostDungeonUpdateSubscriber; 5 | import basemod.interfaces.PostInitializeSubscriber; 6 | import basemod.interfaces.PostUpdateSubscriber; 7 | import basemod.interfaces.PreUpdateSubscriber; 8 | import com.evacipated.cardcrawl.modthespire.lib.SpireConfig; 9 | import com.evacipated.cardcrawl.modthespire.lib.SpireInitializer; 10 | import com.google.gson.Gson; 11 | import com.megacrit.cardcrawl.core.Settings; 12 | import com.megacrit.cardcrawl.dungeons.AbstractDungeon; 13 | import com.megacrit.cardcrawl.helpers.FontHelper; 14 | import com.megacrit.cardcrawl.helpers.ImageMaster; 15 | import communicationmod.patches.InputActionPatch; 16 | import org.apache.logging.log4j.LogManager; 17 | import org.apache.logging.log4j.Logger; 18 | 19 | import java.io.File; 20 | import java.io.IOException; 21 | import java.lang.ProcessBuilder; 22 | import java.util.ArrayList; 23 | import java.util.HashMap; 24 | import java.util.Properties; 25 | import java.util.concurrent.BlockingQueue; 26 | import java.util.concurrent.LinkedBlockingQueue; 27 | import java.util.concurrent.TimeUnit; 28 | 29 | @SpireInitializer 30 | public class CommunicationMod implements PostInitializeSubscriber, PostUpdateSubscriber, PostDungeonUpdateSubscriber, PreUpdateSubscriber, OnStateChangeSubscriber { 31 | 32 | private static Process listener; 33 | private static StringBuilder inputBuffer = new StringBuilder(); 34 | public static boolean messageReceived = false; 35 | private static final Logger logger = LogManager.getLogger(CommunicationMod.class.getName()); 36 | private static Thread writeThread; 37 | private static BlockingQueue writeQueue; 38 | private static Thread readThread; 39 | private static BlockingQueue readQueue; 40 | private static final String MODNAME = "Communication Mod"; 41 | private static final String AUTHOR = "Forgotten Arbiter"; 42 | private static final String DESCRIPTION = "This mod communicates with an external program to play Slay the Spire."; 43 | public static boolean mustSendGameState = false; 44 | private static ArrayList onStateChangeSubscribers; 45 | 46 | private static SpireConfig communicationConfig; 47 | private static final String COMMAND_OPTION = "command"; 48 | private static final String GAME_START_OPTION = "runAtGameStart"; 49 | private static final String VERBOSE_OPTION = "verbose"; 50 | private static final String INITIALIZATION_TIMEOUT_OPTION = "maxInitializationTimeout"; 51 | private static final String DEFAULT_COMMAND = ""; 52 | private static final long DEFAULT_TIMEOUT = 10L; 53 | private static final boolean DEFAULT_VERBOSITY = true; 54 | 55 | public CommunicationMod(){ 56 | BaseMod.subscribe(this); 57 | onStateChangeSubscribers = new ArrayList<>(); 58 | CommunicationMod.subscribe(this); 59 | readQueue = new LinkedBlockingQueue<>(); 60 | try { 61 | Properties defaults = new Properties(); 62 | defaults.put(GAME_START_OPTION, Boolean.toString(false)); 63 | defaults.put(INITIALIZATION_TIMEOUT_OPTION, Long.toString(DEFAULT_TIMEOUT)); 64 | defaults.put(VERBOSE_OPTION, Boolean.toString(DEFAULT_VERBOSITY)); 65 | communicationConfig = new SpireConfig("CommunicationMod", "config", defaults); 66 | String command = communicationConfig.getString(COMMAND_OPTION); 67 | // I want this to always be saved to the file so people can set it more easily. 68 | if (command == null) { 69 | communicationConfig.setString(COMMAND_OPTION, DEFAULT_COMMAND); 70 | communicationConfig.save(); 71 | } 72 | communicationConfig.save(); 73 | } catch (IOException e) { 74 | e.printStackTrace(); 75 | } 76 | 77 | if(getRunOnGameStartOption()) { 78 | boolean success = startExternalProcess(); 79 | } 80 | } 81 | 82 | public static void initialize() { 83 | CommunicationMod mod = new CommunicationMod(); 84 | } 85 | 86 | public void receivePreUpdate() { 87 | if(listener != null && !listener.isAlive() && writeThread != null && writeThread.isAlive()) { 88 | logger.info("Child process has died..."); 89 | writeThread.interrupt(); 90 | readThread.interrupt(); 91 | } 92 | if(messageAvailable()) { 93 | try { 94 | boolean stateChanged = CommandExecutor.executeCommand(readMessage()); 95 | if(stateChanged) { 96 | GameStateListener.registerCommandExecution(); 97 | } 98 | } catch (InvalidCommandException e) { 99 | HashMap jsonError = new HashMap<>(); 100 | jsonError.put("error", e.getMessage()); 101 | jsonError.put("ready_for_command", GameStateListener.isWaitingForCommand()); 102 | Gson gson = new Gson(); 103 | sendMessage(gson.toJson(jsonError)); 104 | } 105 | } 106 | } 107 | 108 | public static void subscribe(OnStateChangeSubscriber sub) { 109 | onStateChangeSubscribers.add(sub); 110 | } 111 | 112 | public static void publishOnGameStateChange() { 113 | for(OnStateChangeSubscriber sub : onStateChangeSubscribers) { 114 | sub.receiveOnStateChange(); 115 | } 116 | } 117 | 118 | public void receiveOnStateChange() { 119 | sendGameState(); 120 | } 121 | 122 | public static void queueCommand(String command) { 123 | readQueue.add(command); 124 | } 125 | 126 | public void receivePostInitialize() { 127 | setUpOptionsMenu(); 128 | } 129 | 130 | public void receivePostUpdate() { 131 | if(!mustSendGameState && GameStateListener.checkForMenuStateChange()) { 132 | mustSendGameState = true; 133 | } 134 | if(mustSendGameState) { 135 | publishOnGameStateChange(); 136 | mustSendGameState = false; 137 | } 138 | InputActionPatch.doKeypress = false; 139 | } 140 | 141 | public void receivePostDungeonUpdate() { 142 | if (GameStateListener.checkForDungeonStateChange()) { 143 | mustSendGameState = true; 144 | } 145 | if(AbstractDungeon.getCurrRoom().isBattleOver) { 146 | GameStateListener.signalTurnEnd(); 147 | } 148 | } 149 | 150 | private void setUpOptionsMenu() { 151 | ModPanel settingsPanel = new ModPanel(); 152 | ModLabeledToggleButton gameStartOptionButton = new ModLabeledToggleButton( 153 | "Start external process at game launch", 154 | 350, 550, Settings.CREAM_COLOR, FontHelper.charDescFont, 155 | getRunOnGameStartOption(), settingsPanel, modLabel -> {}, 156 | modToggleButton -> { 157 | if (communicationConfig != null) { 158 | communicationConfig.setBool(GAME_START_OPTION, modToggleButton.enabled); 159 | try { 160 | communicationConfig.save(); 161 | } catch (IOException e) { 162 | e.printStackTrace(); 163 | } 164 | } 165 | }); 166 | settingsPanel.addUIElement(gameStartOptionButton); 167 | 168 | ModLabel externalCommandLabel = new ModLabel( 169 | "", 350, 600, Settings.CREAM_COLOR, FontHelper.charDescFont, 170 | settingsPanel, modLabel -> { 171 | modLabel.text = String.format("External Process Command: %s", getSubprocessCommandString()); 172 | }); 173 | settingsPanel.addUIElement(externalCommandLabel); 174 | 175 | ModButton startProcessButton = new ModButton( 176 | 350, 650, settingsPanel, modButton -> { 177 | BaseMod.modSettingsUp = false; 178 | startExternalProcess(); 179 | }); 180 | settingsPanel.addUIElement(startProcessButton); 181 | 182 | ModLabel startProcessLabel = new ModLabel( 183 | "(Re)start external process", 184 | 475, 700, Settings.CREAM_COLOR, FontHelper.charDescFont, 185 | settingsPanel, modLabel -> { 186 | if(listener != null && listener.isAlive()) { 187 | modLabel.text = "Restart external process"; 188 | } else { 189 | modLabel.text = "Start external process"; 190 | } 191 | }); 192 | settingsPanel.addUIElement(startProcessLabel); 193 | 194 | ModButton editProcessButton = new ModButton( 195 | 850, 650, settingsPanel, modButton -> {}); 196 | settingsPanel.addUIElement(editProcessButton); 197 | 198 | ModLabel editProcessLabel = new ModLabel( 199 | "Set command (not implemented)", 200 | 975, 700, Settings.CREAM_COLOR, FontHelper.charDescFont, 201 | settingsPanel, modLabel -> {}); 202 | settingsPanel.addUIElement(editProcessLabel); 203 | 204 | ModLabeledToggleButton verbosityOption = new ModLabeledToggleButton( 205 | "Suppress verbose log output", 206 | 350, 500, Settings.CREAM_COLOR, FontHelper.charDescFont, 207 | getVerbosityOption(), settingsPanel, modLabel -> {}, 208 | modToggleButton -> { 209 | if (communicationConfig != null) { 210 | communicationConfig.setBool(VERBOSE_OPTION, modToggleButton.enabled); 211 | try { 212 | communicationConfig.save(); 213 | } catch (IOException e) { 214 | e.printStackTrace(); 215 | } 216 | } 217 | }); 218 | settingsPanel.addUIElement(verbosityOption); 219 | BaseMod.registerModBadge(ImageMaster.loadImage("Icon.png"),"Communication Mod", "Forgotten Arbiter", null, settingsPanel); 220 | } 221 | 222 | private void startCommunicationThreads() { 223 | writeQueue = new LinkedBlockingQueue<>(); 224 | writeThread = new Thread(new DataWriter(writeQueue, listener.getOutputStream(), getVerbosityOption())); 225 | writeThread.start(); 226 | readThread = new Thread(new DataReader(readQueue, listener.getInputStream(), getVerbosityOption())); 227 | readThread.start(); 228 | } 229 | 230 | private static void sendGameState() { 231 | String state = GameStateConverter.getCommunicationState(); 232 | sendMessage(state); 233 | } 234 | 235 | public static void dispose() { 236 | logger.info("Shutting down child process..."); 237 | if(listener != null) { 238 | listener.destroy(); 239 | } 240 | } 241 | 242 | private static void sendMessage(String message) { 243 | if(writeQueue != null && writeThread.isAlive()) { 244 | writeQueue.add(message); 245 | } 246 | } 247 | 248 | private static boolean messageAvailable() { 249 | return readQueue != null && !readQueue.isEmpty(); 250 | } 251 | 252 | private static String readMessage() { 253 | if(messageAvailable()) { 254 | return readQueue.remove(); 255 | } else { 256 | return null; 257 | } 258 | } 259 | 260 | private static String readMessageBlocking() { 261 | try { 262 | return readQueue.poll(getInitializationTimeoutOption(), TimeUnit.SECONDS); 263 | } catch (InterruptedException e) { 264 | throw new RuntimeException("Interrupted while trying to read message from subprocess."); 265 | } 266 | } 267 | 268 | private static String[] getSubprocessCommand() { 269 | if (communicationConfig == null) { 270 | return new String[0]; 271 | } 272 | return communicationConfig.getString(COMMAND_OPTION).trim().split("\\s+"); 273 | } 274 | 275 | private static String getSubprocessCommandString() { 276 | if (communicationConfig == null) { 277 | return ""; 278 | } 279 | return communicationConfig.getString(COMMAND_OPTION).trim(); 280 | } 281 | 282 | private static boolean getRunOnGameStartOption() { 283 | if (communicationConfig == null) { 284 | return false; 285 | } 286 | return communicationConfig.getBool(GAME_START_OPTION); 287 | } 288 | 289 | private static long getInitializationTimeoutOption() { 290 | if (communicationConfig == null) { 291 | return DEFAULT_TIMEOUT; 292 | } 293 | return (long)communicationConfig.getInt(INITIALIZATION_TIMEOUT_OPTION); 294 | } 295 | 296 | private static boolean getVerbosityOption() { 297 | if (communicationConfig == null) { 298 | return DEFAULT_VERBOSITY; 299 | } 300 | return communicationConfig.getBool(VERBOSE_OPTION); 301 | } 302 | 303 | private boolean startExternalProcess() { 304 | if(readThread != null) { 305 | readThread.interrupt(); 306 | } 307 | if(writeThread != null) { 308 | writeThread.interrupt(); 309 | } 310 | if(listener != null) { 311 | listener.destroy(); 312 | try { 313 | boolean success = listener.waitFor(2, TimeUnit.SECONDS); 314 | if (!success) { 315 | listener.destroyForcibly(); 316 | } 317 | } catch (InterruptedException e) { 318 | e.printStackTrace(); 319 | listener.destroyForcibly(); 320 | } 321 | } 322 | ProcessBuilder builder = new ProcessBuilder(getSubprocessCommand()); 323 | File errorLog = new File("communication_mod_errors.log"); 324 | builder.redirectError(ProcessBuilder.Redirect.appendTo(errorLog)); 325 | try { 326 | listener = builder.start(); 327 | } catch (IOException e) { 328 | logger.error("Could not start external process."); 329 | e.printStackTrace(); 330 | } 331 | if(listener != null) { 332 | startCommunicationThreads(); 333 | // We wait for the child process to signal it is ready before we proceed. Note that the game 334 | // will hang while this is occurring, and it will time out after a specified waiting time. 335 | String message = readMessageBlocking(); 336 | if(message == null) { 337 | // The child process waited too long to respond, so we kill it. 338 | readThread.interrupt(); 339 | writeThread.interrupt(); 340 | listener.destroy(); 341 | logger.error("Timed out while waiting for signal from external process."); 342 | logger.error("Check communication_mod_errors.log for stderr from the process."); 343 | return false; 344 | } else { 345 | logger.info(String.format("Received message from external process: %s", message)); 346 | if (GameStateListener.isWaitingForCommand()) { 347 | mustSendGameState = true; 348 | } 349 | return true; 350 | } 351 | } 352 | return false; 353 | } 354 | 355 | } 356 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CommunicationMod 2 | Slay the Spire mod that provides a protocol for allowing another process to control the game 3 | 4 | ## Requirements 5 | 6 | - Slay the Spire 7 | - ModTheSpire (https://github.com/kiooeht/ModTheSpire) 8 | - BaseMod (https://github.com/daviscook477/BaseMod) 9 | 10 | ## Setup 11 | 12 | 1. Copy CommunicationMod.jar to your ModTheSpire mods directory 13 | 2. Run ModTheSpire with CommunicationMod enabled 14 | 3. Edit your newly-created SpireConfig file with the command you want to use with CommunicationMod (see https://github.com/kiooeht/ModTheSpire/wiki/SpireConfig for the location of your config file). Your config file should look something like this (note that certain special characters must be escaped): 15 | ``` 16 | #Sat Apr 20 02:49:10 CDT 2019 17 | command=python C\:\\Path\\To\\Script\\main.py 18 | ``` 19 | 20 | ## What does this mod do? 21 | 22 | CommunicationMod launches a specified process and communicates with this process through stdin and stdout, with the following protocol: 23 | 24 | (Note: all messages are assumed to be ended by a new line '\n') 25 | 26 | - After starting the external process, CommunicationMod waits for the process to send "ready" on stdout. If "ready" is not received before a specified timeout, the external process will be terminated. 27 | - Whenever the state of the game is determined to be stable (no longer changing without external input), CommunicationMod sends a message containing the JSON representation of the current game state to the external process's stdin. For example: 28 | ``` 29 | {"available_commands":["play","end","key","click","wait","state"],"ready_for_command":true,"in_game":true,"game_state":{"screen_type":"NONE","screen_state":{},"seed":-3047511808784702860,"combat_state":{"draw_pile":[{"exhausts":false,"is_playable":true,"cost":1,"name":"Strike","id":"Strike_R","type":"ATTACK","ethereal":false,"uuid":"0560233c-41e8-4620-a474-d0ed627354bd","upgrades":0,"rarity":"BASIC","has_target":true},{"exhausts":false,"is_playable":true,"cost":1,"name":"Defend","id":"Defend_R","type":"SKILL","ethereal":false,"uuid":"f8adc2a6-4d1c-4524-9044-9e2bfacf4256","upgrades":0,"rarity":"BASIC","has_target":false},{"exhausts":false,"is_playable":true,"cost":1,"name":"Strike","id":"Strike_R","type":"ATTACK","ethereal":false,"uuid":"c6594538-debc-4085-81be-3b20a5d44062","upgrades":0,"rarity":"BASIC","has_target":true},{"exhausts":false,"is_playable":false,"cost":-2,"name":"Ascender\u0027s Bane","id":"AscendersBane","type":"CURSE","ethereal":true,"uuid":"da41cd4b-6eda-4020-a031-ad870a52b0e1","upgrades":0,"rarity":"SPECIAL","has_target":false},{"exhausts":false,"is_playable":true,"cost":1,"name":"Strike","id":"Strike_R","type":"ATTACK","ethereal":false,"uuid":"b54d5d98-f074-4f71-b705-f071f1d44fff","upgrades":0,"rarity":"BASIC","has_target":true},{"exhausts":false,"is_playable":true,"cost":1,"name":"Defend","id":"Defend_R","type":"SKILL","ethereal":false,"uuid":"9c10951d-0c08-46bd-bc5f-be2e1b9d53f2","upgrades":0,"rarity":"BASIC","has_target":false}],"discard_pile":[],"exhaust_pile":[],"cards_discarded_this_turn":0,"times_damaged":0,"monsters":[{"is_gone":false,"move_hits":1,"move_base_damage":12,"half_dead":false,"move_adjusted_damage":-1,"max_hp":46,"intent":"DEBUG","move_id":1,"name":"Jaw Worm","current_hp":1,"block":0,"id":"JawWorm","powers":[]}],"turn":1,"limbo":[],"hand":[{"exhausts":false,"is_playable":true,"cost":1,"name":"Strike","id":"Strike_R","type":"ATTACK","ethereal":false,"uuid":"7b54caef-9c56-4134-82a2-be8f1d5c435f","upgrades":0,"rarity":"BASIC","has_target":true},{"exhausts":false,"is_playable":true,"cost":1,"name":"Strike","id":"Strike_R","type":"ATTACK","ethereal":false,"uuid":"5f9bba1a-4c54-4be7-b387-1992937c5717","upgrades":0,"rarity":"BASIC","has_target":true},{"exhausts":false,"is_playable":true,"cost":1,"name":"Defend","id":"Defend_R","type":"SKILL","ethereal":false,"uuid":"0dbea551-c9ae-4228-8821-74e2ffd04889","upgrades":0,"rarity":"BASIC","has_target":false},{"exhausts":false,"is_playable":true,"cost":1,"name":"Defend","id":"Defend_R","type":"SKILL","ethereal":false,"uuid":"612c16f7-f8f7-4253-88bb-ab4813d34b69","upgrades":0,"rarity":"BASIC","has_target":false},{"exhausts":false,"is_playable":true,"cost":2,"name":"Bash","id":"Bash","type":"ATTACK","ethereal":false,"uuid":"41e3754b-d2e3-40b4-a83b-f165b1943ec3","upgrades":0,"rarity":"BASIC","has_target":true}],"player":{"orbs":[],"current_hp":68,"block":0,"max_hp":75,"powers":[],"energy":3}},"deck":[{"exhausts":false,"is_playable":false,"cost":-2,"name":"Ascender\u0027s Bane","id":"AscendersBane","type":"CURSE","ethereal":true,"uuid":"da41cd4b-6eda-4020-a031-ad870a52b0e1","upgrades":0,"rarity":"SPECIAL","has_target":false},{"exhausts":false,"is_playable":true,"cost":1,"name":"Strike","id":"Strike_R","type":"ATTACK","ethereal":false,"uuid":"7b54caef-9c56-4134-82a2-be8f1d5c435f","upgrades":0,"rarity":"BASIC","has_target":true},{"exhausts":false,"is_playable":true,"cost":1,"name":"Strike","id":"Strike_R","type":"ATTACK","ethereal":false,"uuid":"c6594538-debc-4085-81be-3b20a5d44062","upgrades":0,"rarity":"BASIC","has_target":true},{"exhausts":false,"is_playable":true,"cost":1,"name":"Strike","id":"Strike_R","type":"ATTACK","ethereal":false,"uuid":"5f9bba1a-4c54-4be7-b387-1992937c5717","upgrades":0,"rarity":"BASIC","has_target":true},{"exhausts":false,"is_playable":true,"cost":1,"name":"Strike","id":"Strike_R","type":"ATTACK","ethereal":false,"uuid":"0560233c-41e8-4620-a474-d0ed627354bd","upgrades":0,"rarity":"BASIC","has_target":true},{"exhausts":false,"is_playable":true,"cost":1,"name":"Strike","id":"Strike_R","type":"ATTACK","ethereal":false,"uuid":"b54d5d98-f074-4f71-b705-f071f1d44fff","upgrades":0,"rarity":"BASIC","has_target":true},{"exhausts":false,"is_playable":true,"cost":1,"name":"Defend","id":"Defend_R","type":"SKILL","ethereal":false,"uuid":"9c10951d-0c08-46bd-bc5f-be2e1b9d53f2","upgrades":0,"rarity":"BASIC","has_target":false},{"exhausts":false,"is_playable":true,"cost":1,"name":"Defend","id":"Defend_R","type":"SKILL","ethereal":false,"uuid":"0dbea551-c9ae-4228-8821-74e2ffd04889","upgrades":0,"rarity":"BASIC","has_target":false},{"exhausts":false,"is_playable":true,"cost":1,"name":"Defend","id":"Defend_R","type":"SKILL","ethereal":false,"uuid":"612c16f7-f8f7-4253-88bb-ab4813d34b69","upgrades":0,"rarity":"BASIC","has_target":false},{"exhausts":false,"is_playable":true,"cost":1,"name":"Defend","id":"Defend_R","type":"SKILL","ethereal":false,"uuid":"f8adc2a6-4d1c-4524-9044-9e2bfacf4256","upgrades":0,"rarity":"BASIC","has_target":false},{"exhausts":false,"is_playable":true,"cost":2,"name":"Bash","id":"Bash","type":"ATTACK","ethereal":false,"uuid":"41e3754b-d2e3-40b4-a83b-f165b1943ec3","upgrades":0,"rarity":"BASIC","has_target":true}],"relics":[{"name":"Burning Blood","id":"Burning Blood","counter":-1},{"name":"Neow\u0027s Lament","id":"NeowsBlessing","counter":2}],"max_hp":75,"act_boss":"The Guardian","gold":99,"action_phase":"WAITING_ON_USER","act":1,"screen_name":"NONE","room_phase":"COMBAT","is_screen_up":false,"potions":[{"requires_target":false,"can_use":false,"can_discard":false,"name":"Potion Slot","id":"Potion Slot"},{"requires_target":false,"can_use":false,"can_discard":false,"name":"Potion Slot","id":"Potion Slot"}],"current_hp":68,"floor":1,"ascension_level":20,"class":"IRONCLAD","map":[{"symbol":"M","children":[{"x":0,"y":1}],"x":1,"y":0,"parents":[]},{"symbol":"M","children":[{"x":2,"y":1}],"x":2,"y":0,"parents":[]},{"symbol":"M","children":[{"x":4,"y":1}],"x":3,"y":0,"parents":[]},{"symbol":"M","children":[{"x":5,"y":1}],"x":6,"y":0,"parents":[]},{"symbol":"M","children":[{"x":1,"y":2}],"x":0,"y":1,"parents":[]},{"symbol":"M","children":[{"x":1,"y":2},{"x":2,"y":2}],"x":2,"y":1,"parents":[]},{"symbol":"?","children":[{"x":3,"y":2}],"x":4,"y":1,"parents":[]},{"symbol":"$","children":[{"x":4,"y":2}],"x":5,"y":1,"parents":[]},{"symbol":"M","children":[{"x":1,"y":3},{"x":2,"y":3}],"x":1,"y":2,"parents":[]},{"symbol":"?","children":[{"x":2,"y":3},{"x":3,"y":3}],"x":2,"y":2,"parents":[]},{"symbol":"M","children":[{"x":3,"y":3}],"x":3,"y":2,"parents":[]},{"symbol":"M","children":[{"x":5,"y":3}],"x":4,"y":2,"parents":[]},{"symbol":"?","children":[{"x":1,"y":4}],"x":1,"y":3,"parents":[]},{"symbol":"M","children":[{"x":3,"y":4}],"x":2,"y":3,"parents":[]},{"symbol":"?","children":[{"x":3,"y":4}],"x":3,"y":3,"parents":[]},{"symbol":"M","children":[{"x":4,"y":4}],"x":5,"y":3,"parents":[]},{"symbol":"M","children":[{"x":1,"y":5}],"x":1,"y":4,"parents":[]},{"symbol":"?","children":[{"x":2,"y":5},{"x":3,"y":5}],"x":3,"y":4,"parents":[]},{"symbol":"M","children":[{"x":3,"y":5}],"x":4,"y":4,"parents":[]},{"symbol":"E","children":[{"x":1,"y":6}],"x":1,"y":5,"parents":[]},{"symbol":"E","children":[{"x":1,"y":6},{"x":2,"y":6}],"x":2,"y":5,"parents":[]},{"symbol":"R","children":[{"x":2,"y":6},{"x":3,"y":6}],"x":3,"y":5,"parents":[]},{"symbol":"R","children":[{"x":2,"y":7}],"x":1,"y":6,"parents":[]},{"symbol":"M","children":[{"x":2,"y":7},{"x":3,"y":7}],"x":2,"y":6,"parents":[]},{"symbol":"E","children":[{"x":3,"y":7}],"x":3,"y":6,"parents":[]},{"symbol":"E","children":[{"x":1,"y":8},{"x":2,"y":8},{"x":3,"y":8}],"x":2,"y":7,"parents":[]},{"symbol":"R","children":[{"x":3,"y":8}],"x":3,"y":7,"parents":[]},{"symbol":"T","children":[{"x":0,"y":9}],"x":1,"y":8,"parents":[]},{"symbol":"T","children":[{"x":1,"y":9}],"x":2,"y":8,"parents":[]},{"symbol":"T","children":[{"x":2,"y":9},{"x":3,"y":9},{"x":4,"y":9}],"x":3,"y":8,"parents":[]},{"symbol":"R","children":[{"x":1,"y":10}],"x":0,"y":9,"parents":[]},{"symbol":"M","children":[{"x":1,"y":10}],"x":1,"y":9,"parents":[]},{"symbol":"M","children":[{"x":1,"y":10},{"x":3,"y":10}],"x":2,"y":9,"parents":[]},{"symbol":"R","children":[{"x":4,"y":10}],"x":3,"y":9,"parents":[]},{"symbol":"?","children":[{"x":4,"y":10}],"x":4,"y":9,"parents":[]},{"symbol":"?","children":[{"x":0,"y":11},{"x":1,"y":11}],"x":1,"y":10,"parents":[]},{"symbol":"R","children":[{"x":3,"y":11}],"x":3,"y":10,"parents":[]},{"symbol":"E","children":[{"x":3,"y":11},{"x":4,"y":11}],"x":4,"y":10,"parents":[]},{"symbol":"$","children":[{"x":0,"y":12}],"x":0,"y":11,"parents":[]},{"symbol":"M","children":[{"x":1,"y":12}],"x":1,"y":11,"parents":[]},{"symbol":"M","children":[{"x":3,"y":12},{"x":4,"y":12}],"x":3,"y":11,"parents":[]},{"symbol":"?","children":[{"x":4,"y":12}],"x":4,"y":11,"parents":[]},{"symbol":"E","children":[{"x":0,"y":13}],"x":0,"y":12,"parents":[]},{"symbol":"M","children":[{"x":1,"y":13}],"x":1,"y":12,"parents":[]},{"symbol":"?","children":[{"x":3,"y":13}],"x":3,"y":12,"parents":[]},{"symbol":"M","children":[{"x":3,"y":13}],"x":4,"y":12,"parents":[]},{"symbol":"?","children":[{"x":1,"y":14}],"x":0,"y":13,"parents":[]},{"symbol":"M","children":[{"x":1,"y":14}],"x":1,"y":13,"parents":[]},{"symbol":"?","children":[{"x":2,"y":14},{"x":3,"y":14}],"x":3,"y":13,"parents":[]},{"symbol":"R","children":[{"x":3,"y":16}],"x":1,"y":14,"parents":[]},{"symbol":"R","children":[{"x":3,"y":16}],"x":2,"y":14,"parents":[]},{"symbol":"R","children":[{"x":3,"y":16}],"x":3,"y":14,"parents":[]}],"room_type":"MonsterRoom"}} 30 | ``` 31 | - CommunicationMod then waits for a message back from the external process, containing a command to be executed. Possible commands are: 32 | - START PlayerClass [AscensionLevel] [Seed] 33 | - Starts a new game with the selected class, on the selected Ascension level (default 0), with the selected seed (random seed if omitted). 34 | - Seeds are alphanumeric, as displayed in game. 35 | - This and all commands are case insensitive. 36 | - Only currently available in the main menu of the game. 37 | - POTION Use|Discard PotionSlot [TargetIndex] 38 | - Uses or discards the potion in the selected slot, on the selected target, if necessary. 39 | - TargetIndex is the index of the target monster in the game's monster array (0-indexed). 40 | - Only available when potions can be used or discarded. 41 | - PLAY CardIndex [TargetIndex] 42 | - Plays the selected card in your hand, with the selected target, if necessary. 43 | - Only available when cards can be played in combat. 44 | - Currently, CardIndex is 1-indexed to match up with the card numbers in game. 45 | - END 46 | - Ends your turn. 47 | - Only available when the end turn button is available, in combat. 48 | - CHOOSE ChoiceIndex|ChoiceName 49 | - Makes a choice relevant to the current screen. 50 | - A list of names for each choice is provided in the game state. If provided with a name, the first choice index with the matching name is selected. 51 | - Generally, available at any point when PLAY is not available. 52 | - PROCEED 53 | - Clicks the button on the right side of the screen, generally causing the game to proceed to a new screen. 54 | - Equivalent to CONFIRM. 55 | - Available whenever the proceed or confirm button is present on the right side of the screen. 56 | - RETURN 57 | - Clicks the button on the left side of the screen, generally causing you to return to the previous screen. 58 | - Equivalent to SKIP, CANCEL, and LEAVE. 59 | - Available whenever the return, cancel, or leave buttons are present on the left side of the screen. Also used for the skip button on card reward screens. 60 | - KEY Keyname [Timeout] 61 | - Presses the key corresponding to Keyname 62 | - Possible keynames are: Confirm, Cancel, Map, Deck, Draw_Pile, Discard_Pile, Exhaust_Pile, End_Turn, Up, Down, Left, Right, Drop_Card, Card_1, Card_2, ..., Card_10 63 | - The actual keys pressed depend on the corresponding mapping in the game options 64 | - If no state change is detected after [Timeout] frames (default 100), Communication Mod will then transmit the new state and accept input from the game. This is useful for keypresses that open menus or pick up cards, without affecting the state as detected by Communication Mod. 65 | - Only available in a run (not the main menus) 66 | - CLICK Left|Right X Y 67 | - Clicks the selected mouse button at the specified (X,Y) coordinates 68 | - (0,0) is the upper left corner of the screen, and (1920,1080) is the lower right corner, regardless of game resolution 69 | - Will move your cursor to the specified coordindates 70 | - Timeout works the same as the CLICK command 71 | - Only available in a run 72 | - WAIT Timeout 73 | - Waits for the specified number of frames or until a state change is detected, then transmits the current game state (same behavior as Timeout for the CLICK and KEY commands, but no input is sent to the game) 74 | - Possibly useful for KEY and CLICK commands which are expected to produce multiple state changes as detected by Communication Mod 75 | - Only available in a run 76 | - STATE 77 | - Causes CommunicationMod to immediately send a JSON representation of the current state to the external process, whether or not the game state is stable. 78 | - Always available. 79 | - Upon receiving a command, CommunicationMod will execute it, and reply again with a JSON representation of the state of the game, when it is next stable. 80 | - If there was an error in executing the command, CommunicationMod will instead send an error message of the form: 81 | ``` 82 | {"error":"Error message","ready_for_command":True} 83 | ``` 84 | 85 | ## Known issues and limitations, to be hopefully fixed soon: 86 | - The full state of the Match and Keep event is not transmitted. 87 | - There is no feedback or state change if you attempt to take or buy a potion while your potion inventory is full. Beware! 88 | - Unselecting cards in hand select screens is not supported. 89 | - Several actions do not currently register a state change if they are performed manually in game. 90 | - You must manually edit the mod's config file to set the command for your external process. 91 | - Communication Mod has not been tested without fast mode on. 92 | 93 | ## What are some of the potential applications of this mod? 94 | 95 | - Twitch plays Slay the Spire 96 | - Slay the Spire AIs 97 | - Streamers can display detailed information about the current run while in game 98 | 99 | ## Frequently asked questions 100 | 101 | - How do I debug my process? 102 | 103 | Communication Mod captures both the stderr and stdout of the external process. All messages sent to stdout are included in the game log, which is displayed in a window by ModTheSpire. All messages sent to the process by Communication Mod are also visible in the game log. The stdout of the external process is logged in a file named communication_mod_errors.log. Instead of printing debug information to stdout, try using a log file. 104 | 105 | - When I start the external process, the game hangs for 10 seconds, and then the external process quits. What do I do? 106 | 107 | Communication Mod is probably not receiving a ready signal. Make sure your process sends "Ready\n" to stdout when it is ready to receive commands from Communication Mod. If this is not the problem, there is likely some issue with the command used to start the process. Check communication_mod_errors.log to help debug these kinds of issues. 108 | 109 | - Can I get some example code to help get started with the Communication Mod protocol? 110 | 111 | Try looking at [spirecomm](https://github.com/ForgottenArbiter/spirecomm), the Python package I wrote to interface with Communication Mod. -------------------------------------------------------------------------------- /src/main/java/communicationmod/CommandExecutor.java: -------------------------------------------------------------------------------- 1 | package communicationmod; 2 | 3 | import basemod.ReflectionHacks; 4 | import com.badlogic.gdx.Gdx; 5 | import com.megacrit.cardcrawl.cards.AbstractCard; 6 | import com.megacrit.cardcrawl.cards.CardQueueItem; 7 | import com.megacrit.cardcrawl.characters.AbstractPlayer; 8 | import com.megacrit.cardcrawl.characters.CharacterManager; 9 | import com.megacrit.cardcrawl.core.CardCrawlGame; 10 | import com.megacrit.cardcrawl.core.Settings; 11 | import com.megacrit.cardcrawl.dungeons.AbstractDungeon; 12 | import com.megacrit.cardcrawl.helpers.SeedHelper; 13 | import com.megacrit.cardcrawl.helpers.TrialHelper; 14 | import com.megacrit.cardcrawl.helpers.input.InputAction; 15 | import com.megacrit.cardcrawl.helpers.input.InputActionSet; 16 | import com.megacrit.cardcrawl.helpers.input.InputHelper; 17 | import com.megacrit.cardcrawl.monsters.AbstractMonster; 18 | import com.megacrit.cardcrawl.potions.AbstractPotion; 19 | import com.megacrit.cardcrawl.potions.PotionSlot; 20 | import com.megacrit.cardcrawl.random.Random; 21 | import com.megacrit.cardcrawl.relics.AbstractRelic; 22 | import com.megacrit.cardcrawl.rooms.*; 23 | import communicationmod.patches.InputActionPatch; 24 | import org.apache.logging.log4j.LogManager; 25 | import org.apache.logging.log4j.Logger; 26 | 27 | import java.util.ArrayList; 28 | 29 | public class CommandExecutor { 30 | 31 | private static final Logger logger = LogManager.getLogger(CommandExecutor.class.getName()); 32 | 33 | public static boolean executeCommand(String command) throws InvalidCommandException { 34 | command = command.toLowerCase(); 35 | String [] tokens = command.split("\\s+"); 36 | if(tokens.length == 0) { 37 | return false; 38 | } 39 | if (!isCommandAvailable(tokens[0])) { 40 | throw new InvalidCommandException("Invalid command: " + tokens[0] + ". Possible commands: " + getAvailableCommands()); 41 | } 42 | String command_tail = command.substring(tokens[0].length()); 43 | switch(tokens[0]) { 44 | case "play": 45 | executePlayCommand(tokens); 46 | return true; 47 | case "end": 48 | executeEndCommand(); 49 | return true; 50 | case "choose": 51 | executeChooseCommand(tokens); 52 | return true; 53 | case "potion": 54 | executePotionCommand(tokens); 55 | return true; 56 | case "confirm": 57 | case "proceed": 58 | executeConfirmCommand(); 59 | return true; 60 | case "skip": 61 | case "cancel": 62 | case "return": 63 | case "leave": 64 | executeCancelCommand(); 65 | return true; 66 | case "start": 67 | executeStartCommand(tokens); 68 | return true; 69 | case "state": 70 | executeStateCommand(); 71 | return false; 72 | case "key": 73 | executeKeyCommand(tokens); 74 | return true; 75 | case "click": 76 | executeClickCommand(tokens); 77 | return true; 78 | case "wait": 79 | executeWaitCommand(tokens); 80 | return true; 81 | 82 | default: 83 | logger.info("This should never happen."); 84 | throw new InvalidCommandException("Command not recognized."); 85 | } 86 | } 87 | 88 | public static ArrayList getAvailableCommands() { 89 | ArrayList availableCommands = new ArrayList<>(); 90 | if (isPlayCommandAvailable()) { 91 | availableCommands.add("play"); 92 | } 93 | if (isChooseCommandAvailable()) { 94 | availableCommands.add("choose"); 95 | } 96 | if (isEndCommandAvailable()) { 97 | availableCommands.add("end"); 98 | } 99 | if (isPotionCommandAvailable()) { 100 | availableCommands.add("potion"); 101 | } 102 | if (isConfirmCommandAvailable()) { 103 | availableCommands.add(ChoiceScreenUtils.getConfirmButtonText()); 104 | } 105 | if (isCancelCommandAvailable()) { 106 | availableCommands.add(ChoiceScreenUtils.getCancelButtonText()); 107 | } 108 | if (isStartCommandAvailable()) { 109 | availableCommands.add("start"); 110 | } 111 | if (isInDungeon()) { 112 | availableCommands.add("key"); 113 | availableCommands.add("click"); 114 | availableCommands.add("wait"); 115 | } 116 | availableCommands.add("state"); 117 | return availableCommands; 118 | } 119 | 120 | public static boolean isCommandAvailable(String command) { 121 | if(command.equals("confirm") || command.equalsIgnoreCase("proceed")) { 122 | return isConfirmCommandAvailable(); 123 | } else if (command.equals("skip") || command.equals("cancel") || command.equals("return") || command.equals("leave")) { 124 | return isCancelCommandAvailable(); 125 | } else { 126 | return getAvailableCommands().contains(command); 127 | } 128 | } 129 | 130 | public static boolean isInDungeon() { 131 | return CardCrawlGame.mode == CardCrawlGame.GameMode.GAMEPLAY && AbstractDungeon.isPlayerInDungeon() && AbstractDungeon.currMapNode != null; 132 | } 133 | 134 | private static boolean isPlayCommandAvailable() { 135 | if(isInDungeon()) { 136 | if(AbstractDungeon.getCurrRoom().phase == AbstractRoom.RoomPhase.COMBAT && !AbstractDungeon.isScreenUp) { 137 | // Play command is not available if none of the cards are playable. 138 | // TODO: this does not check the case where there is no legal target for a target card. 139 | for (AbstractCard card : AbstractDungeon.player.hand.group) { 140 | if (card.canUse(AbstractDungeon.player, null)) { 141 | return true; 142 | } 143 | } 144 | } 145 | } 146 | return false; 147 | } 148 | 149 | public static boolean isEndCommandAvailable() { 150 | return isInDungeon() && AbstractDungeon.getCurrRoom().phase == AbstractRoom.RoomPhase.COMBAT && !AbstractDungeon.isScreenUp; 151 | } 152 | 153 | public static boolean isChooseCommandAvailable() { 154 | if(isInDungeon()) { 155 | return !isPlayCommandAvailable() && !ChoiceScreenUtils.getCurrentChoiceList().isEmpty(); 156 | } else { 157 | return false; 158 | } 159 | } 160 | 161 | public static boolean isPotionCommandAvailable() { 162 | if(isInDungeon()) { 163 | for(AbstractPotion potion : AbstractDungeon.player.potions) { 164 | if(!(potion instanceof PotionSlot)) { 165 | return true; 166 | } 167 | } 168 | } 169 | return false; 170 | } 171 | 172 | public static boolean isConfirmCommandAvailable() { 173 | if(isInDungeon()) { 174 | return ChoiceScreenUtils.isConfirmButtonAvailable(); 175 | } else { 176 | return false; 177 | } 178 | } 179 | 180 | public static boolean isCancelCommandAvailable() { 181 | if(isInDungeon()) { 182 | return ChoiceScreenUtils.isCancelButtonAvailable(); 183 | } else { 184 | return false; 185 | } 186 | } 187 | 188 | public static boolean isStartCommandAvailable() { 189 | return !isInDungeon() && CardCrawlGame.mainMenuScreen != null; 190 | } 191 | 192 | private static void executeStateCommand() { 193 | CommunicationMod.mustSendGameState = true; 194 | } 195 | 196 | private static void executePlayCommand(String[] tokens) throws InvalidCommandException { 197 | if(tokens.length < 2) { 198 | throw new InvalidCommandException(tokens, InvalidCommandException.InvalidCommandFormat.MISSING_ARGUMENT); 199 | } 200 | int card_index; 201 | try { 202 | card_index = Integer.parseInt(tokens[1]); 203 | } catch (NumberFormatException e) { 204 | throw new InvalidCommandException(tokens, InvalidCommandException.InvalidCommandFormat.INVALID_ARGUMENT, tokens[1]); 205 | } 206 | if(card_index == 0) { 207 | card_index = 10; 208 | } 209 | if((card_index < 1) || (card_index > AbstractDungeon.player.hand.size())) { 210 | throw new InvalidCommandException(tokens, InvalidCommandException.InvalidCommandFormat.OUT_OF_BOUNDS, Integer.toString(card_index)); 211 | } 212 | int monster_index = -1; 213 | if(tokens.length == 3) { 214 | try { 215 | monster_index = Integer.parseInt(tokens[2]); 216 | } catch (NumberFormatException e) { 217 | throw new InvalidCommandException(tokens, InvalidCommandException.InvalidCommandFormat.INVALID_ARGUMENT, tokens[2]); 218 | } 219 | } 220 | AbstractMonster target_monster = null; 221 | if (monster_index != -1) { 222 | if (monster_index < 0 || monster_index >= AbstractDungeon.getCurrRoom().monsters.monsters.size()) { 223 | throw new InvalidCommandException(tokens, InvalidCommandException.InvalidCommandFormat.OUT_OF_BOUNDS, Integer.toString(monster_index)); 224 | } else { 225 | target_monster = AbstractDungeon.getCurrRoom().monsters.monsters.get(monster_index); 226 | } 227 | } 228 | if((card_index < 1) || (card_index > AbstractDungeon.player.hand.size()) || !(AbstractDungeon.player.hand.group.get(card_index - 1).canUse(AbstractDungeon.player, target_monster))) { 229 | throw new InvalidCommandException("Selected card cannot be played with the selected target."); 230 | } 231 | AbstractCard card = AbstractDungeon.player.hand.group.get(card_index - 1); 232 | if(card.target == AbstractCard.CardTarget.ENEMY || card.target == AbstractCard.CardTarget.SELF_AND_ENEMY) { 233 | if(target_monster == null) { 234 | throw new InvalidCommandException("Selected card requires an enemy target."); 235 | } 236 | AbstractDungeon.actionManager.cardQueue.add(new CardQueueItem(card, target_monster)); 237 | } else { 238 | AbstractDungeon.actionManager.cardQueue.add(new CardQueueItem(card, null)); 239 | } 240 | } 241 | 242 | private static void executeEndCommand() throws InvalidCommandException { 243 | AbstractDungeon.overlayMenu.endTurnButton.disable(true); 244 | } 245 | 246 | private static void executeChooseCommand(String[] tokens) throws InvalidCommandException { 247 | ArrayList validChoices = ChoiceScreenUtils.getCurrentChoiceList(); 248 | if(validChoices.size() == 0) { 249 | throw new InvalidCommandException("The choice command is not implemented on this screen."); 250 | } 251 | int choice_index = getValidChoiceIndex(tokens, validChoices); 252 | ChoiceScreenUtils.executeChoice(choice_index); 253 | } 254 | 255 | private static void executePotionCommand(String[] tokens) throws InvalidCommandException { 256 | int potion_index; 257 | boolean use; 258 | if (tokens.length < 3) { 259 | throw new InvalidCommandException(tokens, InvalidCommandException.InvalidCommandFormat.MISSING_ARGUMENT); 260 | } 261 | if(tokens[1].equals("use")) { 262 | use = true; 263 | } else if (tokens[1].equals("discard")) { 264 | use = false; 265 | } else { 266 | throw new InvalidCommandException(tokens, InvalidCommandException.InvalidCommandFormat.INVALID_ARGUMENT, tokens[1]); 267 | } 268 | try { 269 | potion_index = Integer.parseInt(tokens[2]); 270 | } catch (NumberFormatException e) { 271 | throw new InvalidCommandException(tokens, InvalidCommandException.InvalidCommandFormat.INVALID_ARGUMENT, tokens[2]); 272 | } 273 | if(potion_index < 0 || potion_index >= AbstractDungeon.player.potionSlots) { 274 | throw new InvalidCommandException("Potion index out of bounds."); 275 | } 276 | AbstractPotion selectedPotion = AbstractDungeon.player.potions.get(potion_index); 277 | if(selectedPotion instanceof PotionSlot) { 278 | throw new InvalidCommandException("No potion in the selected slot."); 279 | } 280 | if(use && !selectedPotion.canUse()) { 281 | throw new InvalidCommandException("Selected potion cannot be used."); 282 | } 283 | if(!use && !selectedPotion.canDiscard()) { 284 | throw new InvalidCommandException("Selected potion cannot be discarded."); 285 | } 286 | int monster_index = -1; 287 | if (use) { 288 | if (selectedPotion.targetRequired) { 289 | if (tokens.length < 4) { 290 | throw new InvalidCommandException(tokens, InvalidCommandException.InvalidCommandFormat.MISSING_ARGUMENT, " Selected potion requires a target."); 291 | } 292 | AbstractMonster target_monster; 293 | try { 294 | monster_index = Integer.parseInt(tokens[3]); 295 | } catch (NumberFormatException e) { 296 | throw new InvalidCommandException(tokens, InvalidCommandException.InvalidCommandFormat.INVALID_ARGUMENT, tokens[3]); 297 | } 298 | if (monster_index < 0 || monster_index >= AbstractDungeon.getCurrRoom().monsters.monsters.size()) { 299 | throw new InvalidCommandException(tokens, InvalidCommandException.InvalidCommandFormat.OUT_OF_BOUNDS, Integer.toString(monster_index)); 300 | } else { 301 | target_monster = AbstractDungeon.getCurrRoom().monsters.monsters.get(monster_index); 302 | } 303 | selectedPotion.use(target_monster); 304 | } else { 305 | selectedPotion.use(AbstractDungeon.player); 306 | } 307 | for (AbstractRelic r : AbstractDungeon.player.relics) { 308 | r.onUsePotion(); 309 | } 310 | } 311 | AbstractDungeon.topPanel.destroyPotion(selectedPotion.slot); 312 | GameStateListener.registerStateChange(); 313 | } 314 | 315 | private static void executeConfirmCommand() { 316 | ChoiceScreenUtils.pressConfirmButton(); 317 | } 318 | 319 | private static void executeCancelCommand() { 320 | ChoiceScreenUtils.pressCancelButton(); 321 | } 322 | 323 | private static void executeStartCommand(String[] tokens) throws InvalidCommandException { 324 | if (tokens.length < 2) { 325 | throw new InvalidCommandException(tokens, InvalidCommandException.InvalidCommandFormat.MISSING_ARGUMENT); 326 | } 327 | int ascensionLevel = 0; 328 | boolean seedSet = false; 329 | long seed = 0; 330 | AbstractPlayer.PlayerClass selectedClass = null; 331 | for(AbstractPlayer.PlayerClass playerClass : AbstractPlayer.PlayerClass.values()) { 332 | if(playerClass.name().equalsIgnoreCase(tokens[1])) { 333 | selectedClass = playerClass; 334 | } 335 | } 336 | // Better to allow people to specify the character as "silent" rather than requiring "the_silent" 337 | if(tokens[1].equalsIgnoreCase("silent")) { 338 | selectedClass = AbstractPlayer.PlayerClass.THE_SILENT; 339 | } 340 | if(selectedClass == null) { 341 | throw new InvalidCommandException(tokens, InvalidCommandException.InvalidCommandFormat.INVALID_ARGUMENT, tokens[1]); 342 | } 343 | if(tokens.length >= 3) { 344 | try { 345 | ascensionLevel = Integer.parseInt(tokens[2]); 346 | } catch (NumberFormatException e) { 347 | throw new InvalidCommandException(tokens, InvalidCommandException.InvalidCommandFormat.INVALID_ARGUMENT, tokens[2]); 348 | } 349 | if(ascensionLevel < 0 || ascensionLevel > 20) { 350 | throw new InvalidCommandException(tokens, InvalidCommandException.InvalidCommandFormat.OUT_OF_BOUNDS, tokens[2]); 351 | } 352 | } 353 | if(tokens.length >= 4) { 354 | String seedString = tokens[3].toUpperCase(); 355 | if(!seedString.matches("^[A-Z0-9]+$")) { 356 | throw new InvalidCommandException(tokens, InvalidCommandException.InvalidCommandFormat.INVALID_ARGUMENT, seedString); 357 | } 358 | seedSet = true; 359 | seed = SeedHelper.getLong(seedString); 360 | boolean isTrialSeed = TrialHelper.isTrialSeed(seedString); 361 | if (isTrialSeed) { 362 | Settings.specialSeed = seed; 363 | Settings.isTrial = true; 364 | seedSet = false; 365 | } 366 | } 367 | if(!seedSet) { 368 | seed = SeedHelper.generateUnoffensiveSeed(new Random(System.nanoTime())); 369 | } 370 | Settings.seed = seed; 371 | Settings.seedSet = seedSet; 372 | AbstractDungeon.generateSeeds(); 373 | AbstractDungeon.ascensionLevel = ascensionLevel; 374 | AbstractDungeon.isAscensionMode = ascensionLevel > 0; 375 | CardCrawlGame.startOver = true; 376 | CardCrawlGame.mainMenuScreen.isFadingOut = true; 377 | CardCrawlGame.mainMenuScreen.fadeOutMusic(); 378 | CharacterManager manager = new CharacterManager(); 379 | manager.setChosenCharacter(selectedClass); 380 | CardCrawlGame.chosenCharacter = selectedClass; 381 | GameStateListener.resetStateVariables(); 382 | } 383 | 384 | private static void executeKeyCommand(String[] tokens) throws InvalidCommandException { 385 | if (tokens.length < 2) { 386 | throw new InvalidCommandException(tokens, InvalidCommandException.InvalidCommandFormat.MISSING_ARGUMENT); 387 | } 388 | int keycode = getKeycode(tokens[1].toUpperCase()); 389 | if (keycode == -1) { 390 | throw new InvalidCommandException(tokens, InvalidCommandException.InvalidCommandFormat.INVALID_ARGUMENT, tokens[1]); 391 | } 392 | int timeout = 100; 393 | if (tokens.length >= 3) { 394 | try { 395 | timeout = Integer.parseInt(tokens[2]); 396 | } catch (NumberFormatException e) { 397 | throw new InvalidCommandException(tokens, InvalidCommandException.InvalidCommandFormat.INVALID_ARGUMENT, tokens[2]); 398 | } 399 | if(timeout < 0) { 400 | throw new InvalidCommandException(tokens, InvalidCommandException.InvalidCommandFormat.OUT_OF_BOUNDS, tokens[2]); 401 | } 402 | } 403 | InputActionPatch.doKeypress = true; 404 | InputActionPatch.key = keycode; 405 | InputHelper.updateFirst(); 406 | GameStateListener.setTimeout(timeout); 407 | } 408 | 409 | private static void executeClickCommand(String[] tokens) throws InvalidCommandException { 410 | if (tokens.length < 4) { 411 | throw new InvalidCommandException(tokens, InvalidCommandException.InvalidCommandFormat.MISSING_ARGUMENT); 412 | } 413 | float x = 0; 414 | float y = 0; 415 | int timeout = 100; 416 | try { 417 | x = Float.parseFloat(tokens[2]); 418 | } catch (NumberFormatException e) { 419 | throw new InvalidCommandException(tokens, InvalidCommandException.InvalidCommandFormat.INVALID_ARGUMENT, tokens[2]); 420 | } 421 | try { 422 | y = Float.parseFloat(tokens[3]); 423 | } catch (NumberFormatException e) { 424 | throw new InvalidCommandException(tokens, InvalidCommandException.InvalidCommandFormat.INVALID_ARGUMENT, tokens[3]); 425 | } 426 | x = x * Settings.scale; 427 | y = y * Settings.scale; 428 | Gdx.input.setCursorPosition((int)x, (int)y); 429 | InputHelper.updateFirst(); 430 | String token1 = tokens[1].toUpperCase(); 431 | if (token1.equals("LEFT")) { 432 | InputHelper.justClickedLeft = true; 433 | InputHelper.isMouseDown = true; 434 | ReflectionHacks.setPrivateStatic(InputHelper.class, "isPrevMouseDown", true); 435 | } else if (token1.equals("RIGHT")) { 436 | InputHelper.justClickedRight = true; 437 | InputHelper.isMouseDown_R = true; 438 | ReflectionHacks.setPrivateStatic(InputHelper.class, "isPrevMouseDown_R", true); 439 | } else { 440 | throw new InvalidCommandException(tokens, InvalidCommandException.InvalidCommandFormat.INVALID_ARGUMENT, tokens[1]); 441 | } 442 | if (tokens.length >= 5) { 443 | try { 444 | timeout = Integer.parseInt(tokens[4]); 445 | } catch (NumberFormatException e) { 446 | throw new InvalidCommandException(tokens, InvalidCommandException.InvalidCommandFormat.INVALID_ARGUMENT, tokens[4]); 447 | } 448 | if(timeout < 0) { 449 | throw new InvalidCommandException(tokens, InvalidCommandException.InvalidCommandFormat.OUT_OF_BOUNDS, tokens[4]); 450 | } 451 | } 452 | GameStateListener.setTimeout(timeout); 453 | } 454 | 455 | private static void executeWaitCommand(String[] tokens) throws InvalidCommandException { 456 | if (tokens.length < 2) { 457 | throw new InvalidCommandException(tokens, InvalidCommandException.InvalidCommandFormat.MISSING_ARGUMENT); 458 | } 459 | int timeout = 0; 460 | try { 461 | timeout = Integer.parseInt(tokens[1]); 462 | } catch (NumberFormatException e) { 463 | throw new InvalidCommandException(tokens, InvalidCommandException.InvalidCommandFormat.INVALID_ARGUMENT, tokens[1]); 464 | } 465 | if(timeout < 0) { 466 | throw new InvalidCommandException(tokens, InvalidCommandException.InvalidCommandFormat.OUT_OF_BOUNDS, tokens[1]); 467 | } 468 | GameStateListener.setTimeout(timeout); 469 | } 470 | 471 | private static int getKeycode(String keyName) { 472 | InputAction action; 473 | switch(keyName) { 474 | case "CONFIRM": 475 | action = InputActionSet.confirm; 476 | break; 477 | case "CANCEL": 478 | action = InputActionSet.cancel; 479 | break; 480 | case "MAP": 481 | action = InputActionSet.map; 482 | break; 483 | case "DECK": 484 | action = InputActionSet.masterDeck; 485 | break; 486 | case "DRAW_PILE": 487 | action = InputActionSet.drawPile; 488 | break; 489 | case "DISCARD_PILE": 490 | action = InputActionSet.discardPile; 491 | break; 492 | case "EXHAUST_PILE": 493 | action = InputActionSet.exhaustPile; 494 | break; 495 | case "END_TURN": 496 | action = InputActionSet.endTurn; 497 | break; 498 | case "UP": 499 | action = InputActionSet.up; 500 | break; 501 | case "DOWN": 502 | action = InputActionSet.down; 503 | break; 504 | case "LEFT": 505 | action = InputActionSet.left; 506 | break; 507 | case "RIGHT": 508 | action = InputActionSet.right; 509 | break; 510 | case "DROP_CARD": 511 | action = InputActionSet.releaseCard; 512 | break; 513 | case "CARD_1": 514 | action = InputActionSet.selectCard_1; 515 | break; 516 | case "CARD_2": 517 | action = InputActionSet.selectCard_2; 518 | break; 519 | case "CARD_3": 520 | action = InputActionSet.selectCard_3; 521 | break; 522 | case "CARD_4": 523 | action = InputActionSet.selectCard_4; 524 | break; 525 | case "CARD_5": 526 | action = InputActionSet.selectCard_5; 527 | break; 528 | case "CARD_6": 529 | action = InputActionSet.selectCard_6; 530 | break; 531 | case "CARD_7": 532 | action = InputActionSet.selectCard_7; 533 | break; 534 | case "CARD_8": 535 | action = InputActionSet.selectCard_8; 536 | break; 537 | case "CARD_9": 538 | action = InputActionSet.selectCard_9; 539 | break; 540 | case "CARD_10": 541 | action = InputActionSet.selectCard_10; 542 | break; 543 | default: 544 | action = null; 545 | } 546 | if (action == null) { 547 | return -1; 548 | } else { 549 | return (int) ReflectionHacks.getPrivate(action, InputAction.class, "keycode"); 550 | } 551 | } 552 | 553 | private static int getValidChoiceIndex(String[] tokens, ArrayList validChoices) throws InvalidCommandException { 554 | if(tokens.length < 2) { 555 | throw new InvalidCommandException(tokens, InvalidCommandException.InvalidCommandFormat.MISSING_ARGUMENT, " A choice is required."); 556 | } 557 | String choice = merge_arguments(tokens); 558 | int choice_index = -1; 559 | if(validChoices.contains(choice)) { 560 | choice_index = validChoices.indexOf(choice); 561 | } else { 562 | try { 563 | choice_index = Integer.parseInt(choice); 564 | } catch (NumberFormatException e) { 565 | throw new InvalidCommandException(tokens, InvalidCommandException.InvalidCommandFormat.INVALID_ARGUMENT, choice); 566 | } 567 | if(choice_index < 0 || choice_index >= validChoices.size()) { 568 | throw new InvalidCommandException(tokens, InvalidCommandException.InvalidCommandFormat.OUT_OF_BOUNDS, choice); 569 | } 570 | } 571 | return choice_index; 572 | } 573 | 574 | private static String merge_arguments(String[] tokens) { 575 | StringBuilder builder = new StringBuilder(); 576 | for(int i = 1; i < tokens.length; i++) { 577 | builder.append(tokens[i]); 578 | if(i != tokens.length - 1) { 579 | builder.append(' '); 580 | } 581 | } 582 | return builder.toString(); 583 | } 584 | 585 | 586 | 587 | } 588 | -------------------------------------------------------------------------------- /src/main/java/communicationmod/ChoiceScreenUtils.java: -------------------------------------------------------------------------------- 1 | package communicationmod; 2 | 3 | import basemod.ReflectionHacks; 4 | import com.badlogic.gdx.Gdx; 5 | import com.megacrit.cardcrawl.cards.AbstractCard; 6 | import com.megacrit.cardcrawl.cards.CardGroup; 7 | import com.megacrit.cardcrawl.core.CardCrawlGame; 8 | import com.megacrit.cardcrawl.core.Settings; 9 | import com.megacrit.cardcrawl.dungeons.AbstractDungeon; 10 | import com.megacrit.cardcrawl.dungeons.TheEnding; 11 | import com.megacrit.cardcrawl.events.AbstractImageEvent; 12 | import com.megacrit.cardcrawl.events.GenericEventDialog; 13 | import com.megacrit.cardcrawl.events.RoomEventDialog; 14 | import com.megacrit.cardcrawl.events.shrines.GremlinMatchGame; 15 | import com.megacrit.cardcrawl.events.shrines.GremlinWheelGame; 16 | import com.megacrit.cardcrawl.helpers.Hitbox; 17 | import com.megacrit.cardcrawl.helpers.input.InputHelper; 18 | import com.megacrit.cardcrawl.map.DungeonMap; 19 | import com.megacrit.cardcrawl.map.MapRoomNode; 20 | import com.megacrit.cardcrawl.relics.AbstractRelic; 21 | import com.megacrit.cardcrawl.rewards.RewardItem; 22 | import com.megacrit.cardcrawl.rewards.chests.AbstractChest; 23 | import com.megacrit.cardcrawl.rooms.*; 24 | import com.megacrit.cardcrawl.screens.CardRewardScreen; 25 | import com.megacrit.cardcrawl.screens.mainMenu.MenuCancelButton; 26 | import com.megacrit.cardcrawl.screens.select.BossRelicSelectScreen; 27 | import com.megacrit.cardcrawl.screens.select.GridCardSelectScreen; 28 | import com.megacrit.cardcrawl.screens.select.HandCardSelectScreen; 29 | import com.megacrit.cardcrawl.shop.ShopScreen; 30 | import com.megacrit.cardcrawl.shop.StorePotion; 31 | import com.megacrit.cardcrawl.shop.StoreRelic; 32 | import com.megacrit.cardcrawl.ui.buttons.*; 33 | import com.megacrit.cardcrawl.ui.campfire.AbstractCampfireOption; 34 | import communicationmod.patches.*; 35 | import org.apache.logging.log4j.LogManager; 36 | import org.apache.logging.log4j.Logger; 37 | 38 | import java.lang.reflect.InvocationTargetException; 39 | import java.lang.reflect.Method; 40 | import java.util.ArrayList; 41 | import java.util.regex.Matcher; 42 | import java.util.regex.Pattern; 43 | 44 | 45 | public class ChoiceScreenUtils { 46 | 47 | private static final Logger logger = LogManager.getLogger(ChoiceScreenUtils.class.getName()); 48 | 49 | public enum ChoiceType { 50 | EVENT, 51 | CHEST, 52 | SHOP_ROOM, 53 | REST, 54 | CARD_REWARD, 55 | COMBAT_REWARD, 56 | MAP, 57 | BOSS_REWARD, 58 | SHOP_SCREEN, 59 | GRID, 60 | HAND_SELECT, 61 | GAME_OVER, 62 | COMPLETE, 63 | NONE 64 | } 65 | 66 | public enum EventDialogType { 67 | IMAGE, ROOM, NONE 68 | } 69 | 70 | public static ChoiceType getCurrentChoiceType() { 71 | if (!AbstractDungeon.isScreenUp) { 72 | if (AbstractDungeon.getCurrRoom().phase == AbstractRoom.RoomPhase.EVENT || (AbstractDungeon.getCurrRoom().event != null && AbstractDungeon.getCurrRoom().phase == AbstractRoom.RoomPhase.COMPLETE)) { 73 | return ChoiceType.EVENT; 74 | } else if (AbstractDungeon.getCurrRoom() instanceof TreasureRoomBoss || AbstractDungeon.getCurrRoom() instanceof TreasureRoom) { 75 | return ChoiceType.CHEST; 76 | } else if (AbstractDungeon.getCurrRoom() instanceof ShopRoom) { 77 | return ChoiceType.SHOP_ROOM; 78 | } else if (AbstractDungeon.getCurrRoom() instanceof RestRoom) { 79 | return ChoiceType.REST; 80 | } else if (AbstractDungeon.getCurrRoom().phase == AbstractRoom.RoomPhase.COMPLETE && AbstractDungeon.actionManager.isEmpty() && !AbstractDungeon.isFadingOut) { 81 | if (AbstractDungeon.getCurrRoom().event == null || (!(AbstractDungeon.getCurrRoom().event instanceof AbstractImageEvent) && (!AbstractDungeon.getCurrRoom().event.hasFocus))) { 82 | return ChoiceType.COMPLETE; 83 | } 84 | } else { 85 | return ChoiceType.NONE; 86 | } 87 | } 88 | AbstractDungeon.CurrentScreen screen = AbstractDungeon.screen; 89 | switch(screen) { 90 | case CARD_REWARD: 91 | return ChoiceType.CARD_REWARD; 92 | case COMBAT_REWARD: 93 | return ChoiceType.COMBAT_REWARD; 94 | case MAP: 95 | return ChoiceType.MAP; 96 | case BOSS_REWARD: 97 | return ChoiceType.BOSS_REWARD; 98 | case SHOP: 99 | return ChoiceType.SHOP_SCREEN; 100 | case GRID: 101 | return ChoiceType.GRID; 102 | case HAND_SELECT: 103 | return ChoiceType.HAND_SELECT; 104 | case DEATH: 105 | case VICTORY: 106 | case UNLOCK: 107 | case NEOW_UNLOCK: 108 | return ChoiceType.GAME_OVER; 109 | default: 110 | return ChoiceType.NONE; 111 | } 112 | } 113 | 114 | public static ArrayList getCurrentChoiceList() { 115 | ChoiceType choiceType = getCurrentChoiceType(); 116 | ArrayList choices; 117 | switch (choiceType) { 118 | case EVENT: 119 | choices = getEventScreenChoices(); 120 | break; 121 | case CHEST: 122 | choices = getChestRoomChoices(); 123 | break; 124 | case SHOP_ROOM: 125 | choices = getShopRoomChoices(); 126 | break; 127 | case REST: 128 | choices = getRestRoomChoices(); 129 | break; 130 | case CARD_REWARD: 131 | choices = getCardRewardScreenChoices(); 132 | break; 133 | case COMBAT_REWARD: 134 | choices = getCombatRewardScreenChoices(); 135 | break; 136 | case MAP: 137 | choices = getMapScreenChoices(); 138 | break; 139 | case BOSS_REWARD: 140 | choices = getBossRewardScreenChoices(); 141 | break; 142 | case SHOP_SCREEN: 143 | choices = getShopScreenChoices(); 144 | break; 145 | case GRID: 146 | choices = getGridScreenChoices(); 147 | break; 148 | case HAND_SELECT: 149 | choices = getHandSelectScreenChoices(); 150 | break; 151 | default: 152 | return new ArrayList<>(); 153 | } 154 | ArrayList lowerCaseChoices = new ArrayList<>(); 155 | for(String item : choices) { 156 | lowerCaseChoices.add(item.toLowerCase()); 157 | } 158 | return lowerCaseChoices; 159 | } 160 | 161 | public static void executeChoice(int choice_index) { 162 | ChoiceType choiceType = getCurrentChoiceType(); 163 | switch (choiceType) { 164 | case EVENT: 165 | makeEventChoice(choice_index); 166 | return; 167 | case CHEST: 168 | makeChestRoomChoice(choice_index); 169 | return; 170 | case SHOP_ROOM: 171 | makeShopRoomChoice(choice_index); 172 | return; 173 | case REST: 174 | makeRestRoomChoice(choice_index); 175 | return; 176 | case CARD_REWARD: 177 | makeCardRewardChoice(choice_index); 178 | return; 179 | case COMBAT_REWARD: 180 | makeCombatRewardChoice(choice_index); 181 | return; 182 | case MAP: 183 | makeMapChoice(choice_index); 184 | return; 185 | case BOSS_REWARD: 186 | makeBossRewardChoice(choice_index); 187 | return; 188 | case SHOP_SCREEN: 189 | makeShopScreenChoice(choice_index); 190 | return; 191 | case GRID: 192 | makeGridScreenChoice(choice_index); 193 | return; 194 | case HAND_SELECT: 195 | makeHandSelectScreenChoice(choice_index); 196 | return; 197 | default: 198 | logger.info("Unimplemented choice."); 199 | } 200 | } 201 | 202 | private static boolean isCancelButtonAvailable(ChoiceType choiceType) { 203 | switch (choiceType) { 204 | case EVENT: 205 | return false; 206 | case CHEST: 207 | return false; 208 | case SHOP_ROOM: 209 | return false; 210 | case REST: 211 | return false; 212 | case CARD_REWARD: 213 | return isCardRewardSkipAvailable(); 214 | case COMBAT_REWARD: 215 | return isCombatRewardCloseAvailable(); 216 | case MAP: 217 | return AbstractDungeon.dungeonMapScreen.dismissable; 218 | case BOSS_REWARD: 219 | return true; 220 | case SHOP_SCREEN: 221 | return true; 222 | case GRID: 223 | return isGridScreenCancelAvailable(); 224 | case HAND_SELECT: 225 | return false; 226 | case GAME_OVER: 227 | return false; 228 | case COMPLETE: 229 | return false; 230 | default: 231 | return false; 232 | } 233 | } 234 | 235 | public static boolean isCancelButtonAvailable() { 236 | return isCancelButtonAvailable(getCurrentChoiceType()); 237 | } 238 | 239 | private static String getCancelButtonText(ChoiceType choiceType) { 240 | switch (choiceType) { 241 | case CARD_REWARD: 242 | return "skip"; 243 | case COMBAT_REWARD: 244 | return "skip"; 245 | case MAP: 246 | return "return"; 247 | case BOSS_REWARD: 248 | return "skip"; 249 | case SHOP_SCREEN: 250 | return "leave"; 251 | case GRID: 252 | return "cancel"; 253 | default: 254 | return "cancel"; 255 | } 256 | } 257 | 258 | public static String getCancelButtonText() { 259 | return getCancelButtonText(getCurrentChoiceType()); 260 | } 261 | 262 | private static void pressCancelButton(ChoiceType choiceType) { 263 | switch (choiceType) { 264 | case CARD_REWARD: 265 | case COMBAT_REWARD: 266 | AbstractDungeon.closeCurrentScreen(); 267 | return; 268 | case MAP: 269 | clickCancelButton(); 270 | return; 271 | case BOSS_REWARD: 272 | MenuCancelButton button = (MenuCancelButton)ReflectionHacks.getPrivate(AbstractDungeon.bossRelicScreen, BossRelicSelectScreen.class, "cancelButton"); 273 | button.hb.clicked = true; 274 | return; 275 | case SHOP_SCREEN: 276 | clickCancelButton(); 277 | return; 278 | case GRID: 279 | clickCancelButton(); 280 | } 281 | } 282 | 283 | public static void pressCancelButton() { 284 | pressCancelButton(getCurrentChoiceType()); 285 | } 286 | 287 | private static boolean isConfirmButtonAvailable(ChoiceType choiceType) { 288 | switch (choiceType) { 289 | case EVENT: 290 | return false; 291 | case CHEST: 292 | return true; 293 | case SHOP_ROOM: 294 | return true; 295 | case REST: 296 | return isRestRoomProceedAvailable(); 297 | case CARD_REWARD: 298 | return false; 299 | case COMBAT_REWARD: 300 | return true; 301 | case MAP: 302 | return false; 303 | case BOSS_REWARD: 304 | return false; 305 | case SHOP_SCREEN: 306 | return false; 307 | case GRID: 308 | return isGridScreenConfirmAvailable(); 309 | case HAND_SELECT: 310 | return isHandSelectConfirmButtonEnabled(); 311 | case GAME_OVER: 312 | return true; 313 | case COMPLETE: 314 | return true; 315 | default: 316 | return false; 317 | } 318 | } 319 | 320 | public static boolean isConfirmButtonAvailable() { 321 | return isConfirmButtonAvailable(getCurrentChoiceType()); 322 | } 323 | 324 | private static String getConfirmButtonText(ChoiceType choiceType) { 325 | switch (choiceType) { 326 | case CHEST: 327 | return "proceed"; 328 | case SHOP_ROOM: 329 | return "proceed"; 330 | case REST: 331 | return "proceed"; 332 | case COMBAT_REWARD: 333 | return "proceed"; 334 | case GRID: 335 | return "confirm"; 336 | case HAND_SELECT: 337 | return "confirm"; 338 | case GAME_OVER: 339 | return "proceed"; 340 | case COMPLETE: 341 | return "proceed"; 342 | default: 343 | return "confirm"; 344 | } 345 | } 346 | 347 | public static String getConfirmButtonText() { 348 | return getConfirmButtonText(getCurrentChoiceType()); 349 | } 350 | 351 | public static void pressConfirmButton(ChoiceType choiceType) { 352 | switch (choiceType) { 353 | case CHEST: 354 | clickProceedButton(); 355 | return; 356 | case SHOP_ROOM: 357 | clickProceedButton(); 358 | return; 359 | case REST: 360 | clickProceedButton(); 361 | return; 362 | case COMBAT_REWARD: 363 | clickProceedButton(); 364 | return; 365 | case GRID: 366 | clickGridScreenConfirmButton(); 367 | return; 368 | case HAND_SELECT: 369 | clickHandSelectScreenConfirmButton(); 370 | return; 371 | case GAME_OVER: 372 | clickGameOverReturnButton(); 373 | return; 374 | case COMPLETE: 375 | clickProceedButton(); 376 | } 377 | } 378 | 379 | public static void pressConfirmButton() { 380 | pressConfirmButton(getCurrentChoiceType()); 381 | } 382 | 383 | public static ArrayList getCardRewardScreenChoices() { 384 | ArrayList choices = new ArrayList<>(); 385 | for(AbstractCard card : AbstractDungeon.cardRewardScreen.rewardGroup) { 386 | choices.add(card.name.toLowerCase()); 387 | } 388 | if(isBowlAvailable()) { 389 | choices.add("bowl"); 390 | } 391 | return choices; 392 | } 393 | 394 | public static boolean isBowlAvailable() { 395 | SingingBowlButton bowlButton = (SingingBowlButton) ReflectionHacks.getPrivate(AbstractDungeon.cardRewardScreen, CardRewardScreen.class, "bowlButton"); 396 | return !((boolean) ReflectionHacks.getPrivate(bowlButton, SingingBowlButton.class, "isHidden")); 397 | } 398 | 399 | public static boolean isCardRewardSkipAvailable() { 400 | SkipCardButton skipButton = (SkipCardButton) ReflectionHacks.getPrivate(AbstractDungeon.cardRewardScreen, CardRewardScreen.class, "skipButton"); 401 | return !((boolean) ReflectionHacks.getPrivate(skipButton, SkipCardButton.class, "isHidden")); 402 | } 403 | 404 | public static boolean isCombatRewardCloseAvailable() { 405 | CancelButton cancelButton = AbstractDungeon.overlayMenu.cancelButton; 406 | return !cancelButton.isHidden; 407 | } 408 | 409 | public static void makeCardRewardChoice(int choice) { 410 | ArrayList choices = getCardRewardScreenChoices(); 411 | if(choices.get(choice).equals("bowl")) { 412 | SingingBowlButton bowlButton = (SingingBowlButton) ReflectionHacks.getPrivate(AbstractDungeon.cardRewardScreen, CardRewardScreen.class, "bowlButton"); 413 | bowlButton.onClick(); 414 | AbstractDungeon.cardRewardScreen.closeFromBowlButton(); 415 | AbstractDungeon.closeCurrentScreen(); 416 | } else { 417 | AbstractCard selectedCard = AbstractDungeon.cardRewardScreen.rewardGroup.get(choice); 418 | CardRewardScreenPatch.doHover = true; 419 | CardRewardScreenPatch.hoverCard = selectedCard; 420 | selectedCard.hb.clicked = true; 421 | } 422 | } 423 | 424 | public static ArrayList getHandSelectScreenChoices() { 425 | ArrayList choices = new ArrayList<>(); 426 | HandCardSelectScreen screen = AbstractDungeon.handCardSelectScreen; 427 | if(screen.numCardsToSelect == screen.selectedCards.group.size()) { 428 | return choices; 429 | } 430 | for(AbstractCard card : AbstractDungeon.player.hand.group) { 431 | choices.add(card.name.toLowerCase()); 432 | } 433 | return choices; 434 | } 435 | 436 | public static void makeHandSelectScreenChoice(int choice) { 437 | HandCardSelectScreen screen = AbstractDungeon.handCardSelectScreen; 438 | screen.hoveredCard = AbstractDungeon.player.hand.group.get(choice); 439 | screen.hoveredCard.setAngle(0.0f, false); // This might not be necessary 440 | try { 441 | Method hotkeyCheck = HandCardSelectScreen.class.getDeclaredMethod("selectHoveredCard"); 442 | hotkeyCheck.setAccessible(true); 443 | hotkeyCheck.invoke(screen); 444 | } catch(NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { 445 | e.printStackTrace(); 446 | throw new RuntimeException("selectHoveredCard method somehow can't be called."); 447 | } 448 | } 449 | 450 | private static void clickHandSelectScreenConfirmButton() { 451 | HandCardSelectScreen screen = AbstractDungeon.handCardSelectScreen; 452 | screen.button.hb.clicked = true; 453 | } 454 | 455 | private static boolean isHandSelectConfirmButtonEnabled() { 456 | CardSelectConfirmButton button = AbstractDungeon.handCardSelectScreen.button; 457 | boolean isHidden = (boolean)ReflectionHacks.getPrivate(button, CardSelectConfirmButton.class, "isHidden"); 458 | boolean isDisabled = button.isDisabled; 459 | return !(isHidden || isDisabled); 460 | } 461 | 462 | public static ArrayList getGridScreenCards() { 463 | GridCardSelectScreen screen = AbstractDungeon.gridSelectScreen; 464 | CardGroup cards = (CardGroup) ReflectionHacks.getPrivate(screen, GridCardSelectScreen.class, "targetGroup"); 465 | return cards.group; 466 | } 467 | 468 | public static ArrayList getGridScreenChoices() { 469 | ArrayList choices = new ArrayList<>(); 470 | if(AbstractDungeon.gridSelectScreen.confirmScreenUp || AbstractDungeon.gridSelectScreen.isJustForConfirming) { 471 | return choices; 472 | } 473 | for(AbstractCard card : getGridScreenCards()) { 474 | choices.add(card.name.toLowerCase()); 475 | } 476 | return choices; 477 | } 478 | 479 | public static void makeGridScreenChoice (int choice) { 480 | GridCardSelectScreen screen = AbstractDungeon.gridSelectScreen; 481 | GridCardSelectScreenPatch.hoverCard = getGridScreenCards().get(choice); 482 | GridCardSelectScreenPatch.replaceHoverCard = true; 483 | } 484 | 485 | private static void clickGridScreenConfirmButton() { 486 | GridCardSelectScreen screen = AbstractDungeon.gridSelectScreen; 487 | screen.confirmButton.hb.clicked = true; 488 | if (AbstractDungeon.previousScreen == AbstractDungeon.CurrentScreen.SHOP) { 489 | // The rest of the associated shop purge logic will not run in this update, so we need to block until it does. 490 | GameStateListener.blockStateUpdate(); 491 | } 492 | } 493 | 494 | private static boolean isGridScreenCancelAvailable() { 495 | GridCardSelectScreen screen = AbstractDungeon.gridSelectScreen; 496 | boolean canCancel = (boolean)ReflectionHacks.getPrivate(screen, GridCardSelectScreen.class, "canCancel"); 497 | if(canCancel && (screen.forPurge || screen.forTransform || screen.forUpgrade || (AbstractDungeon.previousScreen == AbstractDungeon.CurrentScreen.SHOP))) { 498 | return true; 499 | } else { 500 | return screen.confirmScreenUp; 501 | } 502 | } 503 | 504 | private static boolean isGridScreenConfirmAvailable() { 505 | GridCardSelectScreen screen = AbstractDungeon.gridSelectScreen; 506 | if (screen.confirmScreenUp || screen.isJustForConfirming) { 507 | return true; 508 | } else if ((!screen.confirmButton.isDisabled) && (!(boolean)ReflectionHacks.getPrivate(screen.confirmButton, GridSelectConfirmButton.class, "isHidden")) ) { 509 | if(screen.forUpgrade || screen.forTransform || screen.forPurge || screen.anyNumber) { 510 | return true; 511 | } 512 | } 513 | return false; 514 | } 515 | 516 | public static ArrayList getCombatRewardScreenChoices() { 517 | ArrayList choices = new ArrayList<>(); 518 | for(RewardItem reward : AbstractDungeon.combatRewardScreen.rewards) { 519 | choices.add(reward.type.name().toLowerCase()); 520 | } 521 | return choices; 522 | } 523 | 524 | public static void makeCombatRewardChoice(int choice) { 525 | RewardItem reward = AbstractDungeon.combatRewardScreen.rewards.get(choice); 526 | reward.isDone = true; 527 | } 528 | 529 | public static ArrayList getBossRewardScreenChoices() { 530 | ArrayList choices = new ArrayList<>(); 531 | for(AbstractRelic relic : AbstractDungeon.bossRelicScreen.relics) { 532 | choices.add(relic.name); 533 | } 534 | return choices; 535 | } 536 | 537 | public static void makeBossRewardChoice(int choice) { 538 | AbstractRelic chosenRelic = AbstractDungeon.bossRelicScreen.relics.get(choice); 539 | AbstractRelicUpdatePatch.doHover = true; 540 | AbstractRelicUpdatePatch.hoverRelic = chosenRelic; 541 | InputHelper.justClickedLeft = true; 542 | } 543 | 544 | public static ArrayList getChestRoomChoices() { 545 | ArrayList choices = new ArrayList<>(); 546 | AbstractChest chest = null; 547 | if (AbstractDungeon.getCurrRoom() instanceof TreasureRoomBoss) { 548 | chest = ((TreasureRoomBoss) AbstractDungeon.getCurrRoom()).chest; 549 | } else if (AbstractDungeon.getCurrRoom() instanceof TreasureRoom) { 550 | chest = ((TreasureRoom) AbstractDungeon.getCurrRoom()).chest; 551 | } 552 | if (chest != null && !chest.isOpen) { 553 | choices.add("open"); 554 | } 555 | return choices; 556 | } 557 | 558 | public static void makeChestRoomChoice (int choice) { 559 | if (AbstractDungeon.getCurrRoom() instanceof TreasureRoomBoss) { 560 | AbstractChest chest = ((TreasureRoomBoss) AbstractDungeon.getCurrRoom()).chest; 561 | chest.isOpen = true; 562 | chest.open(false); 563 | } else if (AbstractDungeon.getCurrRoom() instanceof TreasureRoom) { 564 | AbstractChest chest = ((TreasureRoom) AbstractDungeon.getCurrRoom()).chest; 565 | chest.isOpen = true; 566 | chest.open(false); 567 | } 568 | } 569 | 570 | public static ArrayList getShopRoomChoices() { 571 | ArrayList choices = new ArrayList<>(); 572 | choices.add("shop"); 573 | return choices; 574 | } 575 | 576 | public static void makeShopRoomChoice (int choice) { 577 | MerchantPatch.visitMerchant = true; 578 | } 579 | 580 | public static ArrayList getShopScreenChoices() { 581 | ArrayList choices = new ArrayList<>(); 582 | ArrayList shopItems = getAvailableShopItems(); 583 | for (Object item : shopItems) { 584 | if (item instanceof String) { 585 | choices.add((String) item); 586 | } else if (item instanceof AbstractCard) { 587 | choices.add(((AbstractCard) item).name.toLowerCase()); 588 | } else if (item instanceof StoreRelic) { 589 | choices.add(((StoreRelic)item).relic.name); 590 | } else if (item instanceof StorePotion) { 591 | choices.add(((StorePotion)item).potion.name); 592 | } 593 | } 594 | return choices; 595 | } 596 | 597 | @SuppressWarnings("unchecked") 598 | public static ArrayList getShopScreenCards() { 599 | ArrayList cards = new ArrayList<>(); 600 | ShopScreen screen = AbstractDungeon.shopScreen; 601 | ArrayList coloredCards = (ArrayList) ReflectionHacks.getPrivate(screen, ShopScreen.class, "coloredCards"); 602 | ArrayList colorlessCards = (ArrayList) ReflectionHacks.getPrivate(screen, ShopScreen.class, "colorlessCards"); 603 | cards.addAll(coloredCards); 604 | cards.addAll(colorlessCards); 605 | return cards; 606 | } 607 | 608 | @SuppressWarnings("unchecked") 609 | public static ArrayList getShopScreenRelics() { 610 | ShopScreen screen = AbstractDungeon.shopScreen; 611 | return (ArrayList) ReflectionHacks.getPrivate(screen, ShopScreen.class, "relics"); 612 | } 613 | 614 | @SuppressWarnings("unchecked") 615 | public static ArrayList getShopScreenPotions() { 616 | ShopScreen screen = AbstractDungeon.shopScreen; 617 | return (ArrayList) ReflectionHacks.getPrivate(screen, ShopScreen.class, "potions"); 618 | } 619 | 620 | private static ArrayList getAvailableShopItems() { 621 | ArrayList choices = new ArrayList<>(); 622 | ShopScreen screen = AbstractDungeon.shopScreen; 623 | if(screen.purgeAvailable && AbstractDungeon.player.gold >= ShopScreen.actualPurgeCost) { 624 | choices.add("purge"); 625 | } 626 | for(AbstractCard card : getShopScreenCards()) { 627 | if(card.price <= AbstractDungeon.player.gold) { 628 | choices.add(card); 629 | } 630 | } 631 | for(StoreRelic relic : getShopScreenRelics()) { 632 | if(relic.price <= AbstractDungeon.player.gold) { 633 | choices.add(relic); 634 | } 635 | } 636 | for(StorePotion potion : getShopScreenPotions()) { 637 | if(potion.price <= AbstractDungeon.player.gold) { 638 | choices.add(potion); 639 | } 640 | } 641 | return choices; 642 | } 643 | 644 | public static void makeShopScreenChoice(int choice) { 645 | ArrayList shopItems = getAvailableShopItems(); 646 | Object shopItem = shopItems.get(choice); 647 | if (shopItem instanceof String) { 648 | AbstractDungeon.previousScreen = AbstractDungeon.CurrentScreen.SHOP; 649 | AbstractDungeon.gridSelectScreen.open( 650 | CardGroup.getGroupWithoutBottledCards(AbstractDungeon.player.masterDeck.getPurgeableCards()), 651 | 1, ShopScreen.NAMES[13], false, false, true, true); 652 | } else if (shopItem instanceof AbstractCard) { 653 | AbstractCard card = (AbstractCard)shopItem; 654 | ShopScreenPatch.doHover = true; 655 | ShopScreenPatch.hoverCard = card; 656 | card.hb.clicked = true; 657 | } else if (shopItem instanceof StoreRelic) { 658 | StoreRelic relic = (StoreRelic) shopItem; 659 | relic.relic.hb.clicked = true; 660 | } else if (shopItem instanceof StorePotion) { 661 | StorePotion potion = (StorePotion) shopItem; 662 | potion.potion.hb.clicked = true; 663 | } 664 | } 665 | 666 | private static void clickProceedButton() { 667 | AbstractDungeon.overlayMenu.proceedButton.show(); 668 | Hitbox hb = (Hitbox) ReflectionHacks.getPrivate(AbstractDungeon.overlayMenu.proceedButton, ProceedButton.class, "hb"); 669 | hb.clicked = true; 670 | } 671 | 672 | private static void clickCancelButton() { 673 | AbstractDungeon.overlayMenu.cancelButton.hb.clicked = true; 674 | } 675 | 676 | private static void setCursorPosition(float x, float y) { 677 | Gdx.input.setCursorPosition((int)x, (int)y); 678 | InputHelper.updateFirst(); 679 | } 680 | 681 | public static boolean bossNodeAvailable() { 682 | MapRoomNode currMapNode = AbstractDungeon.getCurrMapNode(); 683 | return (currMapNode.y == 14 || (AbstractDungeon.id.equals(TheEnding.ID) && currMapNode.y == 2)); 684 | } 685 | 686 | public static ArrayList getMapScreenChoices() { 687 | ArrayList choices = new ArrayList<>(); 688 | MapRoomNode currMapNode = AbstractDungeon.getCurrMapNode(); 689 | if(bossNodeAvailable()) { 690 | choices.add("boss"); 691 | return choices; 692 | } 693 | ArrayList availableNodes = getMapScreenNodeChoices(); 694 | for (MapRoomNode node: availableNodes) { 695 | choices.add(String.format("x=%d", node.x).toLowerCase()); 696 | } 697 | return choices; 698 | } 699 | 700 | public static ArrayList getMapScreenNodeChoices() { 701 | ArrayList choices = new ArrayList<>(); 702 | MapRoomNode currMapNode = AbstractDungeon.getCurrMapNode(); 703 | ArrayList> map = AbstractDungeon.map; 704 | if(!AbstractDungeon.firstRoomChosen) { 705 | for(MapRoomNode node : map.get(0)) { 706 | if (node.hasEdges()) { 707 | choices.add(node); 708 | } 709 | } 710 | } else { 711 | for (ArrayList rows : map) { 712 | for (MapRoomNode node : rows) { 713 | if (node.hasEdges()) { 714 | boolean normalConnection = currMapNode.isConnectedTo(node); 715 | boolean wingedConnection = currMapNode.wingedIsConnectedTo(node); 716 | if (normalConnection || wingedConnection) { 717 | choices.add(node); 718 | } 719 | } 720 | } 721 | } 722 | } 723 | return choices; 724 | } 725 | 726 | public static void makeMapChoice(int choice) { 727 | MapRoomNode currMapNode = AbstractDungeon.getCurrMapNode(); 728 | if(currMapNode.y == 14 || (AbstractDungeon.id.equals(TheEnding.ID) && currMapNode.y == 2)) { 729 | if(choice == 0) { 730 | DungeonMapPatch.doBossHover = true; 731 | return; 732 | } else { 733 | throw new IndexOutOfBoundsException("Only a boss node can be chosen here."); 734 | } 735 | } 736 | ArrayList nodeChoices = getMapScreenNodeChoices(); 737 | MapRoomNodeHoverPatch.hoverNode = nodeChoices.get(choice); 738 | MapRoomNodeHoverPatch.doHover = true; 739 | AbstractDungeon.dungeonMapScreen.clicked = true; 740 | } 741 | 742 | public static String getOptionName(String input) { 743 | String unformatted = input.replaceAll("#.|NL", ""); 744 | Pattern regex = Pattern.compile("\\[(.*?)\\]"); 745 | Matcher matcher = regex.matcher(unformatted); 746 | if(matcher.find()) { 747 | return matcher.group(1).trim(); 748 | } else { 749 | return unformatted.trim(); 750 | } 751 | } 752 | 753 | 754 | public static EventDialogType getEventDialogType() { 755 | boolean genericShown = (boolean) ReflectionHacks.getPrivateStatic(GenericEventDialog.class, "show"); 756 | if (genericShown) { 757 | return EventDialogType.IMAGE; 758 | } 759 | boolean roomShown = (boolean) ReflectionHacks.getPrivate(AbstractDungeon.getCurrRoom().event.roomEventText, RoomEventDialog.class, "show"); 760 | if (roomShown) { 761 | return EventDialogType.ROOM; 762 | } else { 763 | return EventDialogType.NONE; 764 | } 765 | } 766 | 767 | public static ArrayList getEventButtons() { 768 | EventDialogType eventType = getEventDialogType(); 769 | switch(eventType) { 770 | case IMAGE: 771 | return AbstractDungeon.getCurrRoom().event.imageEventText.optionList; 772 | case ROOM: 773 | return RoomEventDialog.optionList; 774 | default: 775 | return new ArrayList<>(); 776 | } 777 | } 778 | 779 | public static ArrayList getActiveEventButtons() { 780 | ArrayList buttons = getEventButtons(); 781 | ArrayList activeButtons = new ArrayList<>(); 782 | for(LargeDialogOptionButton button : buttons) { 783 | if(!button.isDisabled) { 784 | activeButtons.add(button); 785 | } 786 | } 787 | return activeButtons; 788 | } 789 | 790 | public static ArrayList getEventScreenChoices() { 791 | ArrayList choiceList = new ArrayList<>(); 792 | ArrayList activeButtons = getActiveEventButtons(); 793 | 794 | if (activeButtons.size() > 0) { 795 | for(LargeDialogOptionButton button : activeButtons) { 796 | choiceList.add(getOptionName(button.msg).toLowerCase()); 797 | } 798 | } else if(AbstractDungeon.getCurrRoom().event instanceof GremlinWheelGame) { 799 | choiceList.add("spin"); 800 | } else if(AbstractDungeon.getCurrRoom().event instanceof GremlinMatchGame) { 801 | ArrayList pickableCards = GremlinMatchGamePatch.getOrderedCards(); 802 | for (AbstractCard c : pickableCards) { 803 | if (GremlinMatchGamePatch.revealedCards.contains(c.uuid)) { 804 | choiceList.add(c.cardID); 805 | } else { 806 | choiceList.add(String.format("card%d", GremlinMatchGamePatch.cardPositions.get(c.uuid))); 807 | } 808 | } 809 | } 810 | return choiceList; 811 | } 812 | 813 | public static void makeEventChoice(int choice) { 814 | ArrayList activeButtons = getActiveEventButtons(); 815 | if (activeButtons.size() > 0) { 816 | activeButtons.get(choice).pressed = true; 817 | } else if (AbstractDungeon.getCurrRoom().event instanceof GremlinWheelGame) { 818 | GremlinWheelGame event = (GremlinWheelGame) AbstractDungeon.getCurrRoom().event; 819 | ReflectionHacks.setPrivate(event, GremlinWheelGame.class, "buttonPressed", true); 820 | CardCrawlGame.sound.play("WHEEL"); 821 | } else if (AbstractDungeon.getCurrRoom().event instanceof GremlinMatchGame) { 822 | ArrayList pickable = GremlinMatchGamePatch.getOrderedCards(); 823 | GremlinMatchGamePatch.HoverCardPatch.hoverCard = pickable.get(choice); 824 | GremlinMatchGamePatch.HoverCardPatch.doHover = true; 825 | } 826 | } 827 | 828 | public static ArrayList getRestRoomChoices() { 829 | ArrayList choiceList = new ArrayList<>(); 830 | ArrayList buttons = getValidRestRoomButtons(); 831 | for(AbstractCampfireOption button : buttons) { 832 | choiceList.add(getCampfireOptionName(button)); 833 | } 834 | return choiceList; 835 | } 836 | 837 | public static void makeRestRoomChoice(int choice_index) { 838 | ArrayList buttons = getValidRestRoomButtons(); 839 | AbstractCampfireOption button = buttons.get(choice_index); 840 | RestRoom room = (RestRoom) AbstractDungeon.getCurrRoom(); 841 | button.useOption(); 842 | room.campfireUI.somethingSelected = true; 843 | } 844 | 845 | private static boolean isRestRoomProceedAvailable() { 846 | return AbstractDungeon.getCurrRoom().phase == AbstractRoom.RoomPhase.COMPLETE; 847 | } 848 | 849 | @SuppressWarnings("unchecked") 850 | private static ArrayList getValidRestRoomButtons() { 851 | ArrayList choiceList = new ArrayList<>(); 852 | RestRoom room = (RestRoom) AbstractDungeon.getCurrRoom(); 853 | if(!isRestRoomProceedAvailable()) { 854 | ArrayList buttons = (ArrayList) ReflectionHacks.getPrivate(room.campfireUI, CampfireUI.class, "buttons"); 855 | for (AbstractCampfireOption button : buttons) { 856 | if (button.usable) { 857 | choiceList.add(button); 858 | } 859 | } 860 | } 861 | return choiceList; 862 | } 863 | 864 | private static String getCampfireOptionName(AbstractCampfireOption option) { 865 | String classname = option.getClass().getSimpleName(); 866 | String nameWithoutOption = classname.substring(0, classname.length() - "Option".length()); 867 | return nameWithoutOption.toLowerCase(); 868 | } 869 | 870 | private static void clickGameOverReturnButton() { 871 | //For now, just copying the functionality from VictoryScreen.update(), always skipping credits 872 | AbstractDungeon.unlocks.clear(); 873 | Settings.isTrial = false; 874 | Settings.isDailyRun = false; 875 | Settings.isEndless = false; 876 | CardCrawlGame.trial = null; 877 | CardCrawlGame.startOver(); 878 | } 879 | 880 | } 881 | -------------------------------------------------------------------------------- /src/main/java/communicationmod/GameStateConverter.java: -------------------------------------------------------------------------------- 1 | package communicationmod; 2 | 3 | import basemod.ReflectionHacks; 4 | import com.google.gson.Gson; 5 | import com.megacrit.cardcrawl.actions.GameActionManager; 6 | import com.megacrit.cardcrawl.cards.AbstractCard; 7 | import com.megacrit.cardcrawl.characters.AbstractPlayer; 8 | import com.megacrit.cardcrawl.core.AbstractCreature; 9 | import com.megacrit.cardcrawl.core.Settings; 10 | import com.megacrit.cardcrawl.dungeons.AbstractDungeon; 11 | import com.megacrit.cardcrawl.events.AbstractEvent; 12 | import com.megacrit.cardcrawl.map.MapEdge; 13 | import com.megacrit.cardcrawl.map.MapRoomNode; 14 | import com.megacrit.cardcrawl.monsters.AbstractMonster; 15 | import com.megacrit.cardcrawl.monsters.EnemyMoveInfo; 16 | import com.megacrit.cardcrawl.neow.NeowEvent; 17 | import com.megacrit.cardcrawl.orbs.AbstractOrb; 18 | import com.megacrit.cardcrawl.potions.AbstractPotion; 19 | import com.megacrit.cardcrawl.potions.PotionSlot; 20 | import com.megacrit.cardcrawl.powers.AbstractPower; 21 | import com.megacrit.cardcrawl.relics.AbstractRelic; 22 | import com.megacrit.cardcrawl.relics.RunicDome; 23 | import com.megacrit.cardcrawl.rewards.RewardItem; 24 | import com.megacrit.cardcrawl.rooms.*; 25 | import com.megacrit.cardcrawl.screens.DeathScreen; 26 | import com.megacrit.cardcrawl.screens.GameOverScreen; 27 | import com.megacrit.cardcrawl.screens.VictoryScreen; 28 | import com.megacrit.cardcrawl.screens.select.GridCardSelectScreen; 29 | import com.megacrit.cardcrawl.shop.ShopScreen; 30 | import com.megacrit.cardcrawl.shop.StorePotion; 31 | import com.megacrit.cardcrawl.shop.StoreRelic; 32 | import com.megacrit.cardcrawl.ui.buttons.LargeDialogOptionButton; 33 | import com.megacrit.cardcrawl.ui.panels.EnergyPanel; 34 | import communicationmod.patches.UpdateBodyTextPatch; 35 | 36 | import java.lang.reflect.Field; 37 | import java.util.ArrayList; 38 | import java.util.HashMap; 39 | 40 | public class GameStateConverter { 41 | 42 | /** 43 | * Creates a JSON representation of the status of CommunicationMod that will be sent to the external process. 44 | * The JSON object returned contains: 45 | * - "available_commands" (list): A list of commands (strings) available to the user 46 | * - "ready_for_command" (boolean): Denotes whether the game state is stable and ready to receive a command 47 | * - "in_game" (boolean): True if in the main menu, False if the player is in the dungeon 48 | * - "game_state" (object): Present if in_game=True, contains the game state object returned by getGameState() 49 | * @return A string containing the JSON representation of CommunicationMod's status 50 | */ 51 | public static String getCommunicationState() { 52 | HashMap response = new HashMap<>(); 53 | response.put("available_commands", CommandExecutor.getAvailableCommands()); 54 | response.put("ready_for_command", GameStateListener.isWaitingForCommand()); 55 | boolean isInGame = CommandExecutor.isInDungeon(); 56 | response.put("in_game", isInGame); 57 | if(isInGame) { 58 | response.put("game_state", getGameState()); 59 | } 60 | Gson gson = new Gson(); 61 | return gson.toJson(response); 62 | } 63 | 64 | 65 | /** 66 | * Creates a JSON representation of the game state, which will be sent to the client. 67 | * Always present: 68 | * - "screen_name" (string): The name of the Enum representing the current screen (defined by Mega Crit) 69 | * - "is_screen_up" (boolean): The game's isScreenUp variable 70 | * - "screen_type" (string): The type of screen (or decision) that the user if facing (defined by Communication Mod) 71 | * - "screen_state" (object): The state of the current state, see getScreenState() (as defined by Communication Mod) 72 | * - "room_phase" (string): The phase of the current room (COMBAT, EVENT, etc.) 73 | * - "action_phase" (string): The phase of the action manager (WAITING_FOR_USER_INPUT, EXECUTING_ACTIONS) 74 | * - "room_type" (string): The name of the class of the current room (ShopRoom, TreasureRoom, MonsterRoom, etc.) 75 | * - "current_hp" (int): The player's current hp 76 | * - "max_hp" (int): The player's maximum hp 77 | * - "floor" (int): The current floor number 78 | * - "act" (int): The current act number 79 | * - "act_boss" (string): The name of the current Act's visible boss encounter 80 | * - "gold" (int): The player's current gold total 81 | * - "seed" (long): The seed used by the current game 82 | * - "class" (string): The player's current class 83 | * - "ascension_level" (int): The ascension level of the current run 84 | * - "relics" (list): A list of the player's current relics 85 | * - "deck" (list): A list of the cards in the player's deck 86 | * - "potions" (list): A list of the player's potions (empty slots are PotionSlots) 87 | * - "map" (list): The current dungeon map 88 | * - "keys" (object): Contains booleans for each of the three keys to reach Act 4 89 | * Sometimes present: 90 | * - "current_action" (list): The class name of the action in the action manager queue, if not empty 91 | * - "combat_state" (list): The state of the combat (draw pile, monsters, etc.) 92 | * - "choice_list" (list): If the command is available, the possible choices for the choose command 93 | * @return A HashMap encoding the JSON representation of the game state 94 | */ 95 | private static HashMap getGameState() { 96 | HashMap state = new HashMap<>(); 97 | 98 | state.put("screen_name", AbstractDungeon.screen.name()); 99 | state.put("is_screen_up", AbstractDungeon.isScreenUp); 100 | state.put("screen_type", ChoiceScreenUtils.getCurrentChoiceType()); 101 | state.put("room_phase", AbstractDungeon.getCurrRoom().phase.toString()); 102 | state.put("action_phase", AbstractDungeon.actionManager.phase.toString()); 103 | if(AbstractDungeon.actionManager.currentAction != null) { 104 | state.put("current_action", AbstractDungeon.actionManager.currentAction.getClass().getSimpleName()); 105 | } 106 | state.put("room_type", AbstractDungeon.getCurrRoom().getClass().getSimpleName()); 107 | state.put("current_hp", AbstractDungeon.player.currentHealth); 108 | state.put("max_hp", AbstractDungeon.player.maxHealth); 109 | state.put("floor", AbstractDungeon.floorNum); 110 | state.put("act", AbstractDungeon.actNum); 111 | state.put("act_boss", AbstractDungeon.bossKey); 112 | state.put("gold", AbstractDungeon.player.gold); 113 | state.put("seed", Settings.seed); 114 | state.put("class", AbstractDungeon.player.chosenClass.name()); 115 | state.put("ascension_level", AbstractDungeon.ascensionLevel); 116 | 117 | ArrayList relics = new ArrayList<>(); 118 | for(AbstractRelic relic : AbstractDungeon.player.relics) { 119 | relics.add(convertRelicToJson(relic)); 120 | } 121 | 122 | state.put("relics", relics); 123 | 124 | ArrayList deck = new ArrayList<>(); 125 | for(AbstractCard card : AbstractDungeon.player.masterDeck.group) { 126 | deck.add(convertCardToJson(card)); 127 | } 128 | 129 | state.put("deck", deck); 130 | 131 | ArrayList potions = new ArrayList<>(); 132 | for(AbstractPotion potion : AbstractDungeon.player.potions) { 133 | potions.add(convertPotionToJson(potion)); 134 | } 135 | 136 | state.put("potions", potions); 137 | 138 | state.put("map", convertMapToJson()); 139 | if(CommandExecutor.isChooseCommandAvailable()) { 140 | state.put("choice_list", ChoiceScreenUtils.getCurrentChoiceList()); 141 | } 142 | if(AbstractDungeon.getCurrRoom().phase.equals(AbstractRoom.RoomPhase.COMBAT)) { 143 | state.put("combat_state", getCombatState()); 144 | } 145 | state.put("screen_state", getScreenState()); 146 | 147 | HashMap keys = new HashMap<>(); 148 | keys.put("ruby", Settings.hasRubyKey); 149 | keys.put("emerald", Settings.hasEmeraldKey); 150 | keys.put("sapphire", Settings.hasSapphireKey); 151 | state.put("keys", keys); 152 | 153 | return state; 154 | } 155 | 156 | private static HashMap getRoomState() { 157 | AbstractRoom currentRoom = AbstractDungeon.getCurrRoom(); 158 | HashMap state = new HashMap<>(); 159 | if(currentRoom instanceof TreasureRoom) { 160 | state.put("chest_type", ((TreasureRoom)currentRoom).chest.getClass().getSimpleName()); 161 | state.put("chest_open", ((TreasureRoom) currentRoom).chest.isOpen); 162 | } else if(currentRoom instanceof TreasureRoomBoss) { 163 | state.put("chest_type", ((TreasureRoomBoss)currentRoom).chest.getClass().getSimpleName()); 164 | state.put("chest_open", ((TreasureRoomBoss) currentRoom).chest.isOpen); 165 | } else if(currentRoom instanceof RestRoom) { 166 | state.put("has_rested", currentRoom.phase == AbstractRoom.RoomPhase.COMPLETE); 167 | state.put("rest_options", ChoiceScreenUtils.getRestRoomChoices()); 168 | } 169 | return state; 170 | } 171 | 172 | /** 173 | * This method removes the special text formatting characters found in the game. 174 | * These extra formatting characters are turned into things like colored or wiggly text in game, but 175 | * we would like to report the text without dealing with these characters. 176 | * @param text The text for which the formatting should be removed 177 | * @return The input text, with the formatting characters removed 178 | */ 179 | private static String removeTextFormatting(String text) { 180 | text = text.replaceAll("~|@(\\S+)~|@", "$1"); 181 | return text.replaceAll("#.|NL", ""); 182 | } 183 | 184 | /** 185 | * The event state object contains: 186 | * "body_text" (string): The current body text for the event, or an empty string if there is none 187 | * "event_name" (string): The name of the event, in the current language 188 | * "event_id" (string): The ID of the event (NOTE: This implementation is sketchy and may not play nice with mods) 189 | * "options" (list): A list of options, in the order they are presented in game. Each option contains: 190 | * - "text" (string): The full text associated with the option (Eg. "[Banana] Heal 10 hp") 191 | * - "disabled" (boolean): Whether the current option or button is disabled. Disabled buttons cannot be chosen 192 | * - "label" (string): The simple label of a button or option (Eg. "Banana") 193 | * - "choice_index" (int): The index of the option for the choose command, if applicable 194 | * @return The event state object 195 | */ 196 | private static HashMap getEventState() { 197 | HashMap state = new HashMap<>(); 198 | ArrayList options = new ArrayList<>(); 199 | ChoiceScreenUtils.EventDialogType eventDialogType = ChoiceScreenUtils.getEventDialogType(); 200 | AbstractEvent event = AbstractDungeon.getCurrRoom().event; 201 | int choice_index = 0; 202 | if (eventDialogType == ChoiceScreenUtils.EventDialogType.IMAGE || eventDialogType == ChoiceScreenUtils.EventDialogType.ROOM) { 203 | for (LargeDialogOptionButton button : ChoiceScreenUtils.getEventButtons()) { 204 | HashMap json_button = new HashMap<>(); 205 | json_button.put("text", removeTextFormatting(button.msg)); 206 | json_button.put("disabled", button.isDisabled); 207 | json_button.put("label", ChoiceScreenUtils.getOptionName(button.msg)); 208 | if (!button.isDisabled) { 209 | json_button.put("choice_index", choice_index); 210 | choice_index += 1; 211 | } 212 | options.add(json_button); 213 | } 214 | state.put("body_text", removeTextFormatting(UpdateBodyTextPatch.bodyText)); 215 | } else { 216 | for (String misc_option : ChoiceScreenUtils.getEventScreenChoices()) { 217 | HashMap json_button = new HashMap<>(); 218 | json_button.put("text", misc_option); 219 | json_button.put("disabled", false); 220 | json_button.put("label", misc_option); 221 | json_button.put("choice_index", choice_index); 222 | choice_index += 1; 223 | options.add(json_button); 224 | } 225 | state.put("body_text", ""); 226 | } 227 | state.put("event_name", ReflectionHacks.getPrivateStatic(event.getClass(), "NAME")); 228 | if (event instanceof NeowEvent) { 229 | state.put("event_id", "Neow Event"); 230 | } else { 231 | try { 232 | // AbstractEvent does not have a static "ID" field, but all of the events in the base game do. 233 | Field targetField = event.getClass().getDeclaredField("ID"); 234 | state.put("event_id", (String)targetField.get(null)); 235 | } catch (NoSuchFieldException | IllegalAccessException e) { 236 | state.put("event_id", ""); 237 | } 238 | state.put("event_id", ReflectionHacks.getPrivateStatic(event.getClass(), "ID")); 239 | } 240 | state.put("options", options); 241 | return state; 242 | } 243 | 244 | /** 245 | * The card reward state object contains: 246 | * "bowl_available" (boolean): Whether the Singing Bowl button is present 247 | * "skip_available" (boolean): Whether the card reward is skippable 248 | * "cards" (list): The list of cards that can be chosen 249 | * @return The card reward state object 250 | */ 251 | private static HashMap getCardRewardState() { 252 | HashMap state = new HashMap<>(); 253 | state.put("bowl_available", ChoiceScreenUtils.isBowlAvailable()); 254 | state.put("skip_available", ChoiceScreenUtils.isCardRewardSkipAvailable()); 255 | ArrayList cardRewardJson = new ArrayList<>(); 256 | for(AbstractCard card : AbstractDungeon.cardRewardScreen.rewardGroup) { 257 | cardRewardJson.add(convertCardToJson(card)); 258 | } 259 | state.put("cards", cardRewardJson); 260 | return state; 261 | } 262 | 263 | /** 264 | * The combat reward screen state object contains: 265 | * "rewards" (list): A list of reward objects, each of which contains: 266 | * - "reward_type" (string): The name of the RewardItem.RewardType enum for the reward 267 | * - "gold" (int): The amount of gold in the reward, if applicable 268 | * - "relic" (object): The relic in the reward, if applicable 269 | * - "potion" (object): The potion in the reward, if applicable 270 | * - "link" (object): The relic that the sapphire key is linked to, if applicable 271 | * @return The combat reward screen state object 272 | */ 273 | private static HashMap getCombatRewardState() { 274 | HashMap state = new HashMap<>(); 275 | ArrayList rewards = new ArrayList<>(); 276 | for(RewardItem reward : AbstractDungeon.combatRewardScreen.rewards) { 277 | HashMap jsonReward = new HashMap<>(); 278 | jsonReward.put("reward_type", reward.type.name()); 279 | switch(reward.type) { 280 | case GOLD: 281 | case STOLEN_GOLD: 282 | jsonReward.put("gold", reward.goldAmt + reward.bonusGold); 283 | break; 284 | case RELIC: 285 | jsonReward.put("relic", convertRelicToJson(reward.relic)); 286 | break; 287 | case POTION: 288 | jsonReward.put("potion", convertPotionToJson(reward.potion)); 289 | break; 290 | case SAPPHIRE_KEY: 291 | jsonReward.put("link", convertRelicToJson(reward.relicLink.relic)); 292 | } 293 | rewards.add(jsonReward); 294 | } 295 | state.put("rewards", rewards); 296 | return state; 297 | } 298 | 299 | /** 300 | * The map screen state object contains: 301 | * "current_node" (object): The node object for the currently selected node, if applicable 302 | * "next_nodes" (list): A list of nodes that can be chosen next 303 | * "first_node_chosen" (boolean): Whether the first node in the act has already been chosen 304 | * "boss_available" (boolean): Whether the next node choice is a boss 305 | * @return The map screen state object 306 | */ 307 | private static HashMap getMapScreenState() { 308 | HashMap state = new HashMap<>(); 309 | if (AbstractDungeon.getCurrMapNode() != null) { 310 | state.put("current_node", convertMapRoomNodeToJson(AbstractDungeon.getCurrMapNode())); 311 | } 312 | ArrayList nextNodesJson = new ArrayList<>(); 313 | for(MapRoomNode node : ChoiceScreenUtils.getMapScreenNodeChoices()) { 314 | nextNodesJson.add(convertMapRoomNodeToJson(node)); 315 | } 316 | state.put("next_nodes", nextNodesJson); 317 | state.put("first_node_chosen", AbstractDungeon.firstRoomChosen); 318 | state.put("boss_available", ChoiceScreenUtils.bossNodeAvailable()); 319 | return state; 320 | } 321 | 322 | /** 323 | * The boss reward screen state contains: 324 | * "relics" (list): A list of relics that can be chosen from the boss 325 | * Note: Blights are not supported. 326 | * @return The boss reward screen state object 327 | */ 328 | private static HashMap getBossRewardState() { 329 | HashMap state = new HashMap<>(); 330 | ArrayList bossRelics = new ArrayList<>(); 331 | for(AbstractRelic relic : AbstractDungeon.bossRelicScreen.relics) { 332 | bossRelics.add(convertRelicToJson(relic)); 333 | } 334 | state.put("relics", bossRelics); 335 | return state; 336 | } 337 | 338 | /** 339 | * The shop screen state contains: 340 | * "cards" (list): A list of cards available to buy 341 | * "relics" (list): A list of relics available to buy 342 | * "potions" (list): A list of potions available to buy 343 | * "purge_available" (boolean): Whether the card remove option is available 344 | * "purge_cost" (int): The cost of the card remove option 345 | * @return The shop screen state object 346 | */ 347 | private static HashMap getShopScreenState() { 348 | HashMap state = new HashMap<>(); 349 | ArrayList shopCards = new ArrayList<>(); 350 | ArrayList shopRelics = new ArrayList<>(); 351 | ArrayList shopPotions = new ArrayList<>(); 352 | for(AbstractCard card : ChoiceScreenUtils.getShopScreenCards()) { 353 | HashMap jsonCard = convertCardToJson(card); 354 | jsonCard.put("price", card.price); 355 | shopCards.add(jsonCard); 356 | } 357 | for(StoreRelic relic : ChoiceScreenUtils.getShopScreenRelics()) { 358 | HashMap jsonRelic = convertRelicToJson(relic.relic); 359 | jsonRelic.put("price", relic.price); 360 | shopRelics.add(jsonRelic); 361 | } 362 | for(StorePotion potion : ChoiceScreenUtils.getShopScreenPotions()) { 363 | HashMap jsonPotion = convertPotionToJson(potion.potion); 364 | jsonPotion.put("price", potion.price); 365 | shopPotions.add(jsonPotion); 366 | } 367 | state.put("cards", shopCards); 368 | state.put("relics", shopRelics); 369 | state.put("potions", shopPotions); 370 | state.put("purge_available", AbstractDungeon.shopScreen.purgeAvailable); 371 | state.put("purge_cost", ShopScreen.actualPurgeCost); 372 | return state; 373 | } 374 | 375 | /** 376 | * The grid select screen state contains: 377 | * "cards" (list): The list of cards available to pick, including selected cards 378 | * "selected_cards" (list): The list of cards that are currently selected 379 | * "num_cards" (int): The number of cards that must be selected 380 | * "any_number" (boolean): Whether any number of cards can be selected 381 | * "for_upgrade" (boolean): Whether the selected cards will be upgraded 382 | * "for_transform" (boolean): Whether the selected cards will be transformed 383 | * _for_purge" (boolean): Whether the selected cards will be removed from the deck 384 | * "confirm_up" (boolean): Whether the confirm screen is up, and cards cannot be selected 385 | * @return The grid select screen state object 386 | */ 387 | private static HashMap getGridState() { 388 | HashMap state = new HashMap<>(); 389 | ArrayList gridJson = new ArrayList<>(); 390 | ArrayList gridSelectedJson = new ArrayList<>(); 391 | ArrayList gridCards = ChoiceScreenUtils.getGridScreenCards(); 392 | GridCardSelectScreen screen = AbstractDungeon.gridSelectScreen; 393 | for(AbstractCard card : gridCards) { 394 | gridJson.add(convertCardToJson(card)); 395 | } 396 | for(AbstractCard card : screen.selectedCards) { 397 | gridSelectedJson.add(convertCardToJson(card)); 398 | } 399 | int numCards = (int) ReflectionHacks.getPrivate(screen, GridCardSelectScreen.class, "numCards"); 400 | boolean forUpgrade = (boolean) ReflectionHacks.getPrivate(screen, GridCardSelectScreen.class, "forUpgrade"); 401 | boolean forTransform = (boolean) ReflectionHacks.getPrivate(screen, GridCardSelectScreen.class, "forTransform"); 402 | boolean forPurge = (boolean) ReflectionHacks.getPrivate(screen, GridCardSelectScreen.class, "forPurge"); 403 | state.put("cards", gridJson); 404 | state.put("selected_cards", gridSelectedJson); 405 | state.put("num_cards", numCards); 406 | state.put("any_number", screen.anyNumber); 407 | state.put("for_upgrade", forUpgrade); 408 | state.put("for_transform", forTransform); 409 | state.put("for_purge", forPurge); 410 | state.put("confirm_up", screen.confirmScreenUp || screen.isJustForConfirming); 411 | return state; 412 | } 413 | 414 | /** 415 | * The hand select screen state contains: 416 | * "hand" (list): The list of cards currently in your hand, not including selected cards 417 | * "selected" (list): The list of currently selected cards 418 | * "max_cards" (int): The maximum number of cards that can be selected 419 | * "can_pick_zero" (boolean): Whether zero cards can be selected 420 | * @return The hand select screen state object 421 | */ 422 | private static HashMap getHandSelectState() { 423 | HashMap state = new HashMap<>(); 424 | ArrayList handJson = new ArrayList<>(); 425 | ArrayList selectedJson = new ArrayList<>(); 426 | ArrayList handCards = AbstractDungeon.player.hand.group; 427 | // As far as I can tell, this comment is a Java 8 analogue of a Python list comprehension? I think just looping is more readable. 428 | // handJson = handCards.stream().map(GameStateConverter::convertCardToJson).collect(Collectors.toCollection(ArrayList::new)); 429 | for(AbstractCard card : handCards) { 430 | handJson.add(convertCardToJson(card)); 431 | } 432 | state.put("hand", handJson); 433 | ArrayList selectedCards = AbstractDungeon.handCardSelectScreen.selectedCards.group; 434 | for(AbstractCard card : selectedCards) { 435 | selectedJson.add(convertCardToJson(card)); 436 | } 437 | state.put("selected", selectedJson); 438 | state.put("max_cards", AbstractDungeon.handCardSelectScreen.numCardsToSelect); 439 | state.put("can_pick_zero", AbstractDungeon.handCardSelectScreen.canPickZero); 440 | return state; 441 | } 442 | 443 | /** 444 | * The game over screen state contains: 445 | * "score" (int): Your final score 446 | * "victory" (boolean): Whether you won 447 | * @return The game over screen state object 448 | */ 449 | private static HashMap getGameOverState() { 450 | HashMap state = new HashMap<>(); 451 | int score = 0; 452 | boolean victory = false; 453 | if(AbstractDungeon.screen == AbstractDungeon.CurrentScreen.DEATH) { 454 | score = (int) ReflectionHacks.getPrivate(AbstractDungeon.deathScreen, GameOverScreen.class, "score"); 455 | victory = GameOverScreen.isVictory; 456 | } else if(AbstractDungeon.screen == AbstractDungeon.CurrentScreen.VICTORY) { 457 | score = (int) ReflectionHacks.getPrivate(AbstractDungeon.victoryScreen, GameOverScreen.class, "score"); 458 | victory = true; 459 | } 460 | state.put("score", score); 461 | state.put("victory", victory); 462 | return state; 463 | } 464 | 465 | /** 466 | * Gets the appropriate screen state object 467 | * @return An object containing your current screen state 468 | */ 469 | private static HashMap getScreenState() { 470 | ChoiceScreenUtils.ChoiceType screenType = ChoiceScreenUtils.getCurrentChoiceType(); 471 | switch (screenType) { 472 | case EVENT: 473 | return getEventState(); 474 | case CHEST: 475 | case REST: 476 | return getRoomState(); 477 | case CARD_REWARD: 478 | return getCardRewardState(); 479 | case COMBAT_REWARD: 480 | return getCombatRewardState(); 481 | case MAP: 482 | return getMapScreenState(); 483 | case BOSS_REWARD: 484 | return getBossRewardState(); 485 | case SHOP_SCREEN: 486 | return getShopScreenState(); 487 | case GRID: 488 | return getGridState(); 489 | case HAND_SELECT: 490 | return getHandSelectState(); 491 | case GAME_OVER: 492 | return getGameOverState(); 493 | } 494 | return new HashMap<>(); 495 | } 496 | 497 | /** 498 | * Gets the state of the current combat in game. 499 | * The combat state object contains: 500 | * "draw_pile" (list): The list of cards in your draw pile 501 | * "discard_pile" (list): The list of cards in your discard pile 502 | * "exhaust_pile" (list): The list of cards in your exhaust pile 503 | * "hand" (list): The list of cards in your hand 504 | * "limbo" (list): The list of cards that are in 'limbo', which is used for a variety of effects in game. 505 | * "card_in_play" (object, optional): The card that is currently in play, if any. 506 | * "player" (object): The state of the player 507 | * "monsters" (list): A list of the enemies in the combat, including dead enemies 508 | * "turn" (int): The current turn (or round) number of the combat. 509 | * "cards_discarded_this_turn" (int): The number of cards discarded this turn. 510 | * "times_damaged" (int): The number of times the player has been damaged this combat (for Blood for Blood). 511 | * Note: The order of the draw pile is not currently randomized when sent to the client. 512 | * @return The combat state object 513 | */ 514 | private static HashMap getCombatState() { 515 | HashMap state = new HashMap<>(); 516 | ArrayList monsters = new ArrayList<>(); 517 | for(AbstractMonster monster : AbstractDungeon.getCurrRoom().monsters.monsters) { 518 | monsters.add(convertMonsterToJson(monster)); 519 | } 520 | state.put("monsters", monsters); 521 | ArrayList draw_pile = new ArrayList<>(); 522 | for(AbstractCard card : AbstractDungeon.player.drawPile.group) { 523 | draw_pile.add(convertCardToJson(card)); 524 | } 525 | ArrayList discard_pile = new ArrayList<>(); 526 | for(AbstractCard card : AbstractDungeon.player.discardPile.group) { 527 | discard_pile.add(convertCardToJson(card)); 528 | } 529 | ArrayList exhaust_pile = new ArrayList<>(); 530 | for(AbstractCard card : AbstractDungeon.player.exhaustPile.group) { 531 | exhaust_pile.add(convertCardToJson(card)); 532 | } 533 | ArrayList hand = new ArrayList<>(); 534 | for(AbstractCard card : AbstractDungeon.player.hand.group) { 535 | hand.add(convertCardToJson(card)); 536 | } 537 | ArrayList limbo = new ArrayList<>(); 538 | for(AbstractCard card : AbstractDungeon.player.limbo.group) { 539 | limbo.add(convertCardToJson(card)); 540 | } 541 | state.put("draw_pile", draw_pile); 542 | state.put("discard_pile", discard_pile); 543 | state.put("exhaust_pile", exhaust_pile); 544 | state.put("hand", hand); 545 | state.put("limbo", limbo); 546 | if (AbstractDungeon.player.cardInUse != null) { 547 | state.put("card_in_play", convertCardToJson(AbstractDungeon.player.cardInUse)); 548 | } 549 | state.put("player", convertPlayerToJson(AbstractDungeon.player)); 550 | state.put("turn", GameActionManager.turn); 551 | state.put("cards_discarded_this_turn", GameActionManager.totalDiscardedThisTurn); 552 | state.put("times_damaged", AbstractDungeon.player.damagedThisCombat); 553 | return state; 554 | } 555 | 556 | /** 557 | * Creates a GSON-compatible representation of the game map 558 | * The map object is a list of nodes, each of which with two extra fields: 559 | * "parents" (list): Not implemented 560 | * "children" (list): The nodes connected by an edge out of the node in question 561 | * @return A list of node objects 562 | */ 563 | private static ArrayList convertMapToJson() { 564 | ArrayList> map = AbstractDungeon.map; 565 | ArrayList jsonMap = new ArrayList<>(); 566 | for(ArrayList layer : map) { 567 | for(MapRoomNode node : layer) { 568 | if(node.hasEdges()) { 569 | HashMap json_node = convertMapRoomNodeToJson(node); 570 | ArrayList json_children = new ArrayList<>(); 571 | ArrayList json_parents = new ArrayList<>(); 572 | for(MapEdge edge : node.getEdges()) { 573 | if (edge.srcX == node.x && edge.srcY == node.y) { 574 | json_children.add(convertCoordinatesToJson(edge.dstX, edge.dstY)); 575 | } else { 576 | json_parents.add(convertCoordinatesToJson(edge.srcX, edge.srcY)); 577 | } 578 | } 579 | 580 | json_node.put("parents", json_parents); 581 | json_node.put("children", json_children); 582 | jsonMap.add(json_node); 583 | } 584 | } 585 | } 586 | return jsonMap; 587 | } 588 | 589 | private static HashMap convertCoordinatesToJson(int x, int y) { 590 | HashMap jsonNode = new HashMap<>(); 591 | jsonNode.put("x", x); 592 | jsonNode.put("y", y); 593 | return jsonNode; 594 | } 595 | 596 | /** 597 | * Creates a GSON-compatible representation of the given node 598 | * The node object contains: 599 | * "x" (int): The node's x coordinate 600 | * "y" (int): The node's y coordinate 601 | * "symbol" (string, optional): The map symbol for the node (?, $, T, M, E, R) 602 | * "children" (list, optional): The nodes connected by an edge out of the provided node 603 | * Note: children are added by convertMapToJson() 604 | * @param node The node to convert 605 | * @return A node object 606 | */ 607 | private static HashMap convertMapRoomNodeToJson(MapRoomNode node) { 608 | HashMap jsonNode = convertCoordinatesToJson(node.x, node.y); 609 | jsonNode.put("symbol", node.getRoomSymbol(true)); 610 | return jsonNode; 611 | } 612 | 613 | /** 614 | * Creates a GSON-compatible representation of the given cards 615 | * The card object contains: 616 | * "name" (string): The name of the card, in the currently selected language 617 | * "uuid" (string): The unique identifier of the card 618 | * "misc" (int): The misc field for the card, used by cards like Ritual Dagger 619 | * "is_playable" (boolean): Whether the card can currently be played, though does not guarantee a target 620 | * "cost" (int): The current cost of the card. -2 is unplayable and -1 is X cost 621 | * "upgrades" (int): The number of times the card is upgraded 622 | * "id" (string): The id of the card 623 | * "type" (string): The name of the AbstractCard.CardType enum for the card 624 | * "rarity" (string): The name of the AbstractCard.CardRarity enum for the card 625 | * "has_target" (boolean): Whether the card requires a target to be played 626 | * "exhausts" (boolean): Whether the card exhausts when played 627 | * "ethereal" (boolean): Whether the card is ethereal 628 | * @param card The card to convert 629 | * @return A card object 630 | */ 631 | private static HashMap convertCardToJson(AbstractCard card) { 632 | HashMap jsonCard = new HashMap<>(); 633 | jsonCard.put("name", card.name); 634 | jsonCard.put("uuid", card.uuid.toString()); 635 | if(card.misc != 0) { 636 | jsonCard.put("misc", card.misc); 637 | } 638 | if(AbstractDungeon.getMonsters() != null) { 639 | jsonCard.put("is_playable", card.canUse(AbstractDungeon.player, null)); 640 | } 641 | jsonCard.put("cost", card.costForTurn); 642 | jsonCard.put("upgrades", card.timesUpgraded); 643 | jsonCard.put("id", card.cardID); 644 | jsonCard.put("type", card.type.name()); 645 | jsonCard.put("rarity", card.rarity.name()); 646 | jsonCard.put("has_target", card.target== AbstractCard.CardTarget.SELF_AND_ENEMY || card.target == AbstractCard.CardTarget.ENEMY); 647 | jsonCard.put("exhausts", card.exhaust); 648 | jsonCard.put("ethereal", card.isEthereal); 649 | return jsonCard; 650 | } 651 | 652 | /** 653 | * Creates a GSON-compatible representation of the given monster 654 | * The monster object contains: 655 | * "name" (string): The monster's name, in the currently selected language 656 | * "id" (string): The monster's id 657 | * "current_hp" (int): The monster's current hp 658 | * "max_hp" (int): The monster's maximum hp 659 | * "block" (int): The monster's current block 660 | * "intent" (string): The name of the AbstractMonster.Intent enum for the monster's current intent 661 | * "move_id" (int): The move id byte for the monster's current move 662 | * "move_base_damage" (int): The base damage for the monster's current attack 663 | * "move_adjusted_damage" (int): The damage number actually shown on the intent for the monster's current attack 664 | * "move_hits" (int): The number of hits done by the current attack 665 | * "last_move_id" (int): The move id byte for the monster's previous move 666 | * "second_last_move_id" (int): The move id byte from 2 moves ago 667 | * "half_dead" (boolean): Whether the monster is half dead 668 | * "is_gone" (boolean): Whether the monster is dead or has run away 669 | * "powers" (list): The monster's current powers 670 | * Note: If the player has Runic Dome, intent will always return NONE 671 | * @param monster The monster to convert 672 | * @return A monster object 673 | */ 674 | private static HashMap convertMonsterToJson(AbstractMonster monster) { 675 | HashMap jsonMonster = new HashMap<>(); 676 | jsonMonster.put("id", monster.id); 677 | jsonMonster.put("name", monster.name); 678 | jsonMonster.put("current_hp", monster.currentHealth); 679 | jsonMonster.put("max_hp", monster.maxHealth); 680 | if (AbstractDungeon.player.hasRelic(RunicDome.ID)) { 681 | jsonMonster.put("intent", AbstractMonster.Intent.NONE); 682 | } else { 683 | jsonMonster.put("intent", monster.intent.name()); 684 | EnemyMoveInfo moveInfo = (EnemyMoveInfo)ReflectionHacks.getPrivate(monster, AbstractMonster.class, "move"); 685 | if (moveInfo != null) { 686 | jsonMonster.put("move_id", moveInfo.nextMove); 687 | jsonMonster.put("move_base_damage", moveInfo.baseDamage); 688 | int intentDmg = (int)ReflectionHacks.getPrivate(monster, AbstractMonster.class, "intentDmg"); 689 | if (moveInfo.baseDamage > 0) { 690 | jsonMonster.put("move_adjusted_damage", intentDmg); 691 | } else { 692 | jsonMonster.put("move_adjusted_damage", moveInfo.baseDamage); 693 | } 694 | int move_hits = moveInfo.multiplier; 695 | // If isMultiDamage is not set, the multiplier is probably 0, but there is really 1 attack. 696 | if (!moveInfo.isMultiDamage) { 697 | move_hits = 1; 698 | } 699 | jsonMonster.put("move_hits", move_hits); 700 | } 701 | } 702 | if(monster.moveHistory.size() >= 2) { 703 | jsonMonster.put("last_move_id", monster.moveHistory.get(monster.moveHistory.size() - 2)); 704 | } 705 | if(monster.moveHistory.size() >= 3) { 706 | jsonMonster.put("second_last_move_id", monster.moveHistory.get(monster.moveHistory.size() - 3)); 707 | } 708 | jsonMonster.put("half_dead", monster.halfDead); 709 | jsonMonster.put("is_gone", monster.isDeadOrEscaped()); 710 | jsonMonster.put("block", monster.currentBlock); 711 | jsonMonster.put("powers", convertCreaturePowersToJson(monster)); 712 | return jsonMonster; 713 | } 714 | 715 | /** 716 | * Creates a GSON-compatible representation of the given player 717 | * The player object contains: 718 | * "max_hp" (int): The player's maximum hp 719 | * "current_hp" (int): The player's current hp 720 | * "block" (int): The player's current block 721 | * "powers" (list): The player's current powers 722 | * "energy" (int): The player's current energy 723 | * "orbs" (list): The player's current orb slots 724 | * Note: many other things, like draw pile and discard pile, are in the combat state 725 | * @param player The player to convert 726 | * @return A player object 727 | */ 728 | private static HashMap convertPlayerToJson(AbstractPlayer player) { 729 | HashMap jsonPlayer = new HashMap<>(); 730 | jsonPlayer.put("max_hp", player.maxHealth); 731 | jsonPlayer.put("current_hp", player.currentHealth); 732 | jsonPlayer.put("powers", convertCreaturePowersToJson(player)); 733 | jsonPlayer.put("energy", EnergyPanel.totalCount); 734 | jsonPlayer.put("block", player.currentBlock); 735 | ArrayList orbs = new ArrayList<>(); 736 | for(AbstractOrb orb : player.orbs) { 737 | orbs.add(convertOrbToJson(orb)); 738 | } 739 | jsonPlayer.put("orbs", orbs); 740 | return jsonPlayer; 741 | } 742 | 743 | /** 744 | * Checks whether the given object has the specified field. If so, returns the field's value. Else returns null. 745 | * @param object The object used to look for the specified field 746 | * @param fieldName The field that we want to access 747 | * @return The value of the field, if present, or else null. 748 | */ 749 | private static Object getFieldIfExists(Object object, String fieldName) { 750 | Class objectClass = object.getClass(); 751 | for (Field field : objectClass.getDeclaredFields()) { 752 | if (field.getName().equals(fieldName)) { 753 | try { 754 | field.setAccessible(true); 755 | return field.get(object); 756 | } catch(IllegalAccessException e) { 757 | e.printStackTrace(); 758 | return null; 759 | } 760 | } 761 | } 762 | return null; 763 | } 764 | 765 | /** 766 | * Creates a GSON-compatible representation of the given creature's powers 767 | * The power object contains: 768 | * "id" (string): The id of the power 769 | * "name" (string): The name of the power, in the currently selected language 770 | * "amount" (int): The amount of the power 771 | * "damage" (int, optional): The amount of damage the power does, if applicable 772 | * "card" (object, optional): The card associated with the power (for powers like Nightmare) 773 | * "misc" (int, optional): Contains misc values that don't fit elsewhere (such as the base value for Flight) 774 | * "just_applied" (boolean, optional): Used with many powers to prevent them from expiring immediately 775 | * @param creature The creature whose powers are to be converted 776 | * @return A list of power objects 777 | */ 778 | private static ArrayList convertCreaturePowersToJson(AbstractCreature creature) { 779 | ArrayList powers = new ArrayList<>(); 780 | for(AbstractPower power : creature.powers) { 781 | HashMap json_power = new HashMap<>(); 782 | json_power.put("id", power.ID); 783 | json_power.put("name", power.name); 784 | json_power.put("amount", power.amount); 785 | Object damage = getFieldIfExists(power, "damage"); 786 | if (damage != null) { 787 | json_power.put("damage", (int)damage); 788 | } 789 | Object card = getFieldIfExists(power, "card"); 790 | if (card != null) { 791 | json_power.put("card", convertCardToJson((AbstractCard)card)); 792 | } 793 | String[] miscFieldNames = { 794 | "basePower", "maxAmt", "storedAmount", "hpLoss", "cardsDoubledThisTurn" 795 | }; 796 | // basePower gives the base power for Malleable 797 | // maxAmt gives the max amount of damage per turn for Invincible 798 | // storedAmount gives the number of stacks per turn for Flight 799 | // hpLoss gives the amount of HP lost per turn with Combust 800 | // cardsDoubledThisTurn gives the number of cards already doubled with Echo Form 801 | Object misc = null; 802 | for (String fieldName : miscFieldNames) { 803 | misc = getFieldIfExists(power, fieldName); 804 | if (misc != null) { 805 | json_power.put("misc", (int)misc); 806 | break; 807 | } 808 | } 809 | 810 | String[] justAppliedNames = { 811 | "justApplied", "skipFirst" 812 | }; 813 | // justApplied is used with a variety of powers to prevent them from expiring immediately (cast from bool) 814 | // skipFirst is the same as justApplied, for the Ritual power 815 | Object justApplied = null; 816 | for (String fieldName : justAppliedNames) { 817 | justApplied = getFieldIfExists(power, fieldName); 818 | if (justApplied != null) { 819 | json_power.put("just_applied", (boolean)justApplied); 820 | break; 821 | } 822 | } 823 | 824 | powers.add(json_power); 825 | } 826 | return powers; 827 | } 828 | 829 | /** 830 | * Creates a GSON-compatible representation of the given relic 831 | * The relic object contains: 832 | * "id" (string): The id of the relic 833 | * "name" (string): The name of the relic, in the currently selected language 834 | * "counter" (int): The counter on the relic 835 | * @param relic The relic to convert 836 | * @return A relic object 837 | */ 838 | private static HashMap convertRelicToJson(AbstractRelic relic) { 839 | HashMap jsonRelic = new HashMap<>(); 840 | jsonRelic.put("id", relic.relicId); 841 | jsonRelic.put("name", relic.name); 842 | jsonRelic.put("counter", relic.counter); 843 | return jsonRelic; 844 | } 845 | 846 | /** 847 | * Creates a GSON-compatible representation of the given potion 848 | * The potion object contains: 849 | * "id" (string): The id of the potion 850 | * "name" (string): The name of the potion, in the currently selected language 851 | * "can_use" (boolean): Whether the potion can currently be used 852 | * "can_discard" (boolean): Whether the potion can currently be discarded 853 | * "requires_target" (boolean): Whether the potion must be used with a target 854 | * @param potion The potion to convert 855 | * @return A potion object 856 | */ 857 | private static HashMap convertPotionToJson(AbstractPotion potion) { 858 | HashMap jsonPotion = new HashMap<>(); 859 | jsonPotion.put("id", potion.ID); 860 | jsonPotion.put("name", potion.name); 861 | boolean canUse = potion.canUse(); 862 | boolean canDiscard = potion.canDiscard(); 863 | if (potion instanceof PotionSlot) { 864 | canDiscard = canUse = false; 865 | } 866 | jsonPotion.put("can_use", canUse); 867 | jsonPotion.put("can_discard", canDiscard); 868 | jsonPotion.put("requires_target", potion.isThrown); 869 | return jsonPotion; 870 | } 871 | 872 | /** 873 | * Creates a GSON-compatible representation of the given orb 874 | * The orb object contains: 875 | * "id" (string): The id of the orb 876 | * "name" (string): The name of the orb, in the currently selected language 877 | * "evoke_amount" (int): The evoke amount of the orb 878 | * "passive_amount" (int): The passive amount of the orb 879 | * @param orb The orb to convert 880 | * @return An orb object 881 | */ 882 | private static HashMap convertOrbToJson(AbstractOrb orb) { 883 | HashMap jsonOrb = new HashMap<>(); 884 | jsonOrb.put("id", orb.ID); 885 | jsonOrb.put("name", orb.name); 886 | jsonOrb.put("evoke_amount", orb.evokeAmount); 887 | jsonOrb.put("passive_amount", orb.passiveAmount); 888 | return jsonOrb; 889 | } 890 | 891 | } 892 | --------------------------------------------------------------------------------