diff --git a/src/main/java/ti4/game/Player.java b/src/main/java/ti4/game/Player.java index 57e44edc2c..7e6236a896 100644 --- a/src/main/java/ti4/game/Player.java +++ b/src/main/java/ti4/game/Player.java @@ -107,6 +107,7 @@ import ti4.service.leader.CommanderUnlockCheckService; import ti4.service.map.FractureService; import ti4.service.strategycard.PlayStrategyCardService; +import ti4.service.transaction.TransactionItem; import ti4.service.turn.EndTurnService; import ti4.service.turn.StartTurnService; import ti4.service.unit.CheckUnitContainmentService; @@ -373,8 +374,8 @@ public int getSpentTgsThisWindow() { return 0; } - public List getTransactionItemsWithPlayer(Player player) { - List transactionItemsWithPlayer = new ArrayList<>(); + public List getTransactionItemsWithPlayer(Player player) { + List transactionItemsWithPlayer = new ArrayList<>(); List players = new ArrayList<>(); players.add(this); players.add(player); @@ -383,9 +384,8 @@ public List getTransactionItemsWithPlayer(Player player) { if (sender == player) { receiver = this; } - for (String item : getTransactionItems()) { - if (item.contains("sending" + sender.getFaction()) - && item.contains("receiving" + receiver.getFaction())) { + for (TransactionItem item : getTransactionItems()) { + if (item.isSentFromTo(sender.getFaction(), receiver.getFaction())) { transactionItemsWithPlayer.add(item); } } @@ -395,9 +395,9 @@ public List getTransactionItemsWithPlayer(Player player) { public void clearTransactionItemsWithPlayer(Player player) { List newTransactionItems = new ArrayList<>(); - for (String item : getTransactionItems()) { - if (!item.contains("ing" + player.getFaction())) { - newTransactionItems.add(item); + for (TransactionItem item : getTransactionItems()) { + if (!item.involves(player.getFaction())) { + newTransactionItems.add(item.serialize()); } } game.setStoredValue(player.getFaction() + "NothingMessage", ""); @@ -406,7 +406,14 @@ public void clearTransactionItemsWithPlayer(Player player) { } public void addTransactionItem(String thing) { - getTransactionItems().add(thing); + TransactionItem transactionItem = TransactionItem.parse(thing); + if (transactionItem != null) { + addTransactionItem(transactionItem); + } + } + + public void addTransactionItem(TransactionItem item) { + getSerializedTransactionItems().add(item.serialize()); } public void addBreakthrough(String bt) { diff --git a/src/main/java/ti4/game/PlayerProperties.java b/src/main/java/ti4/game/PlayerProperties.java index aea5d810be..a74a460469 100644 --- a/src/main/java/ti4/game/PlayerProperties.java +++ b/src/main/java/ti4/game/PlayerProperties.java @@ -6,9 +6,14 @@ import java.util.LinkedHashSet; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; import javax.annotation.Nullable; +import lombok.AccessLevel; import lombok.Data; +import lombok.Getter; +import lombok.Setter; +import ti4.service.transaction.TransactionItem; @Data public class PlayerProperties { @@ -123,7 +128,11 @@ public class PlayerProperties { private Set unitsOwned = new HashSet<>(); private Set followedSCs = new HashSet<>(); private Set SCs = new LinkedHashSet<>(); + + @Getter(AccessLevel.NONE) + @Setter(AccessLevel.NONE) private List transactionItems = new ArrayList<>(); + private List promissoryNotesInPlayArea = new ArrayList<>(); private List techs = new ArrayList<>(); private List spentThingsThisWindow = new ArrayList<>(); @@ -146,4 +155,19 @@ public class PlayerProperties { public void loadCommodities(int commodities) { this.commodities = commodities; } + + public List getTransactionItems() { + return transactionItems.stream() + .map(TransactionItem::parse) + .filter(Objects::nonNull) + .toList(); + } + + public List getSerializedTransactionItems() { + return transactionItems; + } + + public void setTransactionItems(List transactionItems) { + this.transactionItems = new ArrayList<>(transactionItems); + } } diff --git a/src/main/java/ti4/game/persistence/GameSaveService.java b/src/main/java/ti4/game/persistence/GameSaveService.java index 4151e9e9f3..28aa8cea66 100644 --- a/src/main/java/ti4/game/persistence/GameSaveService.java +++ b/src/main/java/ti4/game/persistence/GameSaveService.java @@ -934,7 +934,7 @@ private static void saveGameInfo(Writer writer, Game game) throws IOException { writer.write(System.lineSeparator()); writer.write(Constants.BOMBARD_UNITS + " " + String.join(",", player.getBombardUnits())); writer.write(System.lineSeparator()); - writer.write(Constants.TRANSACTION_ITEMS + " " + String.join(",", player.getTransactionItems())); + writer.write(Constants.TRANSACTION_ITEMS + " " + String.join(",", player.getSerializedTransactionItems())); writer.write(System.lineSeparator()); writer.write(Constants.TEAMMATE_IDS + " " + String.join(",", player.getTeamMateIDs())); writer.write(System.lineSeparator()); diff --git a/src/main/java/ti4/helpers/TransactionHelper.java b/src/main/java/ti4/helpers/TransactionHelper.java index 72643c19d2..e1489e41ab 100644 --- a/src/main/java/ti4/helpers/TransactionHelper.java +++ b/src/main/java/ti4/helpers/TransactionHelper.java @@ -48,6 +48,7 @@ import ti4.service.leader.CommanderUnlockCheckService; import ti4.service.relic.SendRelicService; import ti4.service.transaction.SendPromissoryService; +import ti4.service.transaction.TransactionItem; import ti4.settings.users.UserSettingsManager; import ti4.spring.service.gameevent.GameEventService; import ti4.spring.service.gameevent.GameEventType; @@ -89,12 +90,18 @@ public class TransactionHelper { "Dane's Torment Engine LLC"); private static void acceptTransactionOffer(Player p1, Player p2, Game game, ButtonInteractionEvent event) { - List transactionItems = p1.getTransactionItemsWithPlayer(p2); + List transactionItems = p1.getTransactionItemsWithPlayer(p2); GameEventService.commit( game, GameEventType.TRANSACTION, p1, - Map.of("from", p1.getFaction(), "to", p2.getFaction(), "items", transactionItems)); + Map.of( + "from", + p1.getFaction(), + "to", + p2.getFaction(), + "items", + hidePrivateCardIdsForPublicEvent(transactionItems))); List players = new ArrayList<>(); players.add(p1); players.add(p2); @@ -118,19 +125,11 @@ private static void acceptTransactionOffer(Player p1, Player p2, Game game, Butt if (sender == p2) { receiver = p1; } - for (String item : transactionItems) { - if (item.contains("sending" + sender.getFaction()) - && item.contains("receiving" + receiver.getFaction())) { - String thingToTransact = item.split("_")[2]; - String furtherDetail = item.replace( - item.split("_")[0] + "_" + item.split("_")[1] + "_" + item.split("_")[2] + "_", ""); - int amountToTransact = 1; - boolean isGeneralized = - List.of("PNs", "ACs", "SOs").contains(thingToTransact) && furtherDetail.contains("generic"); - if (isGeneralized) { - amountToTransact = Integer.parseInt("" + furtherDetail.charAt(furtherDetail.length() - 1)); - furtherDetail = furtherDetail.substring(0, furtherDetail.length() - 1); - } + for (TransactionItem item : transactionItems) { + if (item.isSentFromTo(sender.getFaction(), receiver.getFaction())) { + String thingToTransact = item.type(); + String furtherDetail = item.detailWithoutQuantity(); + int amountToTransact = item.quantity(); String spoofedButtonID = "send_" + thingToTransact + "_" + receiver.getFaction() + "_" + furtherDetail; if (!thingToTransact.toLowerCase().contains("debt")) { @@ -239,8 +238,14 @@ private static void acceptTransactionOffer(Player p1, Player p2, Game game, Butt } } + static List hidePrivateCardIdsForPublicEvent(List transactionItems) { + return transactionItems.stream() + .map(item -> item.hidePrivateCardIdForPublicEvent().serialize()) + .toList(); + } + private static String buildTransactionOffer(Player p1, Player p2, Game game, boolean hidePrivateCardText) { - List transactionItems = p1.getTransactionItemsWithPlayer(p2); + List transactionItems = p1.getTransactionItemsWithPlayer(p2); StringBuilder trans = new StringBuilder(); List players = new ArrayList<>(); players.add(p1); @@ -253,22 +258,15 @@ private static String buildTransactionOffer(Player p1, Player p2, Game game, boo .append(player.getRepresentation(false, false, true)) .append(" gives:\n"); boolean sendingNothing = true; - for (String item : transactionItems) { - if (!item.contains("sending" + player.getFaction())) { + for (TransactionItem item : transactionItems) { + if (!item.isSentBy(player.getFaction())) { continue; } trans.append("> - "); sendingNothing = false; - String thingToTransact = item.split("_")[2]; - String furtherDetail = item.replace( - item.split("_")[0] + "_" + item.split("_")[1] + "_" + item.split("_")[2] + "_", ""); - int amountToTransact = 1; - boolean isGeneralized = - List.of("PNs", "ACs", "SOs").contains(thingToTransact) && furtherDetail.contains("generic"); - if ("frags".equalsIgnoreCase(thingToTransact) || isGeneralized) { - amountToTransact = Integer.parseInt("" + furtherDetail.charAt(furtherDetail.length() - 1)); - furtherDetail = furtherDetail.substring(0, furtherDetail.length() - 1); - } + String thingToTransact = item.type(); + String furtherDetail = item.detailWithoutQuantity(); + int amountToTransact = item.quantity(); switch (thingToTransact) { case "TGs" -> { amountToTransact = Integer.parseInt(furtherDetail); @@ -416,7 +414,10 @@ private static String buildTransactionOffer(Player p1, Player p2, Game game, boo trans.append(furtherDetail.replace("fin777", " ")); } } - default -> trans.append(" some odd thing: `").append(item).append('`'); + default -> + trans.append(" some odd thing: `") + .append(item.serialize()) + .append('`'); } trans.append('\n'); } @@ -1090,9 +1091,13 @@ private static boolean resolveAgeOfCommerceTechCheck(Player owner, Player receiv public static void finishDealDetails(ModalInteractionEvent event, Game game, Player player, String modalID) { ModalMapping mapping = event.getValue("details"); String thoughts = mapping.getAsString(); - Player opposing = game.getPlayerFromColorOrFaction(modalID.split("_")[1]); - player.addTransactionItem("sending" + player.getFaction() + "_receiving" + modalID.split("_")[1] + "_details_" - + thoughts.replace("_", "").replace(",", "").replace("\n", "").replace(" ", "fin777")); + String receiver = modalID.split("_")[1]; + Player opposing = game.getPlayerFromColorOrFaction(receiver); + player.addTransactionItem(TransactionItem.of( + player.getFaction(), + receiver, + "details", + thoughts.replace("_", "").replace(",", "").replace("\n", "").replace(" ", "fin777"))); String message = "Current transaction offer is:\n" + buildTransactionOffer(player, opposing, game, false) + "### Click something that you wish to __offer to__ " + opposing.getRepresentation(false, false); @@ -1115,9 +1120,13 @@ public static void finishTrackRecord(ModalInteractionEvent event, String modalID public static void finishDealDetailsInvert(ModalInteractionEvent event, Game game, Player player, String modalID) { ModalMapping mapping = event.getValue("details"); String thoughts = mapping.getAsString(); - Player opposing = game.getPlayerFromColorOrFaction(modalID.split("_")[1]); - player.addTransactionItem("sending" + modalID.split("_")[1] + "_receiving" + player.getFaction() + "_details_" - + thoughts.replace("_", "").replace(",", "").replace("\n", "").replace(" ", "fin777")); + String sender = modalID.split("_")[1]; + Player opposing = game.getPlayerFromColorOrFaction(sender); + player.addTransactionItem(TransactionItem.of( + sender, + player.getFaction(), + "details", + thoughts.replace("_", "").replace(",", "").replace("\n", "").replace(" ", "fin777"))); String message = "Current transaction offer is:\n" + buildTransactionOffer(player, opposing, game, false) + "### Click something that you wish to __request from__ " + opposing.getRepresentation(false, false); @@ -1154,24 +1163,22 @@ public static void resolveOfferToTransact(Game game, Player player, String butto int tgP2Sent = commsOfP1Washed - commsOfP2Washed; if (commsOfP1Washed > 0) { player.addTransactionItem( - "sending" + p1.getFaction() + "_receiving" + p2.getFaction() + "_Comms_" + commsOfP1Washed); + TransactionItem.of(p1.getFaction(), p2.getFaction(), "Comms", commsOfP1Washed)); } if (commsOfP2Washed > 0) { player.addTransactionItem( - "sending" + p2.getFaction() + "_receiving" + p1.getFaction() + "_Comms_" + commsOfP2Washed); + TransactionItem.of(p2.getFaction(), p1.getFaction(), "Comms", commsOfP2Washed)); } if (tgP1Sent > 0) { - player.addTransactionItem( - "sending" + p1.getFaction() + "_receiving" + p2.getFaction() + "_TGs_" + tgP1Sent); + player.addTransactionItem(TransactionItem.of(p1.getFaction(), p2.getFaction(), "TGs", tgP1Sent)); } if (tgP2Sent > 0) { - player.addTransactionItem( - "sending" + p2.getFaction() + "_receiving" + p1.getFaction() + "_TGs_" + tgP2Sent); + player.addTransactionItem(TransactionItem.of(p2.getFaction(), p1.getFaction(), "TGs", tgP2Sent)); } } else { - String itemS = "sending" + sender + "_receiving" + receiver + "_" + item + "_" + extraDetail; - if (!player.getTransactionItems().contains(itemS) || !itemS.contains("dmz")) { - player.addTransactionItem(itemS); + TransactionItem transactionItem = TransactionItem.of(sender, receiver, item, extraDetail); + if (!player.getTransactionItems().contains(transactionItem) || !"dmz".equals(transactionItem.type())) { + player.addTransactionItem(transactionItem); } } var userSettings = UserSettingsManager.get(player.getUserID()); @@ -1181,7 +1188,7 @@ public static void resolveOfferToTransact(Game game, Player player, String butto && userSettings.isPrefersAutoDebtClearance() && !p2.hasAbility("data_recovery")) { int amount = Math.min(p2.getDebtTokenCount(p1.getColor()), Integer.parseInt(extraDetail)); - String clear = "sending" + receiver + "_receiving" + sender + "_ClearDebt_" + amount; + TransactionItem clear = TransactionItem.of(receiver, sender, "ClearDebt", amount); if (!player.getTransactionItems().contains(clear)) { player.addTransactionItem(clear); } @@ -1324,18 +1331,15 @@ private static String buildPillageNotice(Game game, Player p1, Player p2) { int p2TgAfter = p2.getTg(); boolean debtOnlyTransaction = true; // compute TGs after transaction is complete - for (String item : p1.getTransactionItemsWithPlayer(p2)) { - String[] parts = item.split("_"); - if (parts.length < 4) continue; - String type = parts[2]; + for (TransactionItem item : p1.getTransactionItemsWithPlayer(p2)) { + String type = item.type(); if (!"SendDebt".equals(type) && !"ClearDebt".equals(type)) { debtOnlyTransaction = false; } - String detail = item.replace(parts[0] + "_" + parts[1] + "_" + type + "_", ""); try { if ("TGs".equals(type)) { - int amount = Integer.parseInt(detail); - if (item.contains("sending" + p1.getFaction())) { + int amount = Integer.parseInt(item.detail()); + if (item.isSentBy(p1.getFaction())) { p1TgAfter -= amount; p2TgAfter += amount; } else { @@ -1343,8 +1347,8 @@ private static String buildPillageNotice(Game game, Player p1, Player p2) { p1TgAfter += amount; } } else if ("Comms".equals(type)) { - int amount = Integer.parseInt(detail); - if (item.contains("sending" + p1.getFaction())) { + int amount = Integer.parseInt(item.detail()); + if (item.isSentBy(p1.getFaction())) { if (!p1.isPlayerMemberOfAlliance(p2)) p2TgAfter += amount; } else { if (!p2.isPlayerMemberOfAlliance(p1)) p1TgAfter += amount; diff --git a/src/main/java/ti4/image/TransactionGenerator.java b/src/main/java/ti4/image/TransactionGenerator.java index 2afd7bdac0..272499c7da 100644 --- a/src/main/java/ti4/image/TransactionGenerator.java +++ b/src/main/java/ti4/image/TransactionGenerator.java @@ -22,6 +22,7 @@ import ti4.service.emoji.MiscEmojis; import ti4.service.emoji.PlanetEmojis; import ti4.service.emoji.TI4Emoji; +import ti4.service.transaction.TransactionItem; public final class TransactionGenerator { @@ -138,7 +139,7 @@ public static BufferedImage drawTradeOfferMeme(Game game, Player p1, Player p2) DrawingUtil.drawEmoji(g2, hero, 210, 294, 200); // good doggies are smaller else DrawingUtil.drawEmoji(g2, hero, 60, 294, 500); - List transactionItems = p1.getTransactionItemsWithPlayer(p2); + List transactionItems = p1.getTransactionItemsWithPlayer(p2); for (Player player : List.of(p1, p2)) { boolean sendingNothing = true; HorizontalAlign hAlign = player == p2 ? HorizontalAlign.Left : HorizontalAlign.Right; @@ -150,20 +151,13 @@ public static BufferedImage drawTradeOfferMeme(Game game, Player p1, Player p2) int emojiRow = 0; int emojiCount = 0; g2.setFont(Storage.getFont24()); - for (String item : transactionItems) { - if (!item.contains("sending" + player.getFaction())) continue; + for (TransactionItem transactionItem : transactionItems) { + if (!transactionItem.isSentBy(player.getFaction())) continue; sendingNothing = false; - String thingToTransact = item.split("_")[2]; - String furtherDetail = item.replace( - item.split("_")[0] + "_" + item.split("_")[1] + "_" + item.split("_")[2] + "_", ""); - int amountToTransact = 1; - if ("frags".equalsIgnoreCase(thingToTransact) - || (("PNs".equalsIgnoreCase(thingToTransact) || "ACs".equalsIgnoreCase(thingToTransact)) - && furtherDetail.contains("generic"))) { - amountToTransact = Integer.parseInt("" + furtherDetail.charAt(furtherDetail.length() - 1)); - furtherDetail = furtherDetail.substring(0, furtherDetail.length() - 1); - } + String thingToTransact = transactionItem.type(); + String furtherDetail = transactionItem.detailWithoutQuantity(); + int amountToTransact = transactionItem.quantity(); boolean skipYShift = false; boolean isEmoji = List.of("TGs", "Comms", "ACs", "PNs", "Frags").contains(thingToTransact); @@ -236,7 +230,7 @@ public static BufferedImage drawTradeOfferMeme(Game game, Player p1, Player p2) y += drawStringMultiLine(g2, txt, x, y + 40, 250, hAlign); } default -> { - String txt = "\"" + item + "\""; + String txt = "\"" + transactionItem.serialize() + "\""; y += drawStringMultiLine(g2, txt, x, y + 40, 250, hAlign); } } diff --git a/src/main/java/ti4/service/transaction/TransactionItem.java b/src/main/java/ti4/service/transaction/TransactionItem.java new file mode 100644 index 0000000000..d2bb5e270d --- /dev/null +++ b/src/main/java/ti4/service/transaction/TransactionItem.java @@ -0,0 +1,66 @@ +package ti4.service.transaction; + +import java.util.List; + +public record TransactionItem(String sender, String receiver, String type, String detail) { + + private static final List PRIVATE_CARD_TYPES = List.of("ACs", "PNs", "SOs"); + + public static TransactionItem parse(String item) { + String[] parts = item.split("_", 4); + if (parts.length < 4 || !parts[0].startsWith("sending") || !parts[1].startsWith("receiving")) { + return null; + } + return new TransactionItem( + parts[0].substring("sending".length()), parts[1].substring("receiving".length()), parts[2], parts[3]); + } + + public static TransactionItem of(String sender, String receiver, String type, Object detail) { + return new TransactionItem(sender, receiver, type, String.valueOf(detail)); + } + + public String serialize() { + return "sending" + sender + "_receiving" + receiver + "_" + type + "_" + detail; + } + + public boolean isSentBy(String faction) { + return sender.equals(faction); + } + + public boolean isSentFromTo(String senderFaction, String receiverFaction) { + return sender.equals(senderFaction) && receiver.equals(receiverFaction); + } + + public boolean involves(String faction) { + return sender.equals(faction) || receiver.equals(faction); + } + + public boolean isPrivateCard() { + return PRIVATE_CARD_TYPES.contains(type); + } + + public TransactionItem hidePrivateCardIdForPublicEvent() { + if (isPrivateCard() && !detail.startsWith("generic")) { + return new TransactionItem(sender, receiver, type, "generic1"); + } + return this; + } + + public boolean hasQuantitySuffix() { + return "frags".equalsIgnoreCase(type) || (isPrivateCard() && detail.contains("generic")); + } + + public int quantity() { + if (!hasQuantitySuffix()) { + return 1; + } + return Integer.parseInt(detail.substring(detail.length() - 1)); + } + + public String detailWithoutQuantity() { + if (!hasQuantitySuffix()) { + return detail; + } + return detail.substring(0, detail.length() - 1); + } +} diff --git a/src/test/java/ti4/helpers/TransactionHelperTest.java b/src/test/java/ti4/helpers/TransactionHelperTest.java new file mode 100644 index 0000000000..114ff492ac --- /dev/null +++ b/src/test/java/ti4/helpers/TransactionHelperTest.java @@ -0,0 +1,35 @@ +package ti4.helpers; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; +import org.junit.jupiter.api.Test; +import ti4.service.transaction.TransactionItem; + +class TransactionHelperTest { + + @Test + void publicEventItemsHidePrivateCardIds() { + List items = List.of( + "sendingkeleresm_receivingsardakk_PNs_15", + "sendingsardakk_receivingkeleresm_ACs_42", + "sendingsardakk_receivingkeleresm_SOs_secret_id", + "sendingkeleresm_receivingsardakk_PNs_generic2", + "sendingkeleresm_receivingsardakk_details_Thefin777dealfin777isfin777thefin777usual", + "sendingkeleresm_receivingsardakk_TGs_3") + .stream() + .map(TransactionItem::parse) + .toList(); + + List sanitized = TransactionHelper.hidePrivateCardIdsForPublicEvent(items); + + assertThat(sanitized) + .containsExactly( + "sendingkeleresm_receivingsardakk_PNs_generic1", + "sendingsardakk_receivingkeleresm_ACs_generic1", + "sendingsardakk_receivingkeleresm_SOs_generic1", + "sendingkeleresm_receivingsardakk_PNs_generic2", + "sendingkeleresm_receivingsardakk_details_Thefin777dealfin777isfin777thefin777usual", + "sendingkeleresm_receivingsardakk_TGs_3"); + } +} diff --git a/src/test/java/ti4/service/transaction/TransactionItemTest.java b/src/test/java/ti4/service/transaction/TransactionItemTest.java new file mode 100644 index 0000000000..12bb7d07a0 --- /dev/null +++ b/src/test/java/ti4/service/transaction/TransactionItemTest.java @@ -0,0 +1,37 @@ +package ti4.service.transaction; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + +class TransactionItemTest { + + @Test + void serializesToLegacyFormat() { + assertThat(TransactionItem.of("sardakk", "keleresm", "PNs", "15").serialize()) + .isEqualTo("sendingsardakk_receivingkeleresm_PNs_15"); + } + + @Test + void roundTripsLegacyFormatWithUnderscoresInDetail() { + String item = "sendingsardakk_receivingkeleresm_SOs_secret_id"; + + assertThat(TransactionItem.parse(item).serialize()).isEqualTo(item); + } + + @Test + void hidesSpecificPrivateCardIds() { + assertThat(TransactionItem.parse("sendingsardakk_receivingkeleresm_ACs_42") + .hidePrivateCardIdForPublicEvent() + .serialize()) + .isEqualTo("sendingsardakk_receivingkeleresm_ACs_generic1"); + } + + @Test + void leavesGenericPrivateCardsAlone() { + assertThat(TransactionItem.parse("sendingsardakk_receivingkeleresm_PNs_generic2") + .hidePrivateCardIdForPublicEvent() + .serialize()) + .isEqualTo("sendingsardakk_receivingkeleresm_PNs_generic2"); + } +}