diff options
Diffstat (limited to 'src/main/java/net/sowgro/npehero/levelapi')
8 files changed, 717 insertions, 0 deletions
diff --git a/src/main/java/net/sowgro/npehero/levelapi/Difficulties.java b/src/main/java/net/sowgro/npehero/levelapi/Difficulties.java new file mode 100644 index 0000000..cffd95e --- /dev/null +++ b/src/main/java/net/sowgro/npehero/levelapi/Difficulties.java @@ -0,0 +1,127 @@ +package net.sowgro.npehero.levelapi; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.nio.file.FileAlreadyExistsException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; + +/** + * Responsible for the list of difficulties in a level + */ +public class Difficulties { + + public final ObservableList<Difficulty> list = FXCollections.observableArrayList(); + public final HashMap<String, Exception> problems = new HashMap<>(); + + private final Level level; + + /** + * Creates a new Difficulties object + * @param level the file path of the level + * @throws IOException If there is a problem reading in the difficulties + */ + public Difficulties(Level level) throws IOException { + this.level = level; + read(); + } + + /** + * Loads difficulties + * <p> + * Creates difficulty objects out of each subfolder in the level and adds it to the list. + * @throws IOException If there is a problem reading in the difficulties + */ + public void read() throws IOException { + list.clear(); + File[] fileList = level.dir.listFiles(); + if (fileList == null) { + throw new FileNotFoundException(); + } + for(File cur: fileList) { + if (cur.isDirectory()) { + try { + Difficulty diff = new Difficulty(cur, level); + list.add(diff); + } catch (IOException e) { + problems.put("", e); + e.printStackTrace(); + } + } + } + list.sort(Comparator.naturalOrder()); + } + + /** + * Removes a difficulty + * <p> + * Recursively deletes the folder and removes it from the list + * @param diff: The difficulty to remove. + * @throws IOException If there is a problem removing the difficulty. + */ + public void remove(Difficulty diff) throws IOException { + File hold = diff.thisDir; + Files.walk(hold.toPath()) + .sorted(Comparator.reverseOrder()) + .map(Path::toFile) + .forEach(File::delete); + list.remove(diff); + } + + /** + * Adds a difficulty + * <p> + * Creates the directory and required files + * @param text The name of the directory + * @throws IOException If there is a problem adding the level + */ + public void add(String text) throws IOException { + File diffDir = new File(level.dir, text.toLowerCase().replaceAll("\\W+", "-")); + if (diffDir.exists()) { + throw new FileAlreadyExistsException(diffDir.getName()); + } + if (diffDir.mkdirs()) { + Difficulty temp = new Difficulty(diffDir, level); + temp.title = text; + list.add(temp); + list.sort(Comparator.naturalOrder()); + } + else { + throw new IOException(); + } + } + + /** + * Saves the order of the difficulties in the list + * <p> + * Updates the order variable of each difficulty in the list to match their index in the list + * @throws IOException If there is a problem saving the difficulty's metadata file + */ + public void saveOrder() throws IOException { + for (Difficulty d : list) { + d.order = list.indexOf(d); + d.writeMetadata(); + } + } + + /** + * Get a list of only the valid difficulties in the level. + * @return A list of the valid difficulties. + */ + public List<Difficulty> getValidList() { + ObservableList<Difficulty> validList = FXCollections.observableArrayList(); + for (Difficulty difficulty : list) { + if (difficulty.isValid()) { + validList.add(difficulty); + } + } + return validList; + } +} diff --git a/src/main/java/net/sowgro/npehero/levelapi/Difficulty.java b/src/main/java/net/sowgro/npehero/levelapi/Difficulty.java new file mode 100755 index 0000000..2e99a7a --- /dev/null +++ b/src/main/java/net/sowgro/npehero/levelapi/Difficulty.java @@ -0,0 +1,95 @@ +package net.sowgro.npehero.levelapi; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.ToNumberPolicy; + +import java.io.*; +import java.util.Map; + +/** + * Represents a difficulty + * Responsible for the data in metadata.yml + */ +public class Difficulty implements Comparable<Difficulty> +{ + public final File thisDir; + public final Level level; + + public String title = "Unnamed"; + public Double bpm = 0.0; + public double endTime = 0; + public int order = 0; + + public final Leaderboard leaderboard; + public final Notes notes; + + private final Gson jsonParser = new GsonBuilder().serializeNulls().setPrettyPrinting().setNumberToNumberStrategy(ToNumberPolicy.DOUBLE).create(); + private final File jsonFile; + + /** + * Creates a new Difficulty + * @param newDir: The file path of the Difficulty + * @throws IOException If there are any problems reading the metadata or leaderboard files + */ + public Difficulty(File newDir, Level level) throws IOException { + thisDir = newDir; + this.level = level; + jsonFile = new File(thisDir, "metadata.json"); + readMetadata(); + notes = new Notes(new File(thisDir, "notes.txt"), this); // needs metadata first + leaderboard = new Leaderboard(new File(thisDir, "leaderboard.json")); + } + + /** + * Read in the data from metadata.json + * @throws IOException If there are any problems loading the file. + */ + public void readMetadata() throws IOException { + if (!jsonFile.exists()) { + return; + } + Map<String, Object> data = jsonParser.fromJson(new FileReader(jsonFile), Map.class); + + title = (String) data.getOrDefault("title", title); + bpm = (Double) data.getOrDefault("bpm", bpm); + endTime = (double) data.getOrDefault("endTime", endTime); + if (endTime == 0) { + int tmp = (int) (double) data.getOrDefault("numBeats", 0.0); + if (tmp != 0) { + endTime = Notes.beatToSecond(tmp, bpm); + } + } + order = (int) (double) data.getOrDefault("priority", (double) order); + } + + /** + * Checks the validity of the difficulty + * <p> + * A valid difficulty has at least one note + * @return True if the difficulty is valid + */ + public boolean isValid() { + return !notes.list.isEmpty(); + } + + /** + * Writes metadata to json file + * @throws IOException If there is a problem writing to the file + */ + public void writeMetadata() throws IOException { + jsonFile.createNewFile(); + Map<String, Object> data = jsonParser.fromJson(new FileReader(jsonFile), Map.class); // start with previous values + data.put("title", title); + data.put("endTime", endTime); + data.put("priority", order); + FileWriter fileWriter = new FileWriter(jsonFile); + jsonParser.toJson(data, fileWriter); + fileWriter.close(); + } + + @Override + public int compareTo(Difficulty d) { + return order - d.order; + } +} diff --git a/src/main/java/net/sowgro/npehero/levelapi/Leaderboard.java b/src/main/java/net/sowgro/npehero/levelapi/Leaderboard.java new file mode 100644 index 0000000..bb1f30c --- /dev/null +++ b/src/main/java/net/sowgro/npehero/levelapi/Leaderboard.java @@ -0,0 +1,81 @@ +package net.sowgro.npehero.levelapi; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; + +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.time.LocalDate; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class Leaderboard { + + public final ObservableList<LeaderboardEntry> entries = FXCollections.observableArrayList(); + private final Gson json = new GsonBuilder().serializeNulls().setPrettyPrinting().create(); + private final File file; + + public Leaderboard(File file) throws IOException{ + this.file = file; + read(); + } + + /** + * Adds new leaderboardEntry to list and updates json file + * @param name: The players name + * @param score The players score + * @throws IOException If there is a problem updating the leaderboard file. + */ + public void add(String name, int score) throws IOException { + entries.add(new LeaderboardEntry(name, score, LocalDate.now().toString())); + save(); + } + + /** + * Writes leaderboard to json file + * @throws IOException If there are problems writing to the file. + */ + public void save() throws IOException { + file.createNewFile(); + List<Map<String, Object>> data = json.fromJson(new FileReader(file), List.class); + for (LeaderboardEntry cur : entries) { + Map<String, Object> obj = new HashMap<>(); + obj.put("name", cur.name); + obj.put("score", cur.score); + obj.put("date", cur.date); + data.add(obj); + } + FileWriter fileWriter = new FileWriter(file); + json.toJson(data, fileWriter); + fileWriter.close(); + } + + /** + * Reads in json leaderboard and assigns populates list with leaderboardEntries + * @throws IOException If there are problems reading the file + */ + public void read() throws IOException { + if (!file.exists()) { + return; + } + List<Map<String, Object>> data = json.fromJson(new FileReader(file), List.class); + if (data == null) { + return; + } + for (Map<String, Object> cur: data) { + String name = (String) cur.getOrDefault("name", null); + int score = (int) (double) cur.getOrDefault("score", -1); + String date = (String) cur.getOrDefault("date", null); + if (name == null || score == -1 || date == null) { + System.out.println("dbg: bad entry skipped"); + continue; // discard invalid entries + } + entries.add(new LeaderboardEntry(name, score, date)); + } + } +} diff --git a/src/main/java/net/sowgro/npehero/levelapi/LeaderboardEntry.java b/src/main/java/net/sowgro/npehero/levelapi/LeaderboardEntry.java new file mode 100755 index 0000000..2b98a29 --- /dev/null +++ b/src/main/java/net/sowgro/npehero/levelapi/LeaderboardEntry.java @@ -0,0 +1,24 @@ +package net.sowgro.npehero.levelapi; + +/** + * Represents one players score in the leaderboard + */ +public class LeaderboardEntry +{ + public final int score; + public final String name; + public final String date; + + /** + * Create a new LeaderboardEntry + * @param name The name the player input after completing the level + * @param score The score the player earned + * @param date The date the player earned this score + */ + public LeaderboardEntry(String name, int score, String date) + { + this.name = name; + this.score = score; + this.date = date; + } +} diff --git a/src/main/java/net/sowgro/npehero/levelapi/Level.java b/src/main/java/net/sowgro/npehero/levelapi/Level.java new file mode 100755 index 0000000..218779f --- /dev/null +++ b/src/main/java/net/sowgro/npehero/levelapi/Level.java @@ -0,0 +1,152 @@ +package net.sowgro.npehero.levelapi; + +import java.io.*; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import javafx.scene.image.Image; +import javafx.scene.media.Media; +import javafx.scene.paint.Color; + +import java.nio.file.Files; +import java.nio.file.StandardCopyOption; +import java.util.Map; + +public class Level implements Comparable<Level>{ + + public final File dir; + + public String title = "Unnamed"; + public String artist = "Unknown"; + public String desc; + public Color[] colors = {Color.RED,Color.BLUE,Color.GREEN,Color.PURPLE,Color.YELLOW}; + public Image preview; + public Image background; + public Media song; + + public Difficulties difficulties; + + private final File jsonFile; + private final Gson jsonParser = new GsonBuilder().serializeNulls().setPrettyPrinting().create(); + + /** + * Creates a new level + * @param newDir The path of the Level + * @throws IOException If there is a problem reading the metadata file or loading the difficulties + */ + public Level(File newDir) throws IOException + { + dir = newDir; + jsonFile = new File(dir, "metadata.json"); + readFiles(); + readMetadata(); + difficulties = new Difficulties(this); + } + + /** + * Check for a song file, background file and preview image file + */ + public void readFiles() { + + File[] fileList = dir.listFiles(); + if (fileList == null) { + return; + } + for (File file : fileList) { + String fileName = file.getName(); + if (fileName.contains("song")) { + song = new Media(file.toURI().toString()); + } + else if (fileName.contains("background")) { + background = new Image(file.toURI().toString()); + } + else if (fileName.contains("preview")) { + preview = new Image(file.toURI().toString()); + } + } + + } + + /** + * Read in metadata file + * @throws IOException If there is a problem reading the file + */ + public void readMetadata() throws IOException { + if (!jsonFile.exists()) { + return; + } + Map<String, Object> data = jsonParser.fromJson(new FileReader(jsonFile), Map.class); + title = (String) data.getOrDefault("title", title); + artist = (String) data.getOrDefault("artist", artist); + desc = (String) data.getOrDefault("desc", desc); + colors[0] = Color.web((String) data.getOrDefault("color1", colors[0].toString())); + colors[1] = Color.web((String) data.getOrDefault("color2", colors[1].toString())); + colors[2] = Color.web((String) data.getOrDefault("color3", colors[2].toString())); + colors[3] = Color.web((String) data.getOrDefault("color4", colors[3].toString())); + colors[4] = Color.web((String) data.getOrDefault("color5", colors[4].toString())); + } + + /** + * Checks if the level is valid. + * <p> + * A valid level has a song file and 1 or more valid difficulties + * @return True if the level is valid + */ + public boolean isValid() { + if (song == null) { + return false; + } + + if (difficulties.getValidList().isEmpty()) { + return false; + } + return true; + } + + /** + * Writes metadata to json file + * @throws IOException If there is a problem writing to the file. + */ + public void writeMetadata() throws IOException { + jsonFile.createNewFile(); + Map<String, Object> data = jsonParser.fromJson(new FileReader(jsonFile), Map.class); + data.put("title", title); + data.put("artist", artist); + data.put("desc", desc); + data.put("color1",colors[0].toString()); + data.put("color2",colors[1].toString()); + data.put("color3",colors[2].toString()); + data.put("color4",colors[3].toString()); + data.put("color5",colors[4].toString()); + FileWriter fileWriter = new FileWriter(jsonFile); + jsonParser.toJson(data, fileWriter); + fileWriter.close(); + } + + + /** + * Copies a file into the level directory with the name provided. The extension will be inherited from the source file + * @param source: the file to be copied + * @param name: the new file name EXCLUDING the extension. + * @throws IOException If there is a problem adding the file + */ + public void addFile(File source, String name) throws IOException { + name = name + "." + getFileExtension(source); + Files.copy(source.toPath(), new File(dir, name).toPath(), StandardCopyOption.REPLACE_EXISTING); + readFiles(); + } + + @Override + public int compareTo(Level other) { + return title.compareTo(other.title); + } + + /** + * Get the extension of a file. + * @param file The file to return the extension of + * @return The extension of the file in the format "*.ext" + */ + public String getFileExtension(File file) { + return file.getName().substring(file.getName().lastIndexOf('.') + 1); + } +} diff --git a/src/main/java/net/sowgro/npehero/levelapi/Levels.java b/src/main/java/net/sowgro/npehero/levelapi/Levels.java new file mode 100755 index 0000000..84ffe51 --- /dev/null +++ b/src/main/java/net/sowgro/npehero/levelapi/Levels.java @@ -0,0 +1,97 @@ +package net.sowgro.npehero.levelapi; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.nio.file.FileAlreadyExistsException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Comparator; +import java.util.HashMap; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; + +/** + * Stores a list of all the levels + */ +public class Levels { + + public static final ObservableList<Level> list = FXCollections.observableArrayList(); + public static final HashMap<String, Exception> problems = new HashMap<>(); + + private static final File dir = new File("levels"); + + /** + * Reads contents of the levels folder and creates a level form each subfolder + * <p> + * All subfolders in the levels folder are assumed to be levels + * @throws FileNotFoundException If the levels folder is missing. + * @throws IOException If there is a problem reading in the levels. + */ + public static void readData() throws IOException { + list.clear(); + File[] fileList = dir.listFiles(); + if (fileList == null) { + throw new FileNotFoundException(); + } + for (File file: fileList) { + try { + Level level = new Level(file); + list.add(level); + } catch (IOException e) { + problems.put("", e); + e.printStackTrace(); + } + } + list.sort(Comparator.naturalOrder()); + } + + /** + * Creates a subfolder in the levels folder for the new level then creates the level with it + * @param text: the name of the directory and default title + * @throws IOException if there was an error adding the level + */ + public static void add(String text) throws IOException { + File levelDir = new File(dir, text.toLowerCase().replaceAll("\\W+", "-")); + if (levelDir.exists()) { + throw new FileAlreadyExistsException(levelDir.getName()); + } + if (levelDir.mkdirs()) { + Level temp = new Level(levelDir); + temp.title = text; + list.add(temp); + } + else { + throw new IOException(); + } + } + + /** + * Removes level from the filesystem then reloads this levelController + * @param level: the level to be removed + * @throws IOException If there is a problem deleting the level + */ + public static void remove(Level level) throws IOException { + File hold = level.dir; + Files.walk(hold.toPath()) + .sorted(Comparator.reverseOrder()) + .map(Path::toFile) + .forEach(File::delete); + list.remove(level); + } + + /** + * Gets a list of only the valid levels. + * @return A list of the valid levels. + */ + public static ObservableList<Level> getValidList() { + ObservableList<Level> validList = FXCollections.observableArrayList(); + for (Level level : list) { + if (level.isValid()) { + validList.add(level); + } + } + return validList; + } +}
\ No newline at end of file diff --git a/src/main/java/net/sowgro/npehero/levelapi/Note.java b/src/main/java/net/sowgro/npehero/levelapi/Note.java new file mode 100644 index 0000000..ab93885 --- /dev/null +++ b/src/main/java/net/sowgro/npehero/levelapi/Note.java @@ -0,0 +1,34 @@ +package net.sowgro.npehero.levelapi; + +import javafx.beans.property.DoubleProperty; +import javafx.beans.property.SimpleDoubleProperty; + +/** + * A note represents a moment in the song when the player should hit a key + * <p> + * The key corresponding to the lane the note is in should be pressed + */ +public class Note { + + public final DoubleProperty time = new SimpleDoubleProperty(); + public final int lane; + + /** + * Creates a new note + * @param time The time the player should hit the note. + * @param lane The lane the note belongs to. + */ + public Note(double time, int lane) { + this.time.set(time); + this.lane = lane; + } + + /** + * Copy constructor + * @param other the note to copy from + */ + public Note(Note other) { + this.lane = other.lane; + this.time.set(other.time.get()); + } +} diff --git a/src/main/java/net/sowgro/npehero/levelapi/Notes.java b/src/main/java/net/sowgro/npehero/levelapi/Notes.java new file mode 100644 index 0000000..1df0248 --- /dev/null +++ b/src/main/java/net/sowgro/npehero/levelapi/Notes.java @@ -0,0 +1,107 @@ +package net.sowgro.npehero.levelapi; + +import javafx.beans.property.ListProperty; +import javafx.beans.property.SimpleListProperty; +import javafx.collections.FXCollections; + +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.util.Scanner; + +/** + * Stores all the notes for a difficulty. + */ +public class Notes { + + private final File file; + private final Difficulty diff; + + public ListProperty<Note> list = new SimpleListProperty<>(FXCollections.observableArrayList()); + + /** + * Create a new Notes object + * @param file The notes.txt file + * @param diff The difficulty these notes belong to + * @throws IOException If there is a problem reading the notes file + */ + public Notes(File file, Difficulty diff) throws IOException { + this.file = file; + this.diff = diff; + readFile(); + } + + /** + * Read notes.txt and add the notes to the list + * @throws IOException if there is a problem reading the file. + */ + public void readFile() throws IOException { + if (!file.exists()) { + return; + } + Scanner scan = new Scanner(file); + while (scan.hasNext()) { + String input = scan.next(); + int lane = switch (input.charAt(0)) { + case 'd' -> 0; + case 'f' -> 1; + case 's' -> 2; + case 'j' -> 3; + case 'k' -> 4; + default -> -1; + }; + if (lane == -1) { + continue; + } + double time = Double.parseDouble(input.substring(1)); + + if (diff.bpm != 0.0) { + time = beatToSecond(time, diff.bpm); + } + list.add(new Note(time, lane)); + } + } + + /** + * Writes the notes to notes.txt + * @throws IOException If there is a problem writing to the file. + */ + public void writeFile() throws IOException{ + var _ = file.createNewFile(); + PrintWriter writer = new PrintWriter(file, StandardCharsets.UTF_8); + for (Note note : list) { + Character lane = switch (note.lane) { + case 0 -> 'd'; + case 1 -> 'f'; + case 2 -> 's'; + case 3 -> 'j'; + case 4 -> 'k'; + default -> null; + }; + if (lane == null) { + continue; + } + writer.println(lane + "" + note.time.get()); + } + writer.close(); + } + + /** + * Converts a beat to a second using the levels bpm + * @param beat The beat to convert to seconds + * @param bpm The beats per minute to use for conversion + * @return The time in seconds the beat was at + */ + public static double beatToSecond(double beat, double bpm) { + return beat/(bpm/60); + } + + /** + * Performs a deep copy of the notes list. + * @return a new list of notes with the same notes. + */ + public ListProperty<Note> deepCopyList() { + ListProperty<Note> ret = new SimpleListProperty<>(FXCollections.observableArrayList()); + list.forEach(e -> ret.add(new Note(e))); + return ret; + } +} |