core/src/com/fteams/sstrain/util/SongUtils.java
public class SongUtils {
    public final static Long NOTE_TYPE_NORMAL = 1l;
    public final static Long NOTE_TYPE_HOLD = 2l;

    public final static Integer NOTE_SYNC_OFF = 0;
    public final static Integer NOTE_SYNC_ON = 1;

    public final static Long NOTE_NO_SWIPE = 0L;
    public final static Long NOTE_SWIPE_LEFT = 1L;
    public final static Long NOTE_SWIPE_RIGHT = 2L;

    public final static Integer SORTING_MODE_FILE_NAME = 0;
    public final static Integer SORTING_MODE_SONG_NAME = 1;
    public final static Integer SORTING_MODE_SONG_ID = 2;
    public final static Integer SORTING_MODE_ATTRIBUTE = 3;

    public final static Integer SORTING_MODE_ASCENDING = 0;
    public final static Integer SORTING_MODE_DESCENDING = 1;

    public final static Integer SYNC_MODE_1 = 0;
    public final static Integer SYNC_MODE_2 = 1;
    public final static Integer SYNC_MODE_3 = 2;
    public final static Integer SYNC_DISABLED = 3;

    public final static Integer GAME_MODE_NORMAL = 0;
    public final static Integer GAME_MODE_ABREPEAT = 1;

    public final static String[] syncModes = {"Default", "Constant Sync", "Initial Sync", "Disabled"};
    public final static Long[] noteSpeeds = {1800L, 1680L, 1560L, 1440L, 1320L, 1200L, 1050L, 900L, 750L, 600L, 450L};

    public final static Double[] overallDiffPerfect = {79.5, 73.5, 67.5, 61.5, 56.5, 49.5, 43.5, 37.5, 31.5, 25.5, 19.5}; // -6
    public final static Double[] overallDiffGreat = {139.5, 131.5, 123.5, 115.5, 107.5, 99.5, 91.5, 83.5, 75.5, 67.5, 59.5}; // -8
    public final static Double[] overallDiffNice = {199.5, 189.5, 179.5, 169.5, 159.5, 149.5, 139.5, 129.5, 119.5, 109.5, 99.5}; // - 10
    public final static Double[] overallDiffBad = {249.5, 237.5, 225.5, 213.5, 201.5, 189.5, 177.5, 165.5, 153.5, 141.5, 129.5};// - 12

    public static Long getSpeedFromConfig(Integer noteSpeed) {
        return noteSpeeds[noteSpeed];
    }

    public final static String[] attributes = {"Cute", "Cool", "Passion", "ALL"};

    public static String getAttribute(Long attribute) {
        if (attribute == null) {
            return "Unknown";
        }
        if (attribute == 0) {
            return "TUTORIAL";
        }
        return attributes[(int) (attribute - 1)];
    }

    public static int compare(int x, int y) {
        return (x < y) ? -1 : ((x == y) ? 0 : 1);
    }

    public static int compare(long x, long y) {
        return (x < y) ? -1 : ((x == y) ? 0 : 1);
    }
}
core/src/com/fteams/sstrain/screens/ResultsScreen.java
public class ResultsScreen implements Screen {

    Stage stage = new Stage();
    private Texture texture = Assets.mainMenuBackgroundTexture;
    private Image splashImage = new Image(texture);
    private Table table = new Table();
    private Label difficulty = new Label("Difficulty:", Assets.menuSkin, "song_style_result_values");
    private Label difficultyLabel;
    private Label approachRateLabel = new Label("Approach Rate:", Assets.menuSkin, "song_style_result_values");
    private Label approachRateConfigLabel;
    private Label overallDifficultyLabel = new Label("Overall Difficulty:", Assets.menuSkin, "song_style_result_values");
    private Label overallDifficultyConfigLabel;
    private Label accuracyLabel = new Label("Avg. Accuracy:", Assets.menuSkin, "song_style_result_values");
    private Label accuracyResultLabel;
    private Label normalizedAccuracyLabel = new Label("% Accuracy:", Assets.menuSkin, "song_style_result_values");
    private Label normalizedAccuracyResultLabel;
    private Label accuracyRangeLabel = new Label("Accuracy Range:", Assets.menuSkin, "song_style_result_values");
    private Label accuracyRangeResultLabel;
    private Label unstableRatingLabel = new Label("Unstable Rating:", Assets.menuSkin, "song_style_result_values");
    private Label unstableRatingValueLabel;
    private Label badLabel = new Label("Bad:", Assets.menuSkin, "song_style_result_values");
    private Label badResultLabel;
    private Label goodLabel = new Label("Nice:", Assets.menuSkin, "song_style_result_values");
    private Label goodResultLabel;
    private Label greatLabel = new Label("Great", Assets.menuSkin, "song_style_result_values");
    private Label greatResultLabel;
    private Label perfectLabel = new Label("Perfect:", Assets.menuSkin, "song_style_result_values");
    private Label perfectResultLabel;
    private Label missLabel = new Label("Miss:", Assets.menuSkin, "song_style_result_values");
    private Label missResultLabel;
    private Label comboLabel = new Label("Largest Combo:", Assets.menuSkin, "song_style_result_values");
    private Label comboResultLabel;
    private Label titleLabel = new Label("Results/結果発表", Assets.menuSkin, "results_title");

    @Override
    public void show() {
        // title font scale = 1 for a 720 height
        float fontScale = stage.getHeight() / GlobalConfiguration.BASE_HEIGHT;

        splashImage.setHeight(stage.getHeight());
        splashImage.setWidth(stage.getWidth());
        stage.addActor(splashImage);
        titleLabel.setX(stage.getWidth() / 2 - titleLabel.getWidth() / 2);
        titleLabel.setY(stage.getHeight() - stage.getHeight() * 0.3f);
        titleLabel.setFontScale(fontScale);

        table.setFillParent(true);

        difficultyLabel = new Label(Assets.selectedBeatmap.metadata.difficulty == null ? Assets.selectedBeatmap.metadata.difficulty + "*" : Assets.selectedBeatmap.metadata.difficultyName, Assets.menuSkin, "song_style_result_values");
        approachRateConfigLabel = new Label("AR-" + GlobalConfiguration.noteSpeed + " (" + SongUtils.getSpeedFromConfig(GlobalConfiguration.noteSpeed) + " ms)", Assets.menuSkin, "song_style_result_values");
        overallDifficultyConfigLabel = new Label("OD-" + GlobalConfiguration.overallDifficulty + " (" + SongUtils.overallDiffBad[GlobalConfiguration.overallDifficulty] + " ms)", Assets.menuSkin, "song_style_result_values");
        accuracyResultLabel = new Label(String.format("%.2f", Results.accuracy * 1000) + " ms.", Assets.menuSkin, "song_style_result_values");
        accuracyRangeResultLabel = new Label(String.format("%.2f", Results.minAccuracy * 1000) + " ms. to " + String.format("%.2f", Results.maxAccuracy * 1000) + " ms.", Assets.menuSkin, "song_style_result_values");
        normalizedAccuracyResultLabel = new Label(String.format("%.2f", Results.normalizedAccuracy * 100f) + "%", Assets.menuSkin, "song_style_result_values");
        unstableRatingValueLabel = new Label(String.format("%.2f", Results.unstableRating * 1000), Assets.menuSkin, "song_style_result_values");
        missResultLabel = new Label(Integer.toString(Results.miss), Assets.menuSkin, "song_style_result_values");
        badResultLabel = new Label(Integer.toString(Results.bads), Assets.menuSkin, "song_style_result_values");
        goodResultLabel = new Label(Integer.toString(Results.goods), Assets.menuSkin, "song_style_result_values");
        greatResultLabel = new Label(Integer.toString(Results.greats), Assets.menuSkin, "song_style_result_values");
        perfectResultLabel = new Label(Integer.toString(Results.perfects), Assets.menuSkin, "song_style_result_values");
        comboResultLabel = new Label(Integer.toString(Results.combo) + (Results.combo == Assets.selectedBeatmap.notes.size() ? " (FC)" : ""), Assets.menuSkin, "song_style_result_values");

        difficulty.setFontScale(fontScale);
        approachRateConfigLabel.setFontScale(fontScale);
        overallDifficultyConfigLabel.setFontScale(fontScale);
        accuracyResultLabel.setFontScale(fontScale);
        accuracyRangeResultLabel.setFontScale(fontScale);
        normalizedAccuracyResultLabel.setFontScale(fontScale);
        unstableRatingValueLabel.setFontScale(fontScale);
        missResultLabel.setFontScale(fontScale);
        badResultLabel.setFontScale(fontScale);
        goodResultLabel.setFontScale(fontScale);
        greatResultLabel.setFontScale(fontScale);
        perfectResultLabel.setFontScale(fontScale);
        comboResultLabel.setFontScale(fontScale);

        difficultyLabel.setFontScale(fontScale);
        approachRateLabel.setFontScale(fontScale);
        overallDifficultyLabel.setFontScale(fontScale);
        accuracyLabel.setFontScale(fontScale);
        accuracyRangeLabel.setFontScale(fontScale);
        normalizedAccuracyLabel.setFontScale(fontScale);
        unstableRatingLabel.setFontScale(fontScale);
        missLabel.setFontScale(fontScale);
        badLabel.setFontScale(fontScale);
        perfectLabel.setFontScale(fontScale);
        goodLabel.setFontScale(fontScale);
        greatLabel.setFontScale(fontScale);
        comboLabel.setFontScale(fontScale);

        Label songResultTitle = new Label(Assets.selectedBeatmap.metadata.songName.replaceAll("\\\\n", " "), Assets.menuSkin, "results_song_title");
        songResultTitle.setFontScale(fontScale);

        table.add(songResultTitle).colspan(3).row();
        table.add(titleLabel).colspan(3).padBottom(stage.getHeight() * 0.04f).row();

        table.add(difficulty).fillX();
        table.add().width(stage.getWidth() * 0.2f);
        table.add(difficultyLabel).fillX().row();
        table.add(comboLabel).fillX().padBottom(20);
        table.add().width(stage.getWidth() * 0.2f).padBottom(20);
        table.add(comboResultLabel).fillX().padBottom(20).row();

        table.add(approachRateLabel).fillX();
        table.add().width(stage.getWidth() * 0.2f);
        table.add(approachRateConfigLabel).fillX().row();
        table.add(overallDifficultyLabel).fillX();
        table.add().width(stage.getWidth() * 0.2f);
        table.add(overallDifficultyConfigLabel).fillX().row();
        table.add(normalizedAccuracyLabel).fillX();
        table.add().width(stage.getWidth() * 0.2f);
        table.add(normalizedAccuracyResultLabel).fillX().row();
        table.add(accuracyLabel).fillX();
        table.add().width(stage.getWidth() * 0.2f);
        table.add(accuracyResultLabel).fillX().row();
        table.add(accuracyRangeLabel).fillX();
        table.add().width(stage.getWidth() * 0.2f);
        table.add(accuracyRangeResultLabel).fillX().row();
        table.add(unstableRatingLabel).padBottom(20).fillX();
        table.add().width(stage.getWidth() * 0.2f).padBottom(20);
        table.add(unstableRatingValueLabel).padBottom(20).fillX().row();

        table.add(perfectLabel).fillX();
        table.add().width(stage.getWidth() * 0.2f);
        table.add(perfectResultLabel).fillX().row();
        table.add(greatLabel).fillX();
        table.add().width(stage.getWidth() * 0.2f);
        table.add(greatResultLabel).fillX().row();
        table.add(goodLabel).fillX();
        table.add().width(stage.getWidth() * 0.2f);
        table.add(goodResultLabel).fillX().row();
        table.add(badLabel).fillX();
        table.add().width(stage.getWidth() * 0.2f);
        table.add(badResultLabel).fillX().row();
        table.add(missLabel).fillX();
        table.add().width(stage.getWidth() * 0.2f);
        table.add(missResultLabel).fillX().row();

        TextButton retryButton = new TextButton("Retry", Assets.menuSkin, "item1");
        TextButton continueButton = new TextButton("Continue", Assets.menuSkin, "item1");

        retryButton.getLabel().setFontScale(fontScale);
        continueButton.getLabel().setFontScale(fontScale);

        retryButton.addListener(new ClickListener() {
            @Override
            public void clicked(InputEvent event, float x, float y) {
                ((Game) Gdx.app.getApplicationListener()).setScreen(new SongScreen());
            }
        });
        continueButton.addListener(new ClickListener() {
            @Override
            public void clicked(InputEvent event, float x, float y) {
                ((Game) Gdx.app.getApplicationListener()).setScreen(new SongSelectionScreen());
            }
        });

        table.add(retryButton).width(stage.getWidth() * 0.3f).height(stage.getHeight() * 0.1f);
        table.add().fillX();
        table.add(continueButton).width(stage.getWidth() * 0.3f).height(stage.getHeight() * 0.1f).row();
        stage.addActor(table);

        Gdx.input.setInputProcessor(stage);
    }

    @Override
    public void render(float delta) {
        Gdx.gl.glClearColor(0, 0, 0, 1);
        Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
        stage.act();
        stage.draw();
    }

    @Override
    public void resize(int width, int height) {

    }

    @Override
    public void pause() {

    }

    @Override
    public void resume() {

    }

    @Override
    public void hide() {
        dispose();
    }

    @Override
    public void dispose() {
        stage.dispose();
    }
}
core/src/com/fteams/sstrain/util/Accuracy.java
public enum Accuracy {
    PERFECT,
    GREAT,
    NICE,
    BAD,
    MISS,
    NONE
}
core/src/com/fteams/sstrain/objects/AccuracyMarker.java
public class AccuracyMarker {
    // stay on screen for 5 seconds
    public float displayTime = 5f;
    private float time;
    public boolean display;

    public AccuracyMarker(float time) {
        this.time = time;
        display = true;
    }

    public void update(float delta) {
        if (!display)
            return;
        displayTime -= delta;
        if (displayTime <= 0) {
            display = false;
        }
    }

    public float getAlpha() {
        float alpha = displayTime / 5f;
        return (alpha >= 1f ? 1f : (alpha <= 0f ? 0f : alpha));
    }

    public float getTime() {
        return time;
    }
}
core/src/com/fteams/sstrain/objects/AccuracyPopup.java
public class AccuracyPopup {

    private float screenTime;
    public boolean show;
    public Accuracy accuracy;
    public boolean soon;
    public boolean fadeIn;
    public float fadeTime = 0.25f;

    public AccuracyPopup(Accuracy accuracy, boolean soon) {
        this.screenTime = 0.5f;
        this.accuracy = accuracy;
        this.soon = soon;
        fadeIn = true;
        fadeTime = 0.25f;
        show = true;
    }

    public void update(float delta) {
        screenTime -= delta;
        fadeTime -= delta;
        if (screenTime <= 0 && show) {
            show = false;
        }
        if (fadeTime <= 0 && fadeIn) {
            fadeIn = false;
        }
    }

    public float getSize() {
        return 1f + (0.5f - screenTime);
    }

    public float getAlpha() {
        float alpha = fadeIn ? 0.75f + screenTime : screenTime / 0.25f;
        return (alpha >= 1f ? 1f : (alpha <= 0f ? 0f : alpha));
    }
}
core/src/com/fteams/sstrain/entities/Results.java
public class Results {
    public static Integer combo;
    public static float accuracy;
    public static int miss;
    public static int bads;
    public static int goods;
    public static int greats;
    public static int perfects;
    public static float maxAccuracy;
    public static float minAccuracy;
    public static float normalizedAccuracy;
    public static float unstableRating;

    public static void clear() {
        combo = 0;
        accuracy = 0;
        miss = 0;
        bads = 0;
        goods = 0;
        greats = 0;
        perfects = 0;
        maxAccuracy = 0;
        minAccuracy = 0;
        normalizedAccuracy = 0;
        unstableRating = 0;
    }


    public static float getAccuracyMultiplierForAccuracy(Accuracy accuracy) {
        if (accuracy == Accuracy.PERFECT) {
            return 1.0f;
        }
        if (accuracy == Accuracy.GREAT) {
            return 0.75f;
        }
        if (accuracy == Accuracy.NICE) {
            return 0.50f;
        }
        if (accuracy == Accuracy.BAD) {
            return 0.25f;
        }
        return 0f;
    }

    public static Accuracy getAccuracyFor(float timing) {
        // Perfect
        if (Math.abs(timing) < SongUtils.overallDiffPerfect[GlobalConfiguration.overallDifficulty] / 1000) {
            return Accuracy.PERFECT;
        }
        if (Math.abs(timing) < SongUtils.overallDiffGreat[GlobalConfiguration.overallDifficulty]/ 1000) {
            return Accuracy.GREAT;
        }
        if (Math.abs(timing) < SongUtils.overallDiffNice[GlobalConfiguration.overallDifficulty]/ 1000) {
            return Accuracy.NICE;
        }
        if (Math.abs(timing) < SongUtils.overallDiffBad[GlobalConfiguration.overallDifficulty]/ 1000) {
            return Accuracy.BAD;
        }
        return Accuracy.MISS;
    }

    // holds and swipes have bigger windows
    public static Accuracy getAccuracyForSwipesAndHolds(float timing) {
        return getAccuracyFor(timing * SWIPE_HOLD_MULTIPLIER);
    }

    public final static float SWIPE_HOLD_MULTIPLIER = 0.5f;
}
core/src/com/fteams/sstrain/World.java
public class World {
    int width;
    int height;

    public int combo;
    public boolean started;
    public boolean paused;
    public int offsetX;
    public int offsetY;

    private Array<AccuracyMarker> accuracyMarkers;
    private Array<AccuracyPopup> accuracyPopups;

    Array<TapZone> tapZones = new Array<>();
    Array<Circle> circles = new Array<>();

    public float delay;

    public World() {
        createWorld();
    }

    private void createWorld() {
        float x = 0f;
        float y = 0f;

        float h = 400;
        float w = 600;

        float radius = h * 0.065f;

        Double noteSpeed = SongUtils.getSpeedFromConfig(GlobalConfiguration.noteSpeed) / 1000.0;

        delay = Assets.selectedBeatmap.metadata.leadIn != null ? Assets.selectedBeatmap.metadata.leadIn : 0f;

        if (delay < noteSpeed) {
            delay += noteSpeed;
        }

        for (Note notesInfo : Assets.selectedBeatmap.notes) {

            // we create a copy which is modified based on the live options - speed / a-b repeat
            Note copy = copy(notesInfo);
            if (GlobalConfiguration.playbackMode != null && GlobalConfiguration.playbackMode.equals(SongUtils.GAME_MODE_ABREPEAT)) {
                if (GlobalConfiguration.aTime != null) {
                    if (copy.timing < GlobalConfiguration.aTime) {
                        continue;
                    }
                }
                if (GlobalConfiguration.bTime != null) {
                    if (copy.timing > GlobalConfiguration.bTime + 2f) {
                        continue;
                    }
                }
            }
            if (GlobalConfiguration.playbackRate != null) {
                copy.timing = copy.timing / GlobalConfiguration.playbackRate;

            }

            x = (copy.endPos - 3) * radius * 4;
            Circle mark = new Circle(x, 0, copy, noteSpeed, delay);
            circles.add(mark);
        }
//        System.out.println("Loaded: " + circles.size + " notes");

        linkCircles(circles);
        linkSyncCircles(circles);

        circles.sort();

        int zoneId = 1;
        tapZones = new Array<>();

        for (int i = 0; i < 11; i++) {
            if (i % 2 == 0)
                continue;

            x = (i * 2 - 10) * radius;

            TapZone zone = new TapZone(x, -249.0f, zoneId++);
            tapZones.add(zone);
        }
        tapZones.sort();
        this.accuracyMarkers = new Array<>();
        this.accuracyPopups = new Array<>();
        paused = false;
    }

    private Note copy(Note notesInfo) {
        Note copy = new Note();
        copy.status = notesInfo.status;
        copy.timing = notesInfo.timing;
        copy.id = notesInfo.id;
        copy.nextNoteId = notesInfo.nextNoteId;
        copy.prevNoteId = notesInfo.prevNoteId;
        copy.endPos = notesInfo.endPos;
        copy.startPos = notesInfo.startPos;
        copy.groupId = notesInfo.groupId;
        copy.sync = notesInfo.sync;
        copy.type = notesInfo.type;
        return copy;
    }

    private void linkCircles(Array<Circle> circles) {
        for (int i = 0; i < circles.size; i++) {
            Circle current = circles.get(i);
            if (current.note.nextNoteId == 0)
                continue;

            Circle next = findNext(circles, current.note.nextNoteId);

            // next can be null in A-B repeat mode since not all the circles are loaded.
            if (next != null) {
                current.setNextNote(next);
                current.nextNote.setPreviousNote(current);
            }
            // 2 side relation
        }
    }

    private Circle findNext(Array<Circle> circles, Long nextNoteId) {
        for (Circle circle : circles) {
            if (circle.note.id.equals(nextNoteId))
                return circle;
        }
        return null;
    }

    private void linkSyncCircles(Array<Circle> circles) {
        for (int i = 0; i < circles.size; i++) {
            Circle mark = circles.get(i);
            if (mark.note.sync != 0) {
                Circle next = findNextSync(circles, mark.note.timing, mark.note.id);
                if (next != null) {
                    mark.nextSyncNote = next;
                }
            }
        }

    }

    private Circle findNextSync(Array<Circle> circles, Double timing, Long id) {
        for (int i = 0; i < circles.size; i++) {
            Circle circle = circles.get(i);
            if (circle.note.timing.equals(timing) && circle.note.id > id) {
                return circle;
            }
        }
        return null;
    }

    public Array<TapZone> getTapZones() {
        return tapZones;
    }

    public Array<Circle> getCircles() {
        return circles;
    }

    public Array<AccuracyMarker> getAccuracyMarkers() {
        return accuracyMarkers;
    }

    public Array<AccuracyPopup> getAccuracyPopups() {
        return accuracyPopups;
    }

    public void setSize(int width, int height, int offsetX, int offsetY) {
        this.width = width;
        this.height = height;
        this.offsetX = offsetX;
        this.offsetY = offsetY;
    }
}
core/src/com/fteams/sstrain/config/GlobalConfiguration.java
public class GlobalConfiguration {

    // package info
    public static String appVersionName;

    public static int songVolume;
    public static int feedbackVolume;
    // offset in milliseconds
    public static int offset;
    public static int inputOffset;
    public static int noteSpeed;
    public static int overallDifficulty;
    // path to beatmaps
    public static String pathToBeatmaps;
    public static boolean playHintSounds;
    public static int sortOrder;
    // sorting related
    public static int sortMode;

    // display the line in the live screen
    public static boolean displayLine;

    // sync config
    public static int syncMode;

    public final static int BASE_HEIGHT = 720;

    // not stored/remembered variables:
    public static Float playbackRate;
    public static Integer playbackMode;
    public static Float aTime;
    public static Float bTime;


    public static void loadConfiguration() {
        Preferences prefs = Gdx.app.getPreferences("ss_train_config");
        offset = prefs.getInteger("offset", 0);
        inputOffset = prefs.getInteger("input_offset", 0);
        songVolume = prefs.getInteger("song_vol", 100);
        feedbackVolume = prefs.getInteger("feedback_vol", 100);
        pathToBeatmaps = prefs.getString("path_to_beatmaps", Gdx.files.getExternalStoragePath() + "sstrain");
        playHintSounds = prefs.getBoolean("play_hint_sounds", false);
        noteSpeed = prefs.getInteger("note_speed", 6);
        overallDifficulty = prefs.getInteger("overall_difficulty", 7);
        displayLine = prefs.getBoolean("display_line", true);
        // default to song name sorting
        sortMode = prefs.getInteger("sorting_mode", SongUtils.SORTING_MODE_SONG_NAME);
        // default ascending order
        sortOrder = prefs.getInteger("sorting_order", SongUtils.SORTING_MODE_ASCENDING);
        // sync mode
        syncMode = prefs.getInteger("sync_mode", SongUtils.SYNC_MODE_1);

    }

    public static void storeConfiguration() {
        Preferences prefs = Gdx.app.getPreferences("ss_train_config");
        prefs.putInteger("offset", offset);
        prefs.putInteger("input_offset", inputOffset);
        prefs.putInteger("song_vol", songVolume);
        prefs.putInteger("feedback_vol", feedbackVolume);
        prefs.putString("path_to_beatmaps", pathToBeatmaps);
        prefs.putBoolean("play_hint_sounds", playHintSounds);
        prefs.putInteger("note_speed", noteSpeed);
        prefs.putInteger("overall_difficulty", overallDifficulty);
        prefs.putBoolean("display_line", displayLine);
        prefs.putInteger("sorting_mode", sortMode);
        prefs.putInteger("sorting_order", sortOrder);
        prefs.putInteger("sync_mode", syncMode);
        prefs.flush();
    }
}
core/src/com/fteams/sstrain/assets/Assets.java
public class Assets {

    public static AssetManager internalManager = new AssetManager(new InternalFileHandleResolver());
    public static AssetManager externalManager = new AssetManager(new ExternalFileHandleResolver());

    static {
        externalManager.setLoader(List.class, new SimplifiedBeatmapLoader(new ExternalFileHandleResolver()));
    }

    public static final String BEATMAP_HOME = "sstrain/beatmaps/";
    public static final String SOUNDFILES_HOME = "sstrain/soundfiles/";

    public static Beatmap selectedBeatmap;
    public static BeatmapGroup selectedGroup;

    public static TextureAtlas atlas;

    public static Skin menuSkin;
    public static Sound noHitTapSound;
    public static Sound niceTapSound;
    public static Sound greatTapSound;
    public static Sound perfectTapSound;
    public static Sound niceSwipeSound;
    public static Sound greatSwipeSound;
    public static Sound perfectSwipeSound;

    public static BitmapFont font;
    public static BitmapFont songFont;

    public static Texture mainMenuBackgroundTexture;
    public static Texture holdBG;

    public static Array<BeatmapGroup> songGroup;

    // In here we'll put everything that needs to be loaded in this format:
    // manager.load("file location in assets", fileType.class);
    //
    // libGDX AssetManager currently supports: Pixmap, Texture, BitmapFont,
    //     TextureAtlas, TiledAtlas, TiledMapRenderer, Music and Sound.
    public static void queueLoading() {
        internalManager.load("textures/textures.pack.atlas", TextureAtlas.class);
        internalManager.load("hitsounds/no_hit_tap.mp3", Sound.class);
        internalManager.load("hitsounds/tap_nice.mp3", Sound.class);
        internalManager.load("hitsounds/tap_great.mp3", Sound.class);
        internalManager.load("hitsounds/tap_perfect.mp3", Sound.class);
        internalManager.load("hitsounds/swipe_nice.mp3", Sound.class);
        internalManager.load("hitsounds/swipe_great.mp3", Sound.class);
        internalManager.load("hitsounds/swipe_perfect.mp3", Sound.class);
        internalManager.load("bigimages/main_menu_background.jpg", Texture.class);
        internalManager.load("images/hold_background.png", Texture.class);
        internalManager.load("fonts/combo-font.fnt", BitmapFont.class);
        internalManager.load("fonts/song-font.fnt", BitmapFont.class);
        reloadBeatmaps();
    }

    // thanks to libgdx, the manager will not actually load maps which were already loaded,
    // so if the same file comes again, it will be skipped
    public static void reloadBeatmaps() {
        if (Gdx.files.absolute(Gdx.files.getExternalStoragePath() + BEATMAP_HOME).exists()) {
            for (String fileName : Gdx.files.absolute(Gdx.files.getExternalStoragePath() + BEATMAP_HOME).file().list()) {
                String fullPath = Gdx.files.getExternalStoragePath() + BEATMAP_HOME + fileName;
                // if for any reason the user placed .osu/.osz files in the datafiles, we process them
                if (Gdx.files.absolute(fullPath).isDirectory() || (!fileName.endsWith(".json")))
                    continue;

                externalManager.load(BEATMAP_HOME + fileName, List.class);
            }
        } else {
            (Gdx.files.absolute(Gdx.files.getExternalStoragePath() + "beatmaps")).mkdirs();
            (Gdx.files.absolute(Gdx.files.getExternalStoragePath() + BEATMAP_HOME)).mkdirs();
            (Gdx.files.absolute(Gdx.files.getExternalStoragePath() + SOUNDFILES_HOME)).mkdirs();
        }
    }

    // unlike the simple reload, in the hard reload we unload everything from the external manager
    // and force a reload of the beatmaps - this will cause .osz files which weren't extracted
    // to be processed, .osu files to be converted and music files within the .osz packages
    // to be copied over to the /beatmaps/soundfiles/ folder.
    public static void hardReloadBeatmaps() {
        selectedBeatmap = null;
        selectedGroup = null;
        externalManager.clear();
        reloadBeatmaps();
    }

    //In here we'll create our skin, so we only have to create it once.
    public static void setMenuSkin() {
        if (menuSkin == null)
            menuSkin = new Skin(Gdx.files.internal("skins/menuSkin.json"), internalManager.get("textures/textures.pack.atlas", TextureAtlas.class));
    }

    public static void setTextures() {
        if (atlas == null)
            atlas = internalManager.get("textures/textures.pack.atlas");

        if (mainMenuBackgroundTexture == null)
            mainMenuBackgroundTexture = internalManager.get("bigimages/main_menu_background.jpg");

        if (holdBG == null)
            holdBG = internalManager.get("images/hold_background.png");
    }

    public static void setFonts() {
        if (font == null) {
            font = internalManager.get("fonts/combo-font.fnt");
        }
        if (songFont == null) {
            songFont= internalManager.get("fonts/song-font.fnt");
        }

    }

    public static void setHitsounds() {
        if (noHitTapSound == null)
            noHitTapSound = internalManager.get("hitsounds/no_hit_tap.mp3");
        if (niceTapSound == null)
            niceTapSound = internalManager.get("hitsounds/tap_nice.mp3");
        if (greatTapSound == null)
            greatTapSound = internalManager.get("hitsounds/tap_great.mp3");
        if (perfectTapSound == null)
            perfectTapSound = internalManager.get("hitsounds/tap_perfect.mp3");
        if (niceSwipeSound == null)
            niceSwipeSound = internalManager.get("hitsounds/swipe_nice.mp3");
        if (greatSwipeSound == null)
            greatSwipeSound = internalManager.get("hitsounds/swipe_great.mp3");
        if (perfectSwipeSound == null)
            perfectSwipeSound = internalManager.get("hitsounds/swipe_perfect.mp3");
    }

    @SuppressWarnings("unchecked")
    public static void setSongs() {
        if (songGroup == null) {
            songGroup = new Array<>();
        } else {
            songGroup.clear();
        }

        Array<String> assets = externalManager.getAssetNames();
        Map<Long, BeatmapGroup> groupMap = new HashMap<>();

        for (String string : assets) {
            List<Beatmap> beatmaps = externalManager.get(string, List.class);
            if (!beatmaps.isEmpty()) {
                Metadata metadata = beatmaps.get(0).metadata;
                Long liveId = metadata.id;
                if (groupMap.get(liveId) == null) {
                    BeatmapGroup group = new BeatmapGroup();
                    group.metadata = new BaseMetadata();
                    group.metadata.id = metadata.id;
                    group.metadata.composer = metadata.composer;
                    group.metadata.lyricist = metadata.lyricist;
                    group.metadata.songFile = metadata.songFile;
                    group.metadata.songName = metadata.songName;
                    group.metadata.attribute = metadata.attribute;
                    group.metadata.duration = metadata.duration;
                    group.beatmaps = new Array<>();
                    groupMap.put(liveId, group);
                }

                BeatmapGroup group = groupMap.get(liveId);
                for (Beatmap beatmap : beatmaps) {
                    group.beatmaps.add(beatmap);
                }
                group.beatmaps.sort();
            }
        }
        for (Long liveId : groupMap.keySet()) {
            songGroup.add(groupMap.get(liveId));
        }
        songGroup.sort();
    }

    public static boolean update() {
        return internalManager.update() && externalManager.update();
    }

    public static float getProgress() {
        return (internalManager.getProgress() + externalManager.getProgress()) / 2;
    }
}
core/src/com/fteams/sstrain/objects/TapZone.java
public class TapZone implements Comparable<TapZone>{

    Integer id;

    public boolean pressed;
    public boolean warn;

    Vector2 position = new Vector2();
    public float touchTime = -1f;

    public TapZone(float x, float y, int i) {
        this.id = i;
        this.position.x = x;
        this.position.y = y;
        pressed = false;
        warn = false;
    }

    public void update(float delta) {

    }

    public Integer getId() {
        return id;
    }

    public Vector2 getPosition() {
        return position;
    }

    @Override
    public int compareTo(TapZone o) {
        return SongUtils.compare(id, o.id);
    }
}
core/src/com/fteams/sstrain/objects/Circle.java
public class Circle implements Comparable<Circle> {

    public Note note;

    public Vector2 origin = new Vector2();
    public Vector2 position = new Vector2();
    Vector2 velocity = new Vector2();

    public boolean hold;
    public Long destination;
    Double speed;
    public Circle nextNote;
    public Circle previousNote;
    public Circle nextSyncNote;

    float spawnTime;
    float despawnTime;
    float startWaitTime;
    float endWaitTime;

    float size;

    public float hitTime;
    float previousTime;
    long previousSystemTime;

    public boolean visible;
    public boolean holding;
    public boolean waiting;
    public boolean soundPlayed;
    public boolean miss;

    public float alpha = 1f;

    public Accuracy accuracy;
    public boolean processed;

    // holds consist of 2 notes out of which the first one must have type 2
    // the second one will either be a type 2 or a type 1 with some effect
    // notes with type 1 can be linked to other notes IF the effect != 0 and the groupID is set.

    public Circle(float x, float y, Note note, Double noteSpeed, float delay) {

        float timing = (float) (delay + note.timing * 1f + GlobalConfiguration.offset * 1f / 1000f);

        this.origin.x = x;
        this.origin.y = y;
        this.position.x = x;
        this.position.y = y;
        this.note = note;
        this.hold = (note.type & SongUtils.NOTE_TYPE_HOLD) != 0;
        // position goes 1-5
        this.destination = note.endPos;
        this.speed = noteSpeed;
        this.spawnTime = (float) (timing - speed);
        this.startWaitTime = (float) (timing - (hold || !note.status.equals(SongUtils.NOTE_NO_SWIPE) ? 2f : 1f) * SongUtils.overallDiffBad[GlobalConfiguration.overallDifficulty] / 1000f);
        this.endWaitTime = (float) (timing + (hold || !note.status.equals(SongUtils.NOTE_NO_SWIPE) ? 2f : 1f) * SongUtils.overallDiffBad[GlobalConfiguration.overallDifficulty] / 1000f);
        this.despawnTime = timing * 1.0f;
        this.size = 1f;
        this.previousSystemTime = 0L;

        hitTime = -9f;
        previousTime = 0f;

        initializeVelocity();
        initializeStates();
    }

    private void initializeStates() {
        visible = false;
        holding = false;
        soundPlayed = false;
        miss = false;
    }

    public void setPreviousNote(Circle previousNote) {
        this.previousNote = previousNote;
        if (previousNote != null) {
            if (previousNote.hold && previousNote.previousNote == null) {
                this.startWaitTime = (float) (despawnTime - 2f * SongUtils.overallDiffBad[GlobalConfiguration.overallDifficulty] / 1000f);
                this.endWaitTime = (float) (despawnTime + 2f * SongUtils.overallDiffBad[GlobalConfiguration.overallDifficulty] / 1000f);

            }
        }

    }

    public void setNextNote(Circle nextNote) {
        this.nextNote = nextNote;
    }

    private void initializeVelocity() {
        // unless the arc movement is implemented,
        // the notes will simply fall from the top towards the tap zones
        velocity.x = 0;
        velocity.y = (float) (-249 / speed);
    }

    public void update(float time) {

        if (miss || (accuracy != null && !holding)) {
            if (visible) {
                visible = false;
            }
            return;
        }

        if (spawnTime <= time && despawnTime > time && !visible) {
            visible = true;
        }

        if (spawnTime >= time && visible)
            visible = false;

        if (visible && despawnTime <= time) {
            if (GlobalConfiguration.playHintSounds && !soundPlayed) {
                // hint sounds play at 50% of the volume
                if (note.status.equals(SongUtils.NOTE_NO_SWIPE)) {
                    Assets.perfectTapSound.play(GlobalConfiguration.feedbackVolume / 200f);
                } else {
                    Assets.perfectSwipeSound.play(GlobalConfiguration.feedbackVolume / 200f);
                }
                soundPlayed = true;
            }

            if (holding) {
                alpha = 1f;
            } else {
                alpha = MathUtils.clamp((endWaitTime - time) / (endWaitTime - despawnTime), 0f, 1f);
                if (alpha == 0f)
                    visible = false;
            }
        }

        if (visible) {
            // TODO: implement parabolic movement of the notes towards the player and use the origin spot instead of spawning from the same lane (more SS-like)
            float scl = time - spawnTime;
            if (holding) {
                position.set(origin.cpy().x, origin.cpy().y - 249);
            } else
                position.set(origin.cpy().add(velocity.cpy().scl(scl)));
        }
        if (startWaitTime <= time && endWaitTime > time && !waiting && accuracy == null) {
            waiting = true;
        }

        processMiss(time);
        previousTime = time;
        previousSystemTime = System.currentTimeMillis();
    }


    private void processMiss(float time) {
        // miss if we miss the first note
        if (nextNote != null && hold && !holding && endWaitTime <= time && accuracy == null && !miss) {
            waiting = false;
            miss = true;
            accuracy = Accuracy.MISS;
            nextNote.miss = true;
            nextNote.accuracy = Accuracy.MISS;
            nextNote.processed = true;
            nextNote.waiting = false;
//            System.out.println("MISS-001: didn't hit the note (" + note.id + ")");
        } else if (nextNote == null && endWaitTime <= time && !miss && accuracy == null) {
            waiting = false;
            miss = true;
            accuracy = Accuracy.MISS;
//            System.out.println("MISS-002: didn't hit the note (" + note.id + ")");
        } else if (nextNote != null && !hold && endWaitTime <= time && accuracy == null && !miss) {
            waiting = false;
            miss = true;
            accuracy = Accuracy.MISS;
//            System.out.println("MISS-003: didn't hit the note (" + note.id + ")");
        }
        if (hold && !miss) {
            // miss if we hold for too long
            if (nextNote != null && nextNote.endWaitTime <= time && nextNote.accuracy == null) {
                miss = true;
                holding = false;
                waiting = false;
//                System.out.println("MISS-004: held for too long (" + note.id + ")");
                accuracy = Accuracy.MISS;
            }
        }
    }

    public Accuracy hit() {
//        System.out.println("H>" + note.id);

        // HIT DOESN'T COUNT FOR HOLD RELEASE!
        if (previousNote != null && previousNote.hold)
            return Accuracy.NONE;

        float delta = (System.currentTimeMillis() - previousSystemTime) / 1000f;
        float hit = previousTime + delta - despawnTime - GlobalConfiguration.inputOffset / 1000f;

        Accuracy accuracy = hold ? Results.getAccuracyForSwipesAndHolds(hit) : Results.getAccuracyFor(hit);
        // If the note was tapped too early, we ignore the tap
        if (despawnTime > previousTime && accuracy == Accuracy.MISS) {
            return Accuracy.NONE;
        }
        hitTime = hit;
        waiting = false;
        if (hold) {
            hitTime *= Results.SWIPE_HOLD_MULTIPLIER;
            holding = true;
        } else {
            visible = false;
        }
        this.accuracy = accuracy;
        return accuracy;
    }

    public Accuracy release() {
//        System.out.println("R>" + note.id);
        // if a non-hold is released it it counts as a miss.
        // some songs have notes with type 1 and status != 0 for release so we check if the previous note was a hold
        if (previousNote != null && previousNote.hold && !note.status.equals(SongUtils.NOTE_NO_SWIPE)) {
            accuracy = Accuracy.MISS;
            miss = true;
            visible = false;
            previousNote.release();
            waiting = false;
            // only type 2 can gain from a release.
            // type 1 with status calls release on swipe
            return accuracy;
        }
        // RELEASE DOESN'T COUNT FOR HOLD START
        if (holding) {
            holding = false;
            visible = false;
        }
        if (nextNote != null)
            return Accuracy.NONE;

        float delta = (System.currentTimeMillis() - previousSystemTime) / 1000f;
        float hit = previousTime + delta - despawnTime - GlobalConfiguration.inputOffset / 1000f;
        accuracy = Results.getAccuracyForSwipesAndHolds(hit);
        previousNote.release();
        waiting = false;
        // miss if we release before we start waiting
        if (accuracy == Accuracy.MISS) {
            waiting = false;
            visible = false;
            miss = true;
            processed = true;
//            System.out.println("MISS-005: released hold too early (" + note.id + ")");
        } else {
            hitTime = hit;
            hitTime *= Results.SWIPE_HOLD_MULTIPLIER;
        }
        return accuracy;
    }

    public Accuracy swipeLeft() {
        // some songs have notes with type 2 and status != 0
        if (note.status.equals(SongUtils.NOTE_NO_SWIPE) || note.status.equals(SongUtils.NOTE_SWIPE_RIGHT)) {
            return Accuracy.NONE;
        }
        if (previousNote != null && previousNote.hold) {
            previousNote.release();
        }

        if (previousNote != null && previousNote.previousNote != null) {
            if (previousNote.previousNote.note.status.equals(note.status) && !previousNote.isDone()) {
                return Accuracy.NONE;
            }
        }
        float delta = (System.currentTimeMillis() - previousSystemTime) / 1000f;
        float hit = previousTime + delta - despawnTime - GlobalConfiguration.inputOffset / 1000f;
        Accuracy accuracy = Results.getAccuracyForSwipesAndHolds(hit);
        // If the note was tapped too early, we ignore the tap
        if (despawnTime > previousTime && accuracy == Accuracy.MISS) {
            return Accuracy.NONE;
        }
        hitTime = hit;
        hitTime *= Results.SWIPE_HOLD_MULTIPLIER;
        waiting = false;
        this.accuracy = accuracy;
        visible = false;
        return accuracy;
    }

    public Accuracy swipeRight() {
        // some songs have notes with type 2 and status != 0
        // legne on pro, for instance.
        if (note.status.equals(SongUtils.NOTE_NO_SWIPE) || note.status.equals(SongUtils.NOTE_SWIPE_LEFT)) {
            return Accuracy.NONE;
        }
        if (previousNote != null && previousNote.hold) {
            previousNote.release();
        }

        if (previousNote != null && previousNote.previousNote != null) {
            if (previousNote.previousNote.note.status.equals(note.status) && !previousNote.isDone()) {
                return Accuracy.NONE;
            }
        }

        float delta = (System.currentTimeMillis() - previousSystemTime) / 1000f;
        float hit = previousTime + delta - despawnTime - GlobalConfiguration.inputOffset / 1000f;
        Accuracy accuracy = Results.getAccuracyForSwipesAndHolds(hit);
        // If the note was tapped too early, we ignore the tap
        if (despawnTime > previousTime && accuracy == Accuracy.MISS) {
            return Accuracy.NONE;
        }
        hitTime = hit;
        hitTime *= Results.SWIPE_HOLD_MULTIPLIER;
        waiting = false;
        this.accuracy = accuracy;
        visible = false;
        return accuracy;
    }

    public boolean isDone() {
        return miss || (accuracy != null && !holding);
    }

    @Override
    public int compareTo(Circle o) {
        if (o == null)
            return 1;
        // if the notes have the same timing, sort them by destination
        if (note.timing.equals(o.note.timing)) {
            return SongUtils.compare(note.endPos, o.note.endPos);
        }

        return Double.compare(note.timing, o.note.timing);
    }
}


Question: import com.badlogic.gdx.Game;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.audio.Music;
import com.badlogic.gdx.math.Vector2;
import com.badlogic.gdx.utils.Array;
import com.fteams.sstrain.World;
import com.fteams.sstrain.assets.Assets;
import com.fteams.sstrain.config.GlobalConfiguration;
import com.fteams.sstrain.entities.Results;
import com.fteams.sstrain.objects.AccuracyMarker;
import com.fteams.sstrain.objects.AccuracyPopup;
import com.fteams.sstrain.objects.Circle;
import com.fteams.sstrain.objects.TapZone;
import com.fteams.sstrain.screens.ResultsScreen;
import com.fteams.sstrain.util.Accuracy;
import com.fteams.sstrain.util.SongUtils;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
        // otherwise just play the beatmap
        {
            time += delta;
        }
        oldTime = time;
    }

    private float calculateAccuracy() {
        float sum = 0f;
        List<Float> high = new ArrayList<>();
        List<Float> low = new ArrayList<>();
        for (AccuracyMarker hit : world.getAccuracyMarkers()) {
            sum += hit.getTime();
        }
        float average = sum / world.getAccuracyMarkers().size;
        for (AccuracyMarker value : world.getAccuracyMarkers()) {
            if (value.getTime() >= average) {
                high.add(value.getTime());
            } else {
                low.add(value.getTime());
            }
        }

        Results.minAccuracy = calcAverage(low);
        Results.maxAccuracy = calcAverage(high);
        Results.unstableRating = 10 * calcDeviation(world.getAccuracyMarkers());

        return sum / world.getAccuracyMarkers().size;
    }

    private float calcAverage(List<Float> values) {
        float sum = 0;
        for (Float value : values) {
            sum += value;
        }
        return sum / values.size();
    }

    private float calcDeviation(Array<AccuracyMarker> values) {
        if (values.size == 0)
            return 0f;

        float sum = 0f;
        for (AccuracyMarker value : values) {
            sum += value.getTime();
        }

        float mean = sum / values.size;
        sum = 0f;
        for (AccuracyMarker value : values) {
            sum += (value.getTime() - mean) * (value.getTime() - mean);
        }
        return (float) Math.sqrt(sum / (values.size - 1));
    }


    private void processAccuracy(Accuracy accuracy, Accuracy accuracy2, boolean isHold) {
        if (!isHold) {
            if (accuracy == Accuracy.BAD) {
                badCount++;
                if (combo > largestCombo) {
                    largestCombo = combo;
                }
                combo = 0;
                world.combo = 0;
            } else if (accuracy == Accuracy.NICE) {
                goodCount++;
                if (combo > largestCombo) {
                    largestCombo = combo;
                }
                combo = 0;
                world.combo = 0;
            } else if (accuracy == Accuracy.GREAT) {
                greatCount++;
                combo++;
                world.combo = combo;
            } else if (accuracy == Accuracy.PERFECT) {
                perfectCount++;
                combo++;
                world.combo = combo;
            } else {
                missCount++;
                if (combo > largestCombo) {
                    largestCombo = combo;
                }
                combo = 0;
                world.combo = 0;
            }
        } else {
            // no combo break
            Accuracy lowest = accuracy.compareTo(accuracy2) >= 0 ? accuracy2 : accuracy;

            if (lowest == Accuracy.BAD) {
                badCount++;
            } else if (lowest == Accuracy.NICE) {
                goodCount++;
            } else if (lowest == Accuracy.GREAT) {
                greatCount++;
            } else if (lowest == Accuracy.PERFECT) {
                perfectCount++;
            } else {
                missCount++;
            }
            if (accuracy2.compareTo(Accuracy.NICE) > 0) {
                combo++;
                world.combo = combo;
            } else {
                if (combo > largestCombo) {
                    largestCombo = combo;
                }
                combo = 0;
                world.combo = 0;

            }

        }
    }

    private void playTapSoundForAccuracy(Accuracy accuracy) {
        if (accuracy == Accuracy.PERFECT) {
Answer:             Assets.perfectTapSound.play(GlobalConfiguration.feedbackVolume / 100f);