diff --git a/.gitignore b/.gitignore
index e36257f..0011502 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,3 +2,4 @@
/.idea/
/LeagueTeamComp.iml
/out/
+/logs/
diff --git a/pom.xml b/pom.xml
index f609244..febe56f 100644
--- a/pom.xml
+++ b/pom.xml
@@ -40,18 +40,7 @@
javafx-fxml
${javafx-fxml.version}
-
-
- org.controlsfx
- controlsfx
- ${controlsfx.version}
-
-
- org.projectlombok
- lombok
- ${lombok.version}
- provided
-
+
org.apache.logging.log4j
log4j-api
@@ -67,6 +56,18 @@
log4j-slf4j-impl
${log4j.version}
+
+
+ org.controlsfx
+ controlsfx
+ ${controlsfx.version}
+
+
+ org.projectlombok
+ lombok
+ ${lombok.version}
+ provided
+
com.google.code.gson
gson
diff --git a/src/main/java/com/st4s1k/leagueteamcomp/LeagueTeamCompApplication.java b/src/main/java/com/st4s1k/leagueteamcomp/LeagueTeamCompApplication.java
index 4beb7d7..792f1f4 100644
--- a/src/main/java/com/st4s1k/leagueteamcomp/LeagueTeamCompApplication.java
+++ b/src/main/java/com/st4s1k/leagueteamcomp/LeagueTeamCompApplication.java
@@ -1,58 +1,43 @@
package com.st4s1k.leagueteamcomp;
import com.google.gson.Gson;
+import com.st4s1k.leagueteamcomp.controller.LTCExceptionController;
import com.st4s1k.leagueteamcomp.controller.LeagueTeamCompController;
-import com.st4s1k.leagueteamcomp.model.champion.Champions;
+import com.st4s1k.leagueteamcomp.exceptions.LTCException;
+import com.st4s1k.leagueteamcomp.model.champion.ChampionsDTO;
import com.st4s1k.leagueteamcomp.repository.ChampionRepository;
+import com.st4s1k.leagueteamcomp.utils.ResizeHelper;
import javafx.application.Application;
+import javafx.application.Platform;
import javafx.fxml.FXMLLoader;
+import javafx.geometry.Insets;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.image.Image;
+import javafx.scene.layout.Region;
+import javafx.scene.paint.Color;
+import javafx.stage.Modality;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
+import java.io.PrintWriter;
+import java.io.StringWriter;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.text.MessageFormat;
-import java.util.ResourceBundle;
-import java.util.concurrent.CompletableFuture;
+import java.util.stream.DoubleStream;
+import static com.st4s1k.leagueteamcomp.utils.Resources.*;
import static java.net.http.HttpResponse.BodyHandlers;
import static java.util.Objects.requireNonNull;
@Slf4j
public class LeagueTeamCompApplication extends Application {
- /* * * * * * * * * * * * * * * * * * * * * * * * * * * *
- * Resource path constants *
- * * * * * * * * * * * * * * * * * * * * * * * * * * * */
-
- public static final String ICON_FILE_PATH = "icon.png";
- public static final String FXML_FILE_PATH = "ltc-view.fxml";
- public static final String APP_BUNDLE_PATH = "com.st4s1k.leagueteamcomp.ltc";
- public static final String FXML_BUNDLE_PATH = "com.st4s1k.leagueteamcomp.ltc-view";
-
- /* * * * * * * * * * * * * * * * * * * * * * * * * * * *
- * Window constants *
- * * * * * * * * * * * * * * * * * * * * * * * * * * * */
-
- private static final String WINDOW_TITLE = "League of Legends Team Composition Tool";
- private static final int WINDOW_WIDTH = 800;
- private static final int WINDOW_HEIGHT = 600;
- private static final boolean WINDOW_IS_RESIZABLE = false;
-
- /* * * * * * * * * * * * * * * * * * * * * * * * * * * *
- * Resource bundles *
- * * * * * * * * * * * * * * * * * * * * * * * * * * * */
-
- private final ResourceBundle ltcProperties = ResourceBundle.getBundle(APP_BUNDLE_PATH);
- private final ResourceBundle ltcViewProperties = ResourceBundle.getBundle(FXML_BUNDLE_PATH);
-
public static void main(String[] args) {
launch();
}
@@ -60,40 +45,84 @@ public static void main(String[] args) {
@Override
@SneakyThrows
public void start(Stage stage) {
- ChampionRepository.init(getChampionsFromUrl());
- FXMLLoader loader = new FXMLLoader(getClass().getResource(FXML_FILE_PATH), ltcViewProperties);
+ Thread.setDefaultUncaughtExceptionHandler(LeagueTeamCompApplication::showError);
+ getChampionsFromUrl();
+ FXMLLoader loader = new FXMLLoader(getClass().getResource(FXML_FILE_PATH), LTC_VIEW_PROPERTIES);
Parent root = loader.load();
LeagueTeamCompController controller = loader.getController();
controller.setStageAndSetupListeners(stage);
- controller.setCloseButtonAction(() -> closeProgram(stage, controller));
+ controller.setCloseButtonAction(() -> closeProgram(stage, controller));
controller.setMinimizeButtonAction(() -> stage.setIconified(true));
Scene scene = new Scene(root, WINDOW_WIDTH, WINDOW_HEIGHT);
+ stage.initStyle(StageStyle.TRANSPARENT);
+ scene.setFill(Color.TRANSPARENT);
stage.setScene(scene);
stage.getIcons().add(new Image(requireNonNull(getClass().getResourceAsStream(ICON_FILE_PATH))));
stage.setTitle(WINDOW_TITLE);
stage.setResizable(WINDOW_IS_RESIZABLE);
- stage.initStyle(StageStyle.UNDECORATED);
stage.show();
}
@SneakyThrows
- private CompletableFuture getChampionsFromUrl() {
+ private void getChampionsFromUrl() {
HttpRequest request = HttpRequest.newBuilder()
- .uri(new URI(ltcProperties.getString("champions-url")))
+ .uri(new URI(CHAMPIONS_URL))
.build();
- return HttpClient.newHttpClient()
+ HttpClient.newHttpClient()
.sendAsync(request, BodyHandlers.ofString())
- .thenApply(this::getChampions);
+ .thenApply(this::getChampions)
+ .thenAccept(ChampionRepository::init);
}
- private Champions getChampions(HttpResponse response) {
+ private ChampionsDTO getChampions(HttpResponse response) {
String json = MessageFormat.format("'{'\"champions\":{0}'}'", response.body());
- Champions champions = new Gson().fromJson(json, Champions.class);
+ ChampionsDTO champions = new Gson().fromJson(json, ChampionsDTO.class);
champions.getChampions().values()
.forEach(champion -> champion.setImage(new Image(champion.getIconUrl(), true)));
return champions;
}
+ private static void showError(Thread t, Throwable e) {
+ log.error("***Default exception handler***");
+ if (Platform.isFxApplicationThread()) {
+ showErrorDialog(e);
+ } else {
+ log.error("An unexpected error occurred in " + t);
+ }
+ }
+
+ @SneakyThrows
+ private static void showErrorDialog(Throwable e) {
+ StringWriter errorMsg = new StringWriter();
+ e.printStackTrace(new PrintWriter(errorMsg));
+ Stage dialog = new Stage();
+ dialog.initModality(Modality.APPLICATION_MODAL);
+ FXMLLoader loader = new FXMLLoader(LeagueTeamCompApplication.class.getResource("ltc-exception.fxml"));
+ Region root = loader.load();
+ LTCExceptionController controller = loader.getController();
+ controller.setStageAndSetupListeners(dialog);
+ int sceneWidth = 600;
+ int sceneHeight = 400;
+ if (e instanceof LTCException ltcException) {
+ sceneWidth = 300;
+ sceneHeight = 150;
+ controller.setErrorText(ltcException.getMessage());
+ } else {
+ controller.setErrorText(errorMsg.toString());
+ }
+ Scene scene = new Scene(root, sceneWidth, sceneHeight);
+ scene.setFill(Color.TRANSPARENT);
+ dialog.initStyle(StageStyle.TRANSPARENT);
+ dialog.setScene(scene);
+ dialog.setMinHeight(sceneHeight);
+ dialog.setMinWidth(sceneWidth);
+ Insets padding = root.getPadding();
+ double border = DoubleStream.of(padding.getTop(), padding.getRight(), padding.getBottom(), padding.getLeft())
+ .min().orElse(4);
+ ResizeHelper.addResizeListener(dialog, border);
+ dialog.show();
+ }
+
private void closeProgram(Stage stage, LeagueTeamCompController controller) {
log.info("Closing application...");
controller.stop();
diff --git a/src/main/java/com/st4s1k/leagueteamcomp/controller/LTCExceptionController.java b/src/main/java/com/st4s1k/leagueteamcomp/controller/LTCExceptionController.java
new file mode 100644
index 0000000..0b0aae7
--- /dev/null
+++ b/src/main/java/com/st4s1k/leagueteamcomp/controller/LTCExceptionController.java
@@ -0,0 +1,59 @@
+package com.st4s1k.leagueteamcomp.controller;
+
+import javafx.fxml.FXML;
+import javafx.fxml.Initializable;
+import javafx.scene.control.Button;
+import javafx.scene.control.TextArea;
+import javafx.scene.input.MouseButton;
+import javafx.scene.layout.HBox;
+import javafx.stage.Stage;
+
+import java.net.URL;
+import java.util.ResourceBundle;
+
+public class LTCExceptionController implements Initializable {
+
+ private static double xOffset = 0;
+ private static double yOffset = 0;
+
+ @FXML
+ private Button minimizeButton;
+ @FXML
+ private Button closeButton;
+ @FXML
+ private Button okButton;
+ @FXML
+ private TextArea errorMessage;
+ @FXML
+ private HBox windowTitleBar;
+
+ @Override
+ public void initialize(URL location, ResourceBundle resources) {
+ errorMessage.setEditable(false);
+ }
+
+ public void setStageAndSetupListeners(Stage stage) {
+ windowTitleBar.setOnMousePressed(event -> {
+ xOffset = stage.getX() - event.getScreenX();
+ yOffset = stage.getY() - event.getScreenY();
+ });
+ windowTitleBar.setOnMouseDragged(event -> {
+ stage.setX(event.getScreenX() + xOffset);
+ stage.setY(event.getScreenY() + yOffset);
+ });
+ windowTitleBar.setOnMouseClicked(event -> {
+ if (event.getButton().equals(MouseButton.PRIMARY)) {
+ if (event.getClickCount() == 2) {
+ stage.setMaximized(!stage.isMaximized());
+ }
+ }
+ });
+ okButton.setOnAction(event -> stage.close());
+ closeButton.setOnAction(actionEvent -> stage.close());
+ minimizeButton.setOnAction(actionEvent -> stage.setIconified(true));
+ }
+
+ public void setErrorText(String text) {
+ errorMessage.setText(text);
+ }
+}
diff --git a/src/main/java/com/st4s1k/leagueteamcomp/controller/LeagueTeamCompController.java b/src/main/java/com/st4s1k/leagueteamcomp/controller/LeagueTeamCompController.java
index 774f12a..e060dff 100644
--- a/src/main/java/com/st4s1k/leagueteamcomp/controller/LeagueTeamCompController.java
+++ b/src/main/java/com/st4s1k/leagueteamcomp/controller/LeagueTeamCompController.java
@@ -1,12 +1,20 @@
package com.st4s1k.leagueteamcomp.controller;
-import com.st4s1k.leagueteamcomp.model.SummonerRole;
-import com.st4s1k.leagueteamcomp.model.champion.AttributeRatings;
-import com.st4s1k.leagueteamcomp.model.champion.Champion;
+import com.st4s1k.leagueteamcomp.exceptions.LTCException;
+import com.st4s1k.leagueteamcomp.model.champion.AttributeRatingsDTO;
+import com.st4s1k.leagueteamcomp.model.champion.ChampionDTO;
import com.st4s1k.leagueteamcomp.model.champion.select.ChampSelectDTO;
import com.st4s1k.leagueteamcomp.model.champion.select.SlotDTO;
+import com.st4s1k.leagueteamcomp.model.champion.select.SummonerDTO;
import com.st4s1k.leagueteamcomp.model.champion.select.TeamDTO;
+import com.st4s1k.leagueteamcomp.model.enums.SummonerRoleEnum;
+import com.st4s1k.leagueteamcomp.model.interfaces.ChampionHolder;
+import com.st4s1k.leagueteamcomp.model.interfaces.Clearable;
+import com.st4s1k.leagueteamcomp.model.interfaces.SlotItem;
+import com.st4s1k.leagueteamcomp.service.ChampionSuggestionService;
import com.st4s1k.leagueteamcomp.service.LeagueTeamCompService;
+import com.st4s1k.leagueteamcomp.service.SummonerRoleListGeneratorService;
+import com.st4s1k.leagueteamcomp.utils.Utils;
import com.stirante.lolclient.ClientApi;
import com.stirante.lolclient.ClientConnectionListener;
import com.stirante.lolclient.ClientWebSocket;
@@ -14,7 +22,6 @@
import generated.LolChampSelectChampSelectSession;
import generated.LolSummonerSummoner;
import javafx.application.Platform;
-import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.css.PseudoClass;
@@ -22,10 +29,8 @@
import javafx.fxml.Initializable;
import javafx.scene.Node;
import javafx.scene.control.*;
-import javafx.scene.image.ImageView;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
-import javafx.scene.layout.GridPane;
import javafx.scene.text.Text;
import javafx.scene.text.TextFlow;
import javafx.stage.Stage;
@@ -35,26 +40,30 @@
import java.net.URL;
import java.util.*;
import java.util.function.DoubleBinaryOperator;
-import java.util.stream.IntStream;
-import java.util.stream.Stream;
-import static com.st4s1k.leagueteamcomp.model.SummonerRole.*;
-import static com.st4s1k.leagueteamcomp.service.SummonerRoleListGeneratorService.getCombinations;
+import static com.st4s1k.leagueteamcomp.model.enums.SummonerRoleEnum.*;
import static java.util.function.Predicate.not;
import static java.util.stream.Collectors.*;
-import static javafx.geometry.Pos.CENTER;
import static javafx.scene.control.SelectionMode.MULTIPLE;
import static org.controlsfx.control.textfield.TextFields.bindAutoCompletion;
@Slf4j
public class LeagueTeamCompController implements Initializable {
- private static double xOffset = 0;
- private static double yOffset = 0;
+ /* * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * App controls *
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * */
- private LeagueTeamCompService service;
+ @FXML
+ private TabPane tabPane;
+ @FXML
+ private Button minimizeButton;
+ @FXML
+ private Button closeButton;
- /* Role Compositions */
+ /* * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * Role Compositions *
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * */
private static final boolean DEFAULT_CHECKBOX_STATE = true;
@@ -132,7 +141,9 @@ public class LeagueTeamCompController implements Initializable {
@FXML
private Button resetButton;
- /* Champion Suggestions */
+ /* * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * Champion Suggestions *
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * */
private static final PseudoClass CHAMPION_STAT_GOOD_PSEUDO_CLASS = PseudoClass.getPseudoClass("good");
private static final PseudoClass CHAMPION_STAT_BAD_PSEUDO_CLASS = PseudoClass.getPseudoClass("bad");
@@ -143,58 +154,85 @@ public class LeagueTeamCompController implements Initializable {
@FXML
private TextField allySearchField;
@FXML
- private ListView allyListView;
+ private ListView> allyListView;
@FXML
- private ListView allyBanListView;
+ private ListView> allyBanListView;
+ @FXML
+ private ListView>> suggestionsListView;
@FXML
private TextFlow allyTeamResultTextFlow;
@FXML
private TextField enemySearchField;
@FXML
- private ListView enemyListView;
+ private ListView> enemyListView;
@FXML
- private ListView enemyBanListView;
+ private ListView> enemyBanListView;
@FXML
private TextFlow enemyTeamResultTextFlow;
@FXML
- private GridPane gridPane;
- @FXML
- private TabPane tabPane;
- @FXML
- private Button minimizeButton;
- @FXML
- private Button closeButton;
+ private ToggleButton manualModeToggle;
+
+ /* * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * Controller fields *
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * */
+
+ private static double xOffset = 0;
+ private static double yOffset = 0;
+
+ private final LeagueTeamCompService service = LeagueTeamCompService.getInstance();
+ private final ChampionSuggestionService suggestionService = ChampionSuggestionService.getInstance();
+ private final SummonerRoleListGeneratorService roleListGeneratorService = SummonerRoleListGeneratorService.getInstance();
+ private final ChampSelectDTO champSelect = new ChampSelectDTO();
+ private final ClientApi api = new ClientApi();
- private ChampSelectDTO champSelect;
private ClientWebSocket socket;
- private ClientApi api;
+
+ public void setStageAndSetupListeners(Stage stage) {
+ tabPane.setOnMousePressed(event -> {
+ xOffset = stage.getX() - event.getScreenX();
+ yOffset = stage.getY() - event.getScreenY();
+ });
+ tabPane.setOnMouseDragged(event -> {
+ stage.setX(event.getScreenX() + xOffset);
+ stage.setY(event.getScreenY() + yOffset);
+ });
+ }
+
+ public void setCloseButtonAction(Runnable closeAction) {
+ closeButton.setOnAction(actionEvent -> closeAction.run());
+ }
+
+ public void setMinimizeButtonAction(Runnable minimizeAction) {
+ minimizeButton.setOnAction(actionEvent -> minimizeAction.run());
+ }
+
+ public void stop() {
+ api.stop();
+ }
@Override
@SneakyThrows
public void initialize(URL url, ResourceBundle resourceBundle) {
- service = LeagueTeamCompService.getInstance();
- api = new ClientApi();
+ registerLeagueClientListeners();
initializeRoleCompositions();
- registerLCUListeners();
initializeChampionSuggestions();
}
- public void stop() {
- api.stop();
- }
-
@SneakyThrows
- private void registerLCUListeners() {
+ private void registerLeagueClientListeners() {
+ log.info("Registering League Client listeners");
api.addClientConnectionListener(new ClientConnectionListener() {
@Override
public void onClientConnected() {
+ log.info("League Client connected");
registerSocketListener();
}
@Override
public void onClientDisconnected() {
+ log.info("League Client disconnected");
if (socket != null) {
socket.close();
}
@@ -204,6 +242,7 @@ public void onClientDisconnected() {
@SneakyThrows
private void registerSocketListener() {
+ log.info("Registering socket listener");
if (!api.isAuthorized()) {
log.warn("Not logged in!");
return;
@@ -212,60 +251,73 @@ private void registerSocketListener() {
socket.setSocketListener(new ClientWebSocket.SocketListener() {
@Override
public void onEvent(ClientWebSocket.Event event) {
- Platform.runLater(() -> {
- if (event.getEventType().equals("Update") &&
- event.getUri().equals("/lol-champ-select/v1/session") &&
- event.getData() instanceof LolChampSelectChampSelectSession session) {
- updateTeam(session.myTeam, champSelect.getAllyTeam());
- updateBans(session.bans.myTeamBans, champSelect.getAllyBanList());
- updateTeam(session.theirTeam, champSelect.getEnemyTeam());
- updateBans(session.bans.theirTeamBans, champSelect.getEnemyBanList());
- }
- });
+ if (event.getEventType().equals("Update") &&
+ event.getUri().equals("/lol-champ-select/v1/session") &&
+ event.getData() instanceof LolChampSelectChampSelectSession session) {
+ Platform.runLater(() -> LeagueTeamCompController.this.onChampSelectUpdate(session));
+ }
}
@Override
public void onClose(int code, String reason) {
- log.warn("Socket closed, reason: " + reason);
+ log.warn("Socket closed, code: {}, reason: {}", code, reason);
}
});
}
- private void updateBans(List bans, TeamDTO teamBans) {
- System.out.println("team: " + teamBans.getTeam());
- System.out.println("response bans: " + bans);
+ private void onChampSelectUpdate(LolChampSelectChampSelectSession session) {
+ if (!manualModeToggle.isSelected()) {
+ updateTeam(champSelect.getAllyTeam(), session.myTeam, session.bans.myTeamBans);
+ updateTeam(champSelect.getEnemyTeam(), session.theirTeam, session.bans.theirTeamBans);
+ }
+ }
+
+ private void updateTeam(
+ TeamDTO team,
+ List playerSelectionList,
+ List bans
+ ) {
+ playerSelectionList.forEach(playerSelection -> service.findChampionDataById(playerSelection.championId)
+ .ifPresent(champion -> populateSummonerSlot(playerSelection, team, champion)));
+ updateBans(team, bans);
+ }
+
+ private void updateBans(TeamDTO team, List bans) {
+ List bannedChampionIdsBefore = team.getBannedChampionIds();
+ boolean shouldLog = Utils.notSame(bannedChampionIdsBefore, bans);
+ if (shouldLog) {
+ log.debug("team: {}", team.getTeamSide());
+ log.debug("response bans: {}", bans);
+ }
bans.stream()
- .filter(championId -> teamBans.getSlots().stream()
- .map(SlotDTO::getChampion)
- .flatMap(Optional::stream)
- .map(Champion::getId)
+ .filter(championId -> team.getBannedChampions().stream()
+ .map(ChampionDTO::getId)
.noneMatch(championId::equals))
.map(service::findChampionDataById)
.flatMap(Optional::stream)
- .forEach(champion -> teamBans.getSlots().stream()
+ .forEach(champion -> team.getBans().stream()
.filter(SlotDTO::isChampionNotSelected)
.findFirst()
.ifPresent(slot -> slot.setChampion(champion)));
- System.out.println("teamBans: " + teamBans.getSlots().stream()
- .map(SlotDTO::getChampion)
- .flatMap(Optional::stream)
- .map(Champion::getId)
- .toList() + "\n");
+ if (shouldLog) {
+ log.debug("teamBans: {}\n", team.getBannedChampionIds());
+ }
}
- private void updateTeam(List session, TeamDTO team) {
- session.forEach(playerSelection -> service.findChampionDataById(playerSelection.championId)
- .ifPresent(champion -> {
- int slotId = playerSelection.cellId.intValue();
- int slotIndex = slotId % 5;
- SlotDTO slot = team.getSlot(slotIndex);
- slot.setSlotId(slotId);
- String summonerName = getSummonerName(playerSelection.summonerId);
- slot.setSummonerName(summonerName);
- slot.setChampion(champion);
- }));
+ private void populateSummonerSlot(
+ LolChampSelectChampSelectPlayerSelection playerSelection,
+ TeamDTO team,
+ ChampionDTO champion
+ ) {
+ int slotId = playerSelection.cellId.intValue();
+ int slotIndex = slotId % 5;
+ team.getSlot(slotIndex).getItem().ifPresent(summoner -> {
+ summoner.setSlotId(slotId);
+ summoner.setSummonerName(getSummonerName(playerSelection.summonerId));
+ summoner.setChampion(champion);
+ });
}
@SneakyThrows
@@ -278,105 +330,167 @@ private String getSummonerName(Long summonerId) {
.orElse("");
}
- protected void onGenerateButtonClick() {
+ private void initializeRoleCompositions() {
+ log.info("Initializing role compositions");
+ textArea.setEditable(false);
+ resetAllCheckboxes();
+ generateButton.setOnAction(event -> onGenerateButtonClick());
+ resetButton.setOnAction(event -> onResetButtonClick());
+ }
+
+ private void onGenerateButtonClick() {
textArea.clear();
- var summonerNames = List.of(
- tf1.getText(),
- tf2.getText(),
- tf3.getText(),
- tf4.getText(),
- tf5.getText()
- );
- var playersToRoles = Stream.of(
+ Map> playersToRoles = getPlayersToRolesMap();
+
+ List