+ * Attempts to create the application data directory if
+ * one does not exist.
+ *
+ */
+ public static void ensureAppDataDirectoryExists() {
+ Path dir = getAppDataDirectory();
+ try {
+ Files.createDirectories(dir);
+
+ Path storageFile = dir.resolve("storage.json");
+ if (!Files.exists(storageFile)) {
+ Files.createFile(storageFile);
+ }
+ } catch (IOException e) {
+ throw new RuntimeException("Could not create app data directory: " + dir, e);
+ }
+ }
+
+ /**
+ * Method for obtaining system-specific directory for application data.
+ *
+ * @return Path object of the found directory.
+ */
+ private static Path getAppDataDirectory() {
+ String userHome = System.getProperty("user.home");
+ String os = System.getProperty("os.name").toLowerCase();
+
+ // AppData folder for windows
+ if (os.contains("win")) {
+ String appData = System.getenv("APPDATA");
+
+ if (appData != null) {
+ return Paths.get(appData, STORAGE_FOLDER);
+ }
+ // If AppData not a environmental variable, sets to user directory.
+ return Paths.get(userHome, STORAGE_FOLDER);
+
+ }
+ // Library folder in MacOS
+ if (os.contains("mac")) {
+ return Paths.get(userHome, "Library", "Application Support", STORAGE_FOLDER);
+ }
+
+ // All linux and unix-like systems.
+ return Paths.get(userHome, ".local", "share", STORAGE_FOLDER);
+
+ }
+
+}
From 302f33c5773c2e863b4238ab87670d52c0960441 Mon Sep 17 00:00:00 2001
From: pawelsa
Date: Sat, 9 May 2026 23:44:15 +0200
Subject: [PATCH 7/8] feat(SessionManager): Class for managing current session
data + persistance.
---
.../idi/idatt/storage/SessionManager.java | 129 ++++++++++++++++++
1 file changed, 129 insertions(+)
create mode 100644 src/main/java/edu/ntnu/idi/idatt/storage/SessionManager.java
diff --git a/src/main/java/edu/ntnu/idi/idatt/storage/SessionManager.java b/src/main/java/edu/ntnu/idi/idatt/storage/SessionManager.java
new file mode 100644
index 0000000..d2f1e55
--- /dev/null
+++ b/src/main/java/edu/ntnu/idi/idatt/storage/SessionManager.java
@@ -0,0 +1,129 @@
+package edu.ntnu.idi.idatt.storage;
+
+import java.io.FileNotFoundException;
+import java.io.FileReader;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.Writer;
+import java.lang.reflect.Type;
+import java.nio.file.Files;
+import java.util.ArrayList;
+import java.util.List;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonIOException;
+import com.google.gson.JsonSyntaxException;
+import com.google.gson.reflect.TypeToken;
+
+import edu.ntnu.idi.idatt.model.Exchange;
+import edu.ntnu.idi.idatt.model.player.Player;
+import edu.ntnu.idi.idatt.session.UserSession;
+import edu.ntnu.idi.idatt.session.UserSession.SessionBundle;
+
+/**
+ * Class for managing user sessions.
+ *
+ *
+ * Utilizes gson serialization and deserialization to manage
+ * player and game state.
+ */
+public class SessionManager {
+
+ // Gson type to list format. Instead of using wrapper class.
+ private static Type SESSION_BUNDLE_TYPE = new TypeToken>() {
+ }.getType();
+
+ private static Gson gson = new GsonBuilder()
+ .setPrettyPrinting()
+ .create();
+
+ // Static initiator to ensure persistent storage file.
+ static {
+ StorageFile.ensureAppDataDirectoryExists();
+ }
+
+ /**
+ * Method for setting new session.
+ *
+ * @see UserSession
+ */
+ public static void newSession(Player player, Exchange exchange) {
+ UserSession.getInstance().setPlayer(player);
+ UserSession.getInstance().setExchange(exchange);
+ }
+
+ /**
+ * Method for saving current session.
+ */
+ public static void saveSession() {
+ // don't save if current session is null accidentally
+ if (UserSession.getInstance().getPlayer() == null || UserSession.getInstance().getExchange() == null) {
+ return;
+ }
+
+ // Load all sessions
+ List bundles = loadAllSessions();
+
+ try (Writer writer = new FileWriter(StorageFile.getStorageFile().toFile())) {
+
+ // Append current session
+ SessionBundle existing = bundles.stream()
+ .filter(s -> s.getPlayer().getName().equals(UserSession.getInstance().getPlayer().getName()))
+ .findFirst().orElse(null);
+
+ if (existing != null) {
+ bundles.set(bundles.indexOf(existing), UserSession.getInstance().getSession());
+ } else {
+ bundles.add(UserSession.getInstance().getSession());
+ }
+
+ gson.toJson(bundles, writer);
+
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to save current session!", e);
+ }
+ }
+
+ /**
+ * Method for loading a user session by player name.
+ *
+ * @return if session was found or not.
+ */
+ public static boolean loadSession(String playerName) {
+
+ List bundles = loadAllSessions();
+
+ for (SessionBundle session : bundles) {
+ if (session.getPlayer().getName().equals(playerName)) {
+ UserSession.getInstance().setPlayer(session.getPlayer());
+ UserSession.getInstance().setExchange(session.getExchange());
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Method for serialization of all sessions.
+ *
+ * @return List or empty list if none entires were made.
+ */
+ private static List loadAllSessions() {
+ try {
+ if (!Files.exists(StorageFile.getStorageFile()) || Files.size(StorageFile.getStorageFile()) == 0) {
+ return new ArrayList<>();
+ }
+ } catch (IOException e) {
+ return new ArrayList<>();
+ }
+
+ try {
+ return gson.fromJson(new FileReader(StorageFile.getStorageFile().toString()), SESSION_BUNDLE_TYPE);
+ } catch (JsonSyntaxException | JsonIOException | FileNotFoundException e) {
+ return new ArrayList<>();
+ }
+
+ }
+
+}
From 03a918ecd937a6930aa3c138e6a83a8e790238c3 Mon Sep 17 00:00:00 2001
From: pawelsa
Date: Sat, 9 May 2026 23:45:03 +0200
Subject: [PATCH 8/8] feat(Start-MVC): Added persistent storage to start view.
---
.../idi/idatt/view/entry/StartController.java | 48 +++++++++++++++----
.../ntnu/idi/idatt/view/entry/StartModel.java | 8 ++++
.../ntnu/idi/idatt/view/entry/StartView.java | 6 ++-
3 files changed, 53 insertions(+), 9 deletions(-)
diff --git a/src/main/java/edu/ntnu/idi/idatt/view/entry/StartController.java b/src/main/java/edu/ntnu/idi/idatt/view/entry/StartController.java
index 52f7eeb..04eea82 100644
--- a/src/main/java/edu/ntnu/idi/idatt/view/entry/StartController.java
+++ b/src/main/java/edu/ntnu/idi/idatt/view/entry/StartController.java
@@ -1,10 +1,15 @@
package edu.ntnu.idi.idatt.view.entry;
import java.io.File;
+import java.io.IOException;
import java.math.BigDecimal;
+import java.util.List;
+import edu.ntnu.idi.idatt.model.Exchange;
+import edu.ntnu.idi.idatt.model.market.Stock;
import edu.ntnu.idi.idatt.model.player.Player;
-import edu.ntnu.idi.idatt.session.UserSession;
+import edu.ntnu.idi.idatt.storage.SessionManager;
+import edu.ntnu.idi.idatt.storage.StockParser;
import edu.ntnu.idi.idatt.view.SceneManager;
import edu.ntnu.idi.idatt.view.components.AbstractController;
import edu.ntnu.idi.idatt.view.primary.MainView;
@@ -28,14 +33,31 @@ public void obtainCSVFile() {
public void initializeGame() {
model.getError().set(" "); // Empty buffers
- if (model.getName().get() == null || model.getBalance().get() == null) {
- model.getError().set("Name and/or balance fields can't be empty");
+ if (model.getName().get() == null) {
+ model.getError().set("Name field can't be empty");
return;
}
- if (model.getName().get().isBlank() ||
- model.getBalance().get().isBlank()) {
- model.getError().set("Name and/or balance fields can't be empty");
+ if (model.getName().get().isBlank()) {
+ model.getError().set("Name field can't be empty!");
+ return;
+ }
+
+ boolean loadResult = SessionManager.loadSession(model.getName().get());
+ if (loadResult) {
+ SceneManager.switchTo(new MainView().getInstance());
+ return;
+ } else {
+ model.isNewGame().set(true);
+ }
+
+ if (model.getBalance().get() == null) {
+ model.getError().set("Balance field can't be empty");
+ return;
+ }
+
+ if (model.getBalance().get().isBlank()) {
+ model.getError().set("Balance field can't be empty!");
return;
}
@@ -52,9 +74,19 @@ public void initializeGame() {
return;
}
- UserSession.getInstance().setPlayer(new Player(model.getName().get(), balance));
- SceneManager.switchTo(new MainView().getInstance());
+ List stocks;
+ try {
+ stocks = StockParser.load(csv.getPath());
+ } catch (IOException e) {
+ model.getError().set(e.getMessage());
+ return;
+ }
+ Player player = new Player(model.getName().get(), balance);
+ Exchange exchange = new Exchange(player.getName(), stocks);
+ SessionManager.newSession(player, exchange);
+ SessionManager.saveSession();
+ SceneManager.switchTo(new MainView().getInstance());
}
}
diff --git a/src/main/java/edu/ntnu/idi/idatt/view/entry/StartModel.java b/src/main/java/edu/ntnu/idi/idatt/view/entry/StartModel.java
index e1b0f92..3e4c4ed 100644
--- a/src/main/java/edu/ntnu/idi/idatt/view/entry/StartModel.java
+++ b/src/main/java/edu/ntnu/idi/idatt/view/entry/StartModel.java
@@ -1,6 +1,8 @@
package edu.ntnu.idi.idatt.view.entry;
import edu.ntnu.idi.idatt.view.components.Model;
+import javafx.beans.property.BooleanProperty;
+import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
@@ -11,6 +13,8 @@ public class StartModel implements Model {
private final StringProperty error = new SimpleStringProperty();
private final StringProperty fileName = new SimpleStringProperty();
+ private final BooleanProperty newGame = new SimpleBooleanProperty();
+
public StringProperty getName() {
return name;
}
@@ -27,4 +31,8 @@ public StringProperty getFileName() {
return fileName;
}
+ public BooleanProperty isNewGame() {
+ return newGame;
+ }
+
}
diff --git a/src/main/java/edu/ntnu/idi/idatt/view/entry/StartView.java b/src/main/java/edu/ntnu/idi/idatt/view/entry/StartView.java
index 60180b1..f32cc87 100644
--- a/src/main/java/edu/ntnu/idi/idatt/view/entry/StartView.java
+++ b/src/main/java/edu/ntnu/idi/idatt/view/entry/StartView.java
@@ -24,6 +24,7 @@ public class StartView extends AbstractView {
private Label errorLabel;
// Global buttons for controller implementation
+ private VBox newGameWrapper;
private Button csvButton;
private Button startButton;
@@ -44,6 +45,7 @@ public Parent createContent() {
// Create and style wrappers
VBox wrapper = new VBox();
+ newGameWrapper = new VBox();
HBox playerWrapper = new HBox();
HBox balanceWrapper = new HBox();
playerWrapper.setAlignment(Pos.CENTER_LEFT);
@@ -98,7 +100,8 @@ public Parent createContent() {
VBox.setVgrow(filler, Priority.ALWAYS);
// Wrap components together and adjust positioning
- wrapper.getChildren().addAll(List.of(playerWrapper, balanceWrapper, csvWrapper));
+ newGameWrapper.getChildren().addAll(balanceWrapper, csvWrapper);
+ wrapper.getChildren().addAll(playerWrapper, newGameWrapper);
wrapper.setAlignment(Pos.BASELINE_LEFT);
root.getChildren().addAll(List.of(title, wrapper, filler, errorLabel, startButton));
@@ -111,6 +114,7 @@ public void setModel(StartModel model) {
this.balanceField.textProperty().bindBidirectional(model.getBalance());
this.errorLabel.textProperty().bind(model.getError());
this.fileLabel.textProperty().bind(model.getFileName());
+ this.newGameWrapper.visibleProperty().bind(model.isNewGame());
}
public void setController(StartController controller) {