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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/main/java/pw/chew/mlb/commands/ScoreCommand.java
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ protected void execute(SlashCommandEvent event) {
}

public String buildScore(String gamePk, String channelId) {
GameState state = GameState.fromPk(gamePk);
GameState state = GameState.fromPk(gamePk, "en");

String channelMention = "";
if (channelId != null) {
Expand Down
23 changes: 17 additions & 6 deletions src/main/java/pw/chew/mlb/commands/StartGameCommand.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import net.dv8tion.jda.api.interactions.commands.OptionType;
import net.dv8tion.jda.api.interactions.commands.build.OptionData;
import net.dv8tion.jda.api.utils.TimeFormat;
import net.dv8tion.jda.internal.utils.Checks;
import org.json.JSONArray;
import org.json.JSONObject;
import pw.chew.chewbotcca.util.RestClient;
Expand All @@ -24,6 +25,7 @@
import java.time.OffsetDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
Expand All @@ -39,33 +41,41 @@ public StartGameCommand() {
);

this.guildOnly = true;
this.options = Collections.singletonList(
this.options = Arrays.asList(
new OptionData(OptionType.INTEGER, "game", "Which game to listen to", true)
.setDescriptionLocalization(DiscordLocale.SPANISH, "A qué juego escuchar")
.setAutoComplete(true)
.setAutoComplete(true),
new OptionData(OptionType.STRING, "lang", "The language to use for the game. By default, it will use the server's language.", false)
.setDescriptionLocalization(DiscordLocale.SPANISH, "El idioma a usar para el juego")
.addChoice("English", "en")
.addChoice("Spanish", "es")
);
}

@Override
protected void execute(SlashCommandEvent event) {
Checks.notNull(event.getGuild(), "server");

String gamePk = event.getOption("game", "0", OptionMapping::getAsString);
String lang = event.optString("lang", event.getGuild().getLocale().getLocale().split("-")[0]);

try {
MessageEmbed startGame = startGame(gamePk, event.getGuildChannel(), event.getUser());
MessageEmbed startGame = startGame(gamePk, lang, event.getGuildChannel(), event.getUser());
event.replyEmbeds(startGame).queue();
} catch (IllegalStateException e) {
event.replyEmbeds(EmbedUtil.failure(e.getMessage())).setEphemeral(true).queue();
}
}

public static MessageEmbed startGame(String gamePk, GuildMessageChannel channel, User invoker) {
public static MessageEmbed startGame(String gamePk, String lang, GuildMessageChannel channel, User invoker) {
String currentGame = GameFeedHandler.currentGame(channel);
if (currentGame != null) {
throw new IllegalStateException("This channel is already playing a game: " + currentGame + ". Please wait for it to finish, or stop it with `/stopgame`.");
}

// Start a new thread
ActiveGame activeGame = new ActiveGame(gamePk, channel.getId());
GameState currentState = GameState.fromPk(gamePk);
ActiveGame activeGame = new ActiveGame(gamePk, lang, channel.getId());
GameState currentState = GameState.fromPk(gamePk, "en");

// Refuse to start if the game is already over
if (currentState.isFinal()) {
Expand All @@ -84,6 +94,7 @@ public static MessageEmbed startGame(String gamePk, GuildMessageChannel channel,

List<String> description = new ArrayList<>();
description.add("First Pitch: %s".formatted(TimeFormat.RELATIVE.format(currentState.officialDate())));
description.add("Language: %s".formatted(lang));
description.add("\n*Invoked by %s*".formatted(invoker.getAsMention()));

EmbedBuilder embed = new EmbedBuilder()
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/pw/chew/mlb/commands/StopGameCommand.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ protected void execute(SlashCommandEvent event) {
if (stoppedGame == null) {
event.replyEmbeds(EmbedUtil.failure("There is no active game in this channel. Please start a game first.")).setEphemeral(true).queue();
} else {
GameState state = GameState.fromPk(stoppedGame);
GameState state = GameState.fromPk(stoppedGame, "en");

EmbedBuilder embed = new EmbedBuilder()
.setTitle("Stopped Game **%s @ %s**".formatted(state.away().clubName(), state.home().clubName()))
Expand Down
64 changes: 33 additions & 31 deletions src/main/java/pw/chew/mlb/listeners/GameFeedHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,9 @@ public static void addGame(ActiveGame game, boolean modifyDb) {
ChannelConfig.getConfig(game.channelId());

if (!GAME_THREADS.containsKey(game.gamePk())) {
Thread thread = new Thread(() -> runGame(game.gamePk()), "Game-" + game.gamePk());
Thread thread = new Thread(() -> runGame(game.gamePk(), game.lang()), game.getThreadCode());

GAME_THREADS.put(game.gamePk(), thread);
GAME_THREADS.put(game.getThreadCode(), thread);
thread.start();
logger.info("Started game thread for gamePk: " + game.gamePk());
}
Expand Down Expand Up @@ -101,9 +101,9 @@ public static void stopGame(ActiveGame game) {

// If this is the last game running, stop the thread
if (currentGames == 1) {
Thread thread = GAME_THREADS.get(game.gamePk());
Thread thread = GAME_THREADS.get(game.getThreadCode());
thread.interrupt();
removeThread(game.gamePk());
removeThread(game.gamePk(), game.lang());
logger.debug("Stopped game " + game.gamePk() + " thread");
}

Expand All @@ -124,10 +124,11 @@ public static List<ActiveGame> allGames() {
*
* @param gamePk The gamePk of the thread to remove.
*/
public static void removeThread(String gamePk) {
public static void removeThread(String gamePk, String lang) {
LoggerFactory.getLogger(GameFeedHandler.class).debug("Removing thread for gamePk " + gamePk);

GAME_THREADS.remove(gamePk);
String threadCode = "Game-%s-%s".formatted(gamePk, lang);
GAME_THREADS.remove(threadCode);

if (GAME_THREADS.isEmpty() && shutdownOnFinish) {
AdminCommand.shutdown();
Expand Down Expand Up @@ -189,22 +190,22 @@ public static ActiveGame currentServerGame(@NotNull Guild server) {
*
* @param gamePk The gamePk of the game to run.
*/
private static void runGame(String gamePk) {
logger.debug("Starting game with gamePk: " + gamePk);
private static void runGame(String gamePk, String lang) {
logger.debug("Starting game with gamePk {} and lang {} ", gamePk, lang);

if (gamePk.isEmpty()) {
return;
}

GameState currentState = GameState.fromPk(gamePk);
GameState currentState = GameState.fromPk(gamePk, lang);
List<JSONObject> postedAdvisories = currentState.gameAdvisories();

// boolean canEdit = channel.getGuild().getSelfMember().hasPermission(channel, Permission.MANAGE_CHANNEL);

// Start the looping thread until the game is stopped
int fails = 0;
while (!currentState.gameState().equals("Final")) {
GameState recentState = GameState.fromPk(gamePk);
GameState recentState = GameState.fromPk(gamePk, lang);

if (recentState.failed()) {
int retryIn = fails + 3;
Expand All @@ -222,15 +223,15 @@ private static void runGame(String gamePk) {
""")
.setColor(Color.RED);

sendMessages(notifier.build(), gamePk);
sendMessages(notifier.build(), gamePk, lang);
}
try {
//noinspection BusyWait
Thread.sleep(retryIn * 1000L);
} catch (InterruptedException e) {
e.printStackTrace();
// Shutdown the thread
removeThread(gamePk);
removeThread(gamePk, lang);
return;
}
fails++;
Expand All @@ -245,17 +246,17 @@ private static void runGame(String gamePk) {
""")
.setColor(Color.GREEN);

sendMessages(notifier.build(), gamePk);
sendMessages(notifier.build(), gamePk, lang);
}
fails = 0;

if (recentState.isCancelled()) {
endGame(gamePk, recentState, "\nUnfortunately, this game was cancelled.");
endGame(gamePk, lang, recentState, "\nUnfortunately, this game was cancelled.");
return;
}

if (recentState.isSuspended()) {
endGame(gamePk, recentState, "\nUnfortunately, this game has been suspended. It will resume at a later time.");
endGame(gamePk, lang, recentState, "\nUnfortunately, this game has been suspended. It will resume at a later time.");
return;
}

Expand Down Expand Up @@ -319,7 +320,7 @@ private static void runGame(String gamePk) {
}

// Send result
sendPlay(embed.build(), gamePk, recentState, scoringPlay);
sendPlay(embed.build(), gamePk, lang, recentState, scoringPlay);
}

// Check for new advisories
Expand Down Expand Up @@ -358,7 +359,7 @@ private static void runGame(String gamePk) {
queuedAdvisories.add(detailEmbed.build());
}

sendAdvisory(queuedAdvisories, gamePk);
sendAdvisory(queuedAdvisories, gamePk, lang);
}

// Check if the inning state changed
Expand All @@ -369,7 +370,7 @@ private static void runGame(String gamePk) {
.setTitle("Inning State Updated")
.setDescription(recentState.inningState() + " of the " + recentState.inningOrdinal());

sendMessages(inningEmbed.build(), gamePk);
sendMessages(inningEmbed.build(), gamePk, lang);
}
}

Expand All @@ -384,7 +385,7 @@ private static void runGame(String gamePk) {
} catch (InterruptedException e) {
e.printStackTrace();
// Shutdown the thread
removeThread(gamePk);
removeThread(gamePk, lang);
return;
}
}
Expand Down Expand Up @@ -456,7 +457,7 @@ private static void runGame(String gamePk) {
tableBuilder.codeblock(true);

// Game is over!
endGame(gamePk, currentState, tableBuilder.build());
endGame(gamePk, lang, currentState, tableBuilder.build());
}

/**
Expand All @@ -465,8 +466,8 @@ private static void runGame(String gamePk) {
* @param embeds The embeds to send.
* @param gamePk The gamePk of the game.
*/
public static void sendAdvisory(List<MessageEmbed> embeds, String gamePk) {
for (ActiveGame game : getGames(gamePk)) {
public static void sendAdvisory(List<MessageEmbed> embeds, String gamePk, String lang) {
for (ActiveGame game : getGames(gamePk, lang)) {
ChannelConfig config = ChannelConfig.getConfig(game.channelId());
if (!config.gameAdvisories()) continue;

Expand All @@ -477,8 +478,8 @@ public static void sendAdvisory(List<MessageEmbed> embeds, String gamePk) {
}
}

public static void sendMessages(MessageEmbed message, String gamePk) {
for (ActiveGame game : getGames(gamePk)) {
public static void sendMessages(MessageEmbed message, String gamePk, String lang) {
for (ActiveGame game : getGames(gamePk, lang)) {
GuildMessageChannel channel = canSafelySend(game);
if (channel == null) continue;

Expand All @@ -494,8 +495,8 @@ public static void sendMessages(MessageEmbed message, String gamePk) {
* @param gameState The game state at the time of this play.
* @param isScoringPlay Whether the play is a scoring play.
*/
public static void sendPlay(MessageEmbed message, String gamePk, GameState gameState, boolean isScoringPlay) {
for (ActiveGame game : getGames(gamePk)) {
public static void sendPlay(MessageEmbed message, String gamePk, String lang, GameState gameState, boolean isScoringPlay) {
for (ActiveGame game : getGames(gamePk, lang)) {
ChannelConfig config = ChannelConfig.getConfig(game.channelId());

// If configured to only show scoring plays, ignore non-scoring plays
Expand Down Expand Up @@ -548,8 +549,8 @@ public static void sendPlay(MessageEmbed message, String gamePk, GameState gameS
}
}

public static void endGame(String gamePk, GameState currentState, String scorecard) {
for (ActiveGame game : getGames(gamePk)) {
public static void endGame(String gamePk, String lang, GameState currentState, String scorecard) {
for (ActiveGame game : getGames(gamePk, lang)) {
GuildChannel gChan = jda.getGuildChannelById(game.channelId());
if (gChan == null) continue;

Expand Down Expand Up @@ -577,12 +578,12 @@ public static void endGame(String gamePk, GameState currentState, String scoreca
}

// Remove the games from the active games list
for (ActiveGame game : getGames(gamePk)) {
for (ActiveGame game : getGames(gamePk, lang)) {
stopGame(game);
}

// Remove the game thread
removeThread(gamePk);
removeThread(gamePk, lang);
}

/**
Expand Down Expand Up @@ -625,11 +626,12 @@ private static GuildMessageChannel canSafelySend(ActiveGame game) {
* @param gamePk The gamePk to get active games for
* @return A list of active games
*/
public static List<ActiveGame> getGames(String gamePk) {
public static List<ActiveGame> getGames(String gamePk, String lang) {
List<ActiveGame> games = new ArrayList<>();

for (ActiveGame game : gamesMap.values()) {
if (!game.gamePk().equals(gamePk)) continue;
if (!game.lang().equals(lang)) continue;

games.add(game);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ public void onButtonInteraction(@NotNull ButtonInteractionEvent event) {
case "refresh" -> event.deferEdit().queue(e -> e.editOriginalEmbeds(PlanGameCommand.generateGameBlurb(gamePk)).queue());
case "start" -> {
try {
MessageEmbed startGame = StartGameCommand.startGame(gamePk, event.getGuildChannel(), event.getUser());
MessageEmbed startGame = StartGameCommand.startGame(gamePk, "en", event.getGuildChannel(), event.getUser());
event.replyEmbeds(startGame).queue();
} catch (IllegalStateException e) {
event.replyEmbeds(EmbedUtil.failure(e.getMessage())).setEphemeral(true).queue();
Expand Down
9 changes: 7 additions & 2 deletions src/main/java/pw/chew/mlb/objects/ActiveGame.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,22 @@
import java.io.IOException;
import java.io.Serializable;

public record ActiveGame(String gamePk, String channelId) implements Serializable {
public record ActiveGame(String gamePk, String lang, String channelId) implements Serializable {
public String getThreadCode() {
return "Game-%s-%s".formatted(gamePk, lang);
}

public static class EntrySerializer implements Serializer<ActiveGame>, Serializable {
@Override
public void serialize(@NotNull DataOutput2 out, @NotNull ActiveGame value) throws IOException {
out.writeUTF(value.gamePk());
out.writeUTF(value.lang());
out.writeUTF(value.channelId());
}

@Override
public ActiveGame deserialize(@NotNull DataInput2 input, int available) throws IOException {
return new ActiveGame(input.readUTF(), input.readUTF());
return new ActiveGame(input.readUTF(), input.readUTF(), input.readUTF());
}
}
}
14 changes: 8 additions & 6 deletions src/main/java/pw/chew/mlb/objects/GameState.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,25 +18,27 @@
*
* @param gameData The game data from the MLB API
*/
public record GameState(JSONObject gameData) {
public record GameState(JSONObject gameData, String locale) {
/**
* Retrieves the latest game data for the provided game PK (ID)
*
* @param gamePk The gamePk of the game to get the state of
* @param locale The locale to use. en for English, es for Spanish.
* @return The game state
*/
@NotNull
public static GameState fromPk(String gamePk) {
String res = RestClient.get("https://statsapi.mlb.com/api/v1.1/game/:id/feed/live?language=en&fields=gameData,game,pk,datetime,dateTime,status,detailedState,abstractGameState,liveData,plays,allPlays,result,description,awayScore,homeScore,event,about,isComplete,count,balls,strikes,outs,playEvents,details,isInPlay,isScoringPlay,description,event,eventType,hitData,launchSpeed,launchAngle,totalDistance,trajectory,hardness,isPitch,atBatIndex,playId,currentPlay,count,outs,matchup,batter,fullName,pitcher,fullName,postOnFirst,fullName,postOnSecond,postOnThird,fullName,linescore,currentInning,currentInningOrdinal,inningState,linescore,teams,home,name,clubName,abbreviation,runs,away,runs,innings,num,home,runs,away,runs,teams,home,runs,hits,errors,leftOnBase,away,runs,hits,errors,leftOnBase,decisions,winner,fullName,id,loser,save,boxscore,teams,away,home,players,stats,pitching,note"
public static GameState fromPk(String gamePk, String locale) {
String res = RestClient.get("https://statsapi.mlb.com/api/v1.1/game/:id/feed/live?language=:lang&fields=gameData,game,pk,datetime,dateTime,status,detailedState,abstractGameState,liveData,plays,allPlays,result,description,awayScore,homeScore,event,about,isComplete,count,balls,strikes,outs,playEvents,details,isInPlay,isScoringPlay,description,event,eventType,hitData,launchSpeed,launchAngle,totalDistance,trajectory,hardness,isPitch,atBatIndex,playId,currentPlay,count,outs,matchup,batter,fullName,pitcher,fullName,postOnFirst,fullName,postOnSecond,postOnThird,fullName,linescore,currentInning,currentInningOrdinal,inningState,linescore,teams,home,name,clubName,abbreviation,runs,away,runs,innings,num,home,runs,away,runs,teams,home,runs,hits,errors,leftOnBase,away,runs,hits,errors,leftOnBase,decisions,winner,fullName,id,loser,save,boxscore,teams,away,home,players,stats,pitching,note"
.replace(":lang", locale)
.replace(":id", gamePk));

try {
JSONObject json = new JSONObject(res);

return new GameState(json);
return new GameState(json, locale);
} catch (JSONException e) {
LoggerFactory.getLogger(GameState.class).error("Failed to parse game data (error: {}): {}", e, res);
return new GameState(new JSONObject());
return new GameState(new JSONObject(), locale);
}
}

Expand Down Expand Up @@ -306,7 +308,7 @@ public String hitInfo() {
} else {
JSONObject hitData = event.getJSONObject("hitData");

return String.format("Ball left the bat at a speed of %s mph at a %s° angle, and travelled %s feet.",
return String.format("Ball left the bat at a speed of %s mph at a %s° angle, and travelled %s feet.", // TODO: This needs to be spanish
hitData.getFloat("launchSpeed"),
hitData.getFloat("launchAngle"),
hitData.getFloat("totalDistance"));
Expand Down