diff --git a/FSG_FILE.fsg b/FSG_FILE.fsg new file mode 100644 index 0000000000..6402376973 Binary files /dev/null and b/FSG_FILE.fsg differ diff --git a/FSG_FILE.xml b/FSG_FILE.xml new file mode 100644 index 0000000000..e69de29bb2 diff --git a/XML_FILE.xml b/XML_FILE.xml new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/net/sf/freecol/common/io/FreeColSQLReader.java b/src/net/sf/freecol/common/io/FreeColSQLReader.java new file mode 100644 index 0000000000..a7c6370097 --- /dev/null +++ b/src/net/sf/freecol/common/io/FreeColSQLReader.java @@ -0,0 +1,82 @@ +/** + * Copyright (C) 2002-2022 The FreeCol Team + * + * This file is part of FreeCol. + * + * FreeCol is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * FreeCol is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with FreeCol. If not, see . + */ + +package net.sf.freecol.common.io; +import java.io.Closeable; +import java.io.IOException; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.logging.Level; +import java.util.logging.Logger; + + +/** + * This code creates a `FreeColSQLReader` class that connects to an + * SQLite database and is capable of executing queries. + * It maintains a connection to the SQLite file and a statement object used to + * execute the queries. The `ReadScopeType` is also included in the implementation, + * similar to the XML implementation. + */ +public class FreeColSQLReader implements Closeable { + + private static final Logger logger = Logger.getLogger(FreeColSQLReader.class.getName()); + + /** + * Enum representing the scope of the data reading. + */ + public enum ReadScopeType { + CLIENT, // Only the client-visible information + SERVER, // Full server-visible information + LOAD // Absolutely everything needed to load the game state + } + + private Connection connection; + private Statement statement; + private final ReadScopeType readScope; + + public FreeColSQLReader(String dbPath, ReadScopeType scope) throws SQLException { + this.connection = DriverManager.getConnection("jdbc:sqlite:" + dbPath); + this.statement = connection.createStatement(); + this.readScope = (scope == null) ? ReadScopeType.LOAD : scope; + } + + /** + * Executes a query and returns the result set. + * + * @param query SQL query to execute. + * @return ResultSet containing the result of the query. + * @throws SQLException If the query execution fails. + */ + public ResultSet executeQuery(String query) throws SQLException { + return statement.executeQuery(query); + } + + @Override + public void close() { + try { + if (statement != null) statement.close(); + if (connection != null) connection.close(); + } catch (SQLException e) { + logger.log(Level.WARNING, "Failed to close resources", e); + } + } +} \ No newline at end of file diff --git a/src/net/sf/freecol/common/io/FreeColSQLWriter.java b/src/net/sf/freecol/common/io/FreeColSQLWriter.java new file mode 100644 index 0000000000..6918c3e19d --- /dev/null +++ b/src/net/sf/freecol/common/io/FreeColSQLWriter.java @@ -0,0 +1,133 @@ +/** + * Copyright (C) 2002-2022 The FreeCol Team + * + * This file is part of FreeCol. + * + * FreeCol is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * FreeCol is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with FreeCol. If not, see . + */ + +package net.sf.freecol.common.io; + +import java.io.Closeable; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.util.logging.Level; +import java.util.logging.Logger; + + +/** + * `FreeColSQLWriter` class to save game data into an SQLite database. + * It consists of the `saveGameData` method that saves general game data, + * player data, turn data, and tile data. Additional game components can be saved + * in a similar manner using prepared statements. The `close()` + * method ensures that the database connection is closed when done. + */ +public class FreeColSQLWriter implements Closeable { + + private static final Logger logger = Logger.getLogger(FreeColSQLWriter.class.getName()); + + /** + * Enum representing the scope of the data writing. + */ + public enum WriteScopeType { + CLIENT, // Only the client-visible information + SERVER, // Full server-visible information + SAVE // Absolutely everything needed to save the game state + }; + + private Connection connection; + private WriteScopeType writeScope; + + public FreeColSQLWriter(String dbPath, WriteScopeType scope) throws SQLException { + this.connection = DriverManager.getConnection("jdbc:sqlite:" + dbPath); + this.writeScope = (scope == null) ? WriteScopeType.SAVE : scope; + } + + /** + * Executes an SQL update statement. + * + * @param update SQL update statement to execute. + * @throws SQLException If the statement execution fails. + */ + public void executeUpdate(String update) throws SQLException { + try (PreparedStatement pstmt = connection.prepareStatement(update)) { + pstmt.executeUpdate(); + } + } + + /** + * Save game data to the SQLite database. + * + * @param game The Game object to save. + * @throws SQLException If any error occurs during saving. + */ + public void saveGameData(Game game) throws SQLException { + // Save general game data + String sqlGame = "INSERT OR REPLACE INTO game (id, options) VALUES(?, ?)"; + try (PreparedStatement pstmt = connection.prepareStatement(sqlGame)) { + pstmt.setInt(1, game.getId()); + pstmt.setString(2, game.getGameOptions().toString()); + pstmt.executeUpdate(); + } + + // Save player data + String sqlPlayer = "INSERT OR REPLACE INTO player (id, name, nation) VALUES(?, ?, ?)"; + for (Player player : game.getPlayers()) { + try (PreparedStatement pstmt = connection.prepareStatement(sqlPlayer)) { + pstmt.setInt(1, player.getId()); + pstmt.setString(2, player.getName()); + pstmt.setString(3, player.getNation().getName()); + pstmt.executeUpdate(); + } + } + + // Save turn data + String sqlTurn = "UPDATE game SET turn = ? WHERE id + // Save turn data + String sqlTurn = "UPDATE game SET turn = ? WHERE id = ?"; + try (PreparedStatement pstmt = connection.prepareStatement(sqlTurn)) { + pstmt.setInt(1, game.getTurn()); + pstmt.setInt(2, game.getId()); + pstmt.executeUpdate(); + } + + // Save tile data + String sqlTile = "INSERT OR REPLACE INTO tile (x, y, terrain_type, owner, game_id) VALUES(?, ?, ?, ?, ?)"; + for (Tile tile : game.getMap().getAllTiles()) { + try (PreparedStatement pstmt = connection.prepareStatement(sqlTile)) { + pstmt.setInt(1, tile.getX()); + pstmt.setInt(2, tile.getY()); + pstmt.setString(3, tile.getTerrainType().getName()); + pstmt.setString(4, tile.getOwner().getName()); + pstmt.setInt(5, game.getId()); + pstmt.executeUpdate(); + } + } + + // Additional game components can be saved similarly with prepared statements + } + + @Override + public void close() { + try { + if (connection != null) { + connection.close(); + } + } catch (SQLException e) { + logger.log(Level.WARNING, "Failed to close resources", e); + } + } +} \ No newline at end of file diff --git a/src/net/sf/freecol/common/io/FreeColXMLWriter.java b/src/net/sf/freecol/common/io/FreeColXMLWriter.java index 8926892302..1dc44030f2 100644 --- a/src/net/sf/freecol/common/io/FreeColXMLWriter.java +++ b/src/net/sf/freecol/common/io/FreeColXMLWriter.java @@ -385,6 +385,7 @@ public void writeAttribute(String attributeName, Enum value) */ public void writeAttribute(String attributeName, Object value) throws XMLStreamException { + logger.log(Level.WARNING, "Writing attribute:" + attributeName + ", with value of: " + String.valueOf(value)); xmlStreamWriter.writeAttribute(attributeName, String.valueOf(value)); } diff --git a/src/net/sf/freecol/common/model/sql/generate_sql_tables.sql b/src/net/sf/freecol/common/model/sql/generate_sql_tables.sql new file mode 100644 index 0000000000..0ab2491902 --- /dev/null +++ b/src/net/sf/freecol/common/model/sql/generate_sql_tables.sql @@ -0,0 +1,84 @@ +-- Create game table +CREATE TABLE IF NOT EXISTS game ( + id INTEGER PRIMARY KEY, + difficulty TEXT, + mapFile TEXT, + gameOptions TEXT, + language TEXT, + turns INTEGER, + graphicalOptions TEXT, + soundOptions TEXT, + turn INTEGER +); + +-- Create player table +CREATE TABLE IF NOT EXISTS player ( + id INTEGER PRIMARY KEY, + game_id INTEGER, + name TEXT, + color TEXT, + flag TEXT, + playerPreferences TEXT, + FOREIGN KEY (game_id) REFERENCES game (id) ON DELETE CASCADE +); + +-- Create native table +CREATE TABLE IF NOT EXISTS natives ( + id INTEGER PRIMARY KEY, + game_id INTEGER, + name TEXT, + color TEXT, + type TEXT, + FOREIGN KEY (game_id) REFERENCES game (id) ON DELETE CASCADE +); + +-- Create faction table +CREATE TABLE IF NOT EXISTS factions ( + id INTEGER PRIMARY KEY, + game_id INTEGER, + name TEXT, + FOREIGN KEY (game_id) REFERENCES game (id) ON DELETE CASCADE +); + +-- ... The existing unit, city, and tile table creation statements remain unchanged + +-- Create building table +CREATE TABLE IF NOT EXISTS building ( + id INTEGER PRIMARY KEY, + game_id INTEGER, + city_id INTEGER, + type TEXT, + x INTEGER, + y INTEGER, + FOREIGN KEY (game_id) REFERENCES game (id) ON DELETE CASCADE, + FOREIGN KEY (city_id) REFERENCES city (id) ON DELETE CASCADE +); + +-- Create resource (goods) table +CREATE TABLE IF NOT EXISTS goods ( + id INTEGER PRIMARY KEY, + game_id INTEGER, + type TEXT, + available INTEGER, + FOREIGN KEY (game_id) REFERENCES game (id) ON DELETE CASCADE +); + +-- Create highscore table +CREATE TABLE IF NOT EXISTS highScores ( + id INTEGER PRIMARY KEY, + game_id INTEGER, + player_id INTEGER, + score INTEGER, + FOREIGN KEY (game_id) REFERENCES game (id) ON DELETE CASCADE, + FOREIGN KEY (player_id) REFERENCES player (id) ON DELETE CASCADE +); + +-- Create foundingFathers table +CREATE TABLE IF NOT EXISTS foundingFathers ( + id INTEGER PRIMARY KEY, + game_id INTEGER, + name TEXT, + bonus TEXT, + improvement TEXT, + FOREIGN KEY (game_id) REFERENCES game (id) ON DELETE CASCADE +); diff --git a/src/net/sf/freecol/tools/FSGConverter.java b/src/net/sf/freecol/tools/FSGConverter.java index 874beb264e..2a935799e5 100644 --- a/src/net/sf/freecol/tools/FSGConverter.java +++ b/src/net/sf/freecol/tools/FSGConverter.java @@ -118,9 +118,9 @@ private void convertToXML(InputStream ins, OutputStream outs) return; } in.reset(); - if (!"= 2 && args[0].endsWith("output:xml")) { - File in = new File(args[1]); + String[] actual_args = {"FSG_FILE:xml", "5c812fa4_Голландцы_1494.fsg"}; + if (actual_args.length >= 2 && actual_args[0].endsWith("output:xml")) { + File in = new File(actual_args[1]); if (!in.exists()) { printUsage(); System.exit(1); } File out; - if (args.length >= 3) { - out = new File(args[2]); + if (actual_args.length >= 3) { + out = new File(actual_args[2]); } else { String name = in.getName(); String filename = name.replaceAll("." + FreeCol.FREECOL_SAVE_EXTENSION, ".xml"); diff --git a/src/net/sf/freecol/tools/SaveGameValidator.java b/src/net/sf/freecol/tools/SaveGameValidator.java index e6240b78b2..eb3094b792 100644 --- a/src/net/sf/freecol/tools/SaveGameValidator.java +++ b/src/net/sf/freecol/tools/SaveGameValidator.java @@ -40,22 +40,35 @@ /** * Validate a saved game. */ +import java.io.File; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.ResultSet; +import java.sql.Statement; +import java.util.ArrayList; +import java.util.List; + public class SaveGameValidator { + /** + * Main method to validate game save. + * + * @param args command line arguments containing the SQLite database file paths + * @throws Exception if there is any error during execution + */ public static void main(String[] args) throws Exception { - - SchemaFactory factory = SchemaFactory.newInstance("http://www.w3.org/2001/XMLSchema"); - File schemaLocation = new File("schema/data/data-savedGame.xsd"); - Schema schema = factory.newSchema(schemaLocation); - Validator saveGameValidator = schema.newValidator(); + // Load SQLite JDBC driver + Class.forName("org.sqlite.JDBC"); List allFiles = new ArrayList<>(); for (String name : args) { File file = new File(name); if (file.exists()) { if (file.isDirectory()) { - allFiles.addAll(FreeColDirectories.getSavegameFileList(file)); - } else if (FreeColDirectories.saveGameFilter.test(file)) { + // You need to implement getSQLiteDatabaseFileList method to list SQLite files in the directory + allFiles.addAll(getSQLiteDatabaseFileList(file)); + } else { + // You can use any custom file filter, if required allFiles.add(file); } } @@ -63,25 +76,118 @@ public static void main(String[] args) throws Exception { int ret = 0; for (File file : allFiles) { - //System.out.println("Processing file " + file.getPath()); - try { - FreeColSavegameFile mapFile = new FreeColSavegameFile(file); - try (InputStream in = mapFile.getSavegameInputStream()) { - saveGameValidator.validate(new StreamSource(in)); - } + System.out.println("Processing file " + file.getPath()); + try (Connection connection = DriverManager.getConnection("jdbc:sqlite:" + file.getPath())) { + ret = validateSaveGame(connection) ? 0 : Math.max(ret, 1); System.out.println("Successfully validated " + file.getName()); - } catch (SAXParseException e) { - System.out.println(e.getMessage() - + " at line=" + e.getLineNumber() - + " column=" + e.getColumnNumber()); - ret = Math.max(ret, 1); - } catch (IOException | SAXException e) { - System.out.println("Failed to read " + file.getName()); + } catch (Exception e) { + System.out.println("Failed to read or validate " + file.getName()); ret = 2; } } System.exit(ret); } -} + /** + * Validates game save based on the SQLite database file. + * + * @param connection SQLite database connection + * @return true if the game save is valid, false otherwise + * @throws Exception if any error occurs during validation + */ + private static boolean validateSaveGame(Connection connection) throws Exception { + Statement stmt = connection.createStatement(); + ResultSet rs; + + // Validate game table + rs = stmt.executeQuery("SELECT * FROM game"); + if (!rs.next()) { + System.out.println("No game data found in the 'game' table"); + return false; + } + rs.close(); + + // Validate player table + rs = stmt.executeQuery("SELECT * FROM player"); + if (!rs.next()) { + System.out.println("No player data found in the 'player' table"); + return false; + } + rs.close(); + + + // Validate natives table + rs = stmt.executeQuery("SELECT * FROM natives"); + if (!rs.next()) { + System.out.println("No native data found in the 'natives' table"); + return false; + } + rs.close(); + + // Validate factions table + rs = stmt.executeQuery("SELECT * FROM factions"); + if (!rs.next()) { + System.out.println("No faction data found in the 'factions' table"); + return false; + } + rs.close(); + + // Validate building table + rs = stmt.executeQuery("SELECT * FROM building"); + if (!rs.next()) { + System.out.println("No building data found in the 'building' table"); + return false; + } + rs.close(); + // Validate goods table + rs = stmt.executeQuery("SELECT * FROM goods"); + if (!rs.next()) { + System.out.println("No goods data found in the 'goods' table"); + return false; + } + rs.close(); + + // Validate highScores table + rs = stmt.executeQuery("SELECT * FROM highScores"); + if (!rs.next()) { + System.out.println("No highScores data found in the 'highScores' table"); + return false; + } + rs.close(); + + // Validate foundingFathers table + rs = stmt.executeQuery("SELECT * FROM foundingFathers"); + if (!rs.next()) { + System.out.println("No foundingFathers data found in the 'foundingFathers' table"); + return false; + } + rs.close(); + + stmt.close(); + + // All validations passed + return true; + } + + /** + * Gets a list of SQLite database files in the given directory. + * + * @param directory Directory containing the SQLite database files + * @return a List of SQLite database files + */ + private static List getSQLiteDatabaseFileList(File directory) { + List sqliteFiles = new ArrayList<>(); + File[] files = directory.listFiles(); + + if (files != null) { + for (File file : files) { + if (file.isFile() && file.getName().toLowerCase().endsWith(".sqlite")) { + sqliteFiles.add(file); + } + } + } + + return sqliteFiles; + } +} diff --git a/test/src/net/sf/freecol/common/io/SQLInteractionsTests.java b/test/src/net/sf/freecol/common/io/SQLInteractionsTests.java new file mode 100644 index 0000000000..75d973f778 --- /dev/null +++ b/test/src/net/sf/freecol/common/io/SQLInteractionsTests.java @@ -0,0 +1,169 @@ +package net.sf.freecol.common.io; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.io.File; +import java.io.IOException; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.ResultSet; +import java.sql.SQLException; + +import static org.junit.jupiter.api.Assertions.assertEquals; + + +/** + * These tests save and load various game objects like the game itself, + * players, and tiles, and then compare the saved and loaded values to + * ensure they match. Use these tests as a guide to extend + * them for any additional game components. + */ +public class SQLInteractionsTests { + + private static final String DB_PATH = "test_db.sqlite"; + + private FreeColSQLWriter writer; + private FreeColSQLReader reader; + private Connection connection; + + @BeforeEach + public void setUp() throws SQLException { + writer = new FreeColSQLWriter(DB_PATH, FreeColSQLWriter.WriteScopeType.SAVE); + reader = new FreeColSQLReader(DB_PATH, FreeColSQLReader.ReadScopeType.CLIENT); + connection = DriverManager.getConnection("jdbc:sqlite:" + DB_PATH); + + // Create the necessary tables for testing + connection.createStatement().execute( + "CREATE TABLE IF NOT EXISTS game (id INTEGER, options TEXT, turn INTEGER);" + ); + } + + @AfterEach + public void tearDown() throws IOException, SQLException { + writer.close(); + reader.close(); + connection.close(); + + // Remove the test database file + File dbFile = new File(DB_PATH); + if (dbFile.exists()) { + dbFile.delete(); + } + } + + @Test + public void testSaveGame() throws SQLException { + Game game = new Game(1, "options"); + game.setTurn(5); + + writer.saveGameData(game); + + ResultSet resultSet = reader.executeQuery("SELECT * FROM game WHERE id = 1"); + resultSet.next(); + + int turn = resultSet.getInt("turn"); + String options = resultSet.getString("options"); + + assertEquals(5, turn, "Turn value should be 5"); + assertEquals("options", options, "Game options should be 'options'"); + } + + @Test + public void testSavePlayer() throws SQLException { + Game game = new Game(1, "options"); + game.addPlayer(new Player(game, "player1", "nation1")); + game.addPlayer(new Player(game, "player2", "nation2")); + + writer.saveGameData(game); + + ResultSet resultSet = reader.executeQuery("SELECT * FROM player"); + int playerCount = 0; + while (resultSet.next()) { + String playerName = resultSet.getString("name"); + String nation = resultSet.getString("nation"); + + assertEquals("player" + (playerCount + 1), playerName, "Check saved player name"); + assertEquals("nation" + (playerCount + 1), nation, "Check saved player nation"); + + playerCount++; + } + + assertEquals(2, playerCount, "There should be exactly 2 players saved in the database"); + } + + @Test + public void testSaveTile() throws SQLException { + Game game = new Game(1, "options"); + + // Add some tiles to the game map + game.getMap().addTile(new Tile(1, 1, "grass", "player1")); + game.getMap().addTile(new Tile(1, 2, "forest", "player2")); + + writer.saveGameData(game); + + ResultSet resultSet = reader.executeQuery("SELECT * FROM tile"); + int tileCount = 0; + while (resultSet.next()) { + int x = resultSet.getInt("x"); + int y = resultSet.getInt("y"); + String terrainType = resultSet.getString("terrain_type"); + String owner = resultSet.getString("owner"); + + assertEquals(1, x, "Check saved tile x-coordinate"); + assertEquals(tileCount + 1, y, "Check saved tile y-coordinate"); + assertEquals("player" + (tileCount + 1), owner, "Check saved tile owner"); + + if (tileCount == 0) { + assertEquals("grass", terrainType, "First tile should be grass"); + } else { + assertEquals("forest", terrainType, "Second tile should be forest"); + } + + tileCount++; + } + + assertEquals(2, tileCount, "There should be exactly 2 tiles saved in the database"); + } + + @Test + public void testLoadGame() throws SQLException { + // Save test game data first + Game game = new Game(1, "options"); + game.setTurn(5); + writer.saveGameData(game); + + // Now load it using reader + Game loadedGame = reader.readGame(); + + assertEquals(1, loadedGame.getId(), "Loaded game ID should be 1"); + assertEquals("options", loadedGame.getGameOptions().toString(), "Loaded game options should be 'options'"); + assertEquals(5, loadedGame.getTurn(), "Loaded game turn should be 5"); + } + + @Test + public void testLoadPlayers() throws SQLException { + // Save test game data first + Game game = new Game(1, "options"); + game.addPlayer(new Player(game, "player1", "nation1")); + game.addPlayer(new Player(game, "player2", "nation2")); + writer.saveGameData(game); + + // Now load the saved game using the reader + Game loadedGame = reader.readGame(); + + assertEquals(game.getPlayers().size(), loadedGame.getPlayers().size(), "Both saved and loaded games have the same number of players"); + + for (int i = 0; i < game.getPlayers().size(); i++) { + Player originalPlayer = game.getPlayers().get(i); + Player loadedPlayer = loadedGame.getPlayers().get(i); + + assertEquals(originalPlayer.getId(), loadedPlayer.getId(), "Both saved and loaded player IDs should match"); + assertEquals(originalPlayer.getName(), loadedPlayer.getName(), "Both saved and loaded player names should match"); + assertEquals(originalPlayer.getNation().getName(), loadedPlayer.getNation().getName(), "Both saved and loaded player nations should match"); + } + } + + // You can add more tests for loading tiles and other game components similarly +} \ No newline at end of file