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; +    } +}  | 
