Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
import ti4.game.Game;
import ti4.game.Player;
import ti4.logging.BotLogger;
import ti4.message.MessageHelper;
import ti4.service.actioncard.KnownActionCardsService;
import ti4.service.breakthrough.EidolonMaximumService;
import ti4.service.info.CardsInfoService;

Expand All @@ -31,4 +33,13 @@ public static void sendCardsInfo(Game game, Player player, GenericInteractionCre
CardsInfoService.sendCardsInfo(game, player, event);
EidolonMaximumService.sendEidolonMaximumFlipButtons(game, player);
}

@ButtonHandler(value = "showKnownActionCards", save = false)
public static void showKnownActionCards(Game game, Player player) {
if (player == null) {
return;
}
MessageHelper.sendMessageToPlayerCardsInfoThread(
player, KnownActionCardsService.getKnownActionCardsText(game, player));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@
import ti4.discord.interactions.commands.GameStateSubcommand;
import ti4.game.Game;
import ti4.game.Player;
import ti4.helpers.ActionCardHelper;
import ti4.helpers.Constants;
import ti4.image.Mapper;
import ti4.message.MessageHelper;
import ti4.service.actioncard.KnownActionCardsService;

class ShowAC extends GameStateSubcommand {

Expand Down Expand Up @@ -58,7 +58,7 @@ public void execute(SlashCommandInteractionEvent event) {
return;
}

ActionCardHelper.sendActionCardInfo(game, player);
KnownActionCardsService.rememberShownActionCard(playerToShowTo, player, acID);
MessageHelper.sendMessageToPlayerCardsInfoThread(playerToShowTo, sb);
Comment on lines 58 to 62

Copilot AI Apr 21, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This command still mutates the source player’s hand by calling player.setActionCard(acID) earlier in execute(). setActionCard adds a new copy of the card (new identifier), so using /show_ac will duplicate the shown card instead of just revealing it. Remove that setActionCard call (and any other hand mutation) so the command is read-only aside from tracking via KnownActionCardsService.

Copilot uses AI. Check for mistakes.
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import ti4.helpers.Constants;
import ti4.image.Mapper;
import ti4.message.MessageHelper;
import ti4.service.actioncard.KnownActionCardsService;

class ShowACToAll extends GameStateSubcommand {

Expand Down Expand Up @@ -43,7 +44,7 @@ public void execute(SlashCommandInteractionEvent event) {
+ player.getUserName() + "\n" + "Shown Action Card:"
+ "\n" + Mapper.getActionCard(acID).getRepresentation(game)
+ "\n";
player.setActionCard(acID);
KnownActionCardsService.rememberShownActionCardToAll(game, player, acID);
MessageHelper.sendMessageToChannel(event.getChannel(), sb);
}
}
2 changes: 2 additions & 0 deletions src/main/java/ti4/game/Player.java
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@
import ti4.model.TechnologyModel.TechnologyType;
import ti4.model.TemporaryCombatModifierModel;
import ti4.model.UnitModel;
import ti4.service.actioncard.KnownActionCardsService;
import ti4.service.agenda.IsPlayerElectedService;
import ti4.service.breakthrough.DeepgloomService;
import ti4.service.breakthrough.ValefarZService;
Expand Down Expand Up @@ -1246,6 +1247,7 @@ public void removeActionCard(Integer identifier) {
}
}
actionCards.remove(idToRemove);
KnownActionCardsService.forgetCardFromKnownHands(game, this, idToRemove);
}

public void removePromissoryNote(Integer identifier) {
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/ti4/helpers/ActionCardHelper.java
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
import ti4.model.TemporaryCombatModifierModel;
import ti4.model.UnitModel;
import ti4.model.metadata.AutoPingMetadataManager;
import ti4.service.actioncard.KnownActionCardsService;
import ti4.service.actioncard.SabotageService;
import ti4.service.agenda.IsPlayerElectedService;
import ti4.service.breakthrough.DeepgloomService;
Expand Down Expand Up @@ -2067,6 +2068,7 @@ public static void showAll(Player player, Player player_, Game game) {
}
MessageHelper.sendMessageToPlayerCardsInfoThread(player, sa.toString());
MessageHelper.sendMessageToPlayerCardsInfoThread(player_, sb.toString());
KnownActionCardsService.rememberViewedHand(player_, player);
}

public static String actionCardListCondensedNoIds(List<String> discards, String title) {
Expand Down
154 changes: 154 additions & 0 deletions src/main/java/ti4/service/actioncard/KnownActionCardsService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
package ti4.service.actioncard;

import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import lombok.experimental.UtilityClass;
import ti4.game.Game;
import ti4.game.Player;
import ti4.helpers.ActionCardHelper;
import ti4.helpers.StringHelper;

@UtilityClass
public class KnownActionCardsService {

private static final String KNOWN_ACTION_CARDS_PREFIX = "knownActionCards_";

public static void rememberViewedHand(Player viewer, Player target) {
if (viewer == null || target == null || viewer == target) {
return;
}

removeTrackedKnowledge(viewer, target);
List<String> knownCards = new ArrayList<>(target.getActionCards().keySet());
if (!knownCards.isEmpty()) {
viewer.addToStoredList(getStorageKey(target), knownCards.toArray(String[]::new));
}
}

public static void rememberShownActionCard(Player viewer, Player target, String actionCardId) {
if (viewer == null || target == null || viewer == target || isUnset(actionCardId)) {
return;
}

String storageKey = getStorageKey(target);
if (!viewer.getStoredList(storageKey).contains(actionCardId)) {
viewer.addToStoredList(storageKey, actionCardId);
}
}

public static void rememberShownActionCardToAll(Game game, Player target, String actionCardId) {
if (game == null || target == null || isUnset(actionCardId)) {
return;
}

for (Player player : game.getRealPlayers()) {
rememberShownActionCard(player, target, actionCardId);
}
}

public static void forgetCardFromKnownHands(Game game, Player target, String actionCardId) {
if (game == null || target == null || actionCardId == null || actionCardId.isBlank()) {
return;
}

List<String> storageKeys = getStorageKeys(target);
for (Player player : game.getPlayers().values()) {
if (player == target) {
continue;
}
for (String key : storageKeys) {
player.removeFromStoredList(key, actionCardId);
}
}
}

public static boolean shouldShowKnownActionCardsButton(Player player) {
return player != null
&& (player.hasTech("mi")
|| player.getExhaustedTechs().contains("mi")
|| player.getStoredValueMap().keySet().stream()
.map(StringHelper::unescape)
.anyMatch(key -> key.startsWith(KNOWN_ACTION_CARDS_PREFIX)));
}

public static String getKnownActionCardsText(Game game, Player viewer) {
StringBuilder sb = new StringBuilder("__Known action cards in other players' hands__:");
boolean hasAnyKnownCards = false;

for (Player target : game.getRealPlayers()) {
if (target == viewer) {
continue;
}

List<String> knownCards = getKnownCards(viewer, target);
if (knownCards.isEmpty()) {
continue;
}

hasAnyKnownCards = true;
sb.append("\n### ").append(target.getRepresentationNoPing()).append('\n');
sb.append(ActionCardHelper.actionCardListCondensedNoIds(knownCards, null))
.append('\n');
}

if (!hasAnyKnownCards) {
sb.append("\n> None");
}

return sb.toString().trim();
}

private static String getStorageKey(Player target) {
String targetId = target.getUserID();
if (isUnset(targetId)) {
targetId = target.getUserName();
}
if (isUnset(targetId)) {
targetId = "unknownPlayer";
}
return KNOWN_ACTION_CARDS_PREFIX + targetId;
}

private static List<String> getStorageKeys(Player target) {
Set<String> keys = new LinkedHashSet<>();
keys.add(getStorageKey(target));

// Keep reading/removing the older non-userID keys so tracked knowledge survives migration
// from the earlier faction/color-based storage format.
String targetId = target.getFaction();
if (isUnset(targetId)) {
targetId = target.getColor();
}
if (isUnset(targetId)) {
targetId = target.getUserID();
}
if (isUnset(targetId)) {
targetId = target.getUserName();
}
if (isUnset(targetId)) {
targetId = "unknownPlayer";
}
keys.add(KNOWN_ACTION_CARDS_PREFIX + targetId);
return new ArrayList<>(keys);
}

private static List<String> getKnownCards(Player viewer, Player target) {
Set<String> knownCards = new LinkedHashSet<>();
for (String key : getStorageKeys(target)) {
knownCards.addAll(viewer.getStoredList(key));
}
return new ArrayList<>(knownCards);
}

private static void removeTrackedKnowledge(Player viewer, Player target) {
for (String key : getStorageKeys(target)) {
viewer.removeStoredValue(key);
}
}

private static boolean isUnset(String value) {
return value == null || value.isBlank() || "null".equalsIgnoreCase(value);
}
}
4 changes: 4 additions & 0 deletions src/main/java/ti4/service/info/CardsInfoService.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import ti4.helpers.PromissoryNoteHelper;
import ti4.logging.BotLogger;
import ti4.message.MessageHelper;
import ti4.service.actioncard.KnownActionCardsService;
import ti4.service.agenda.IsPlayerElectedService;
import ti4.service.emoji.CardEmojis;
import ti4.service.emoji.FactionEmojis;
Expand Down Expand Up @@ -169,6 +170,9 @@ public static void sendVariousAdditionalButtons(Game game, Player player) {
if (player.hasUnlockedBreakthrough("yssarilbt")) {
buttons.add(Buttons.green("startYssarilbt", "Use Yssaril Breakthrough", FactionEmojis.Yssaril));
}
if (KnownActionCardsService.shouldShowKnownActionCardsButton(player)) {
buttons.add(Buttons.gray("showKnownActionCards", "Show Known Action Cards", CardEmojis.ActionCard));
}
if (player.hasAbility("pillage") && !game.isTwilightsFallMode()) {
if (game.getStoredValue("willPillageOwnTransactions" + player.getFaction())
.isEmpty()) {
Expand Down
Loading
Loading