diff --git a/build.gradle b/build.gradle index a90df70..d4a54ad 100644 --- a/build.gradle +++ b/build.gradle @@ -57,7 +57,7 @@ configure(subprojects - project(':android')) { subprojects { version = "$projectVersion" - ext.appName = 'Funkin-Java' + ext.appName = 'Polyverse' repositories { mavenCentral() maven { url 'https://s01.oss.sonatype.org' } @@ -69,4 +69,4 @@ subprojects { } } -eclipse.project.name = 'Funkin-Java' + '-parent' +eclipse.project.name = 'Polyverse' + '-parent' diff --git a/flixelgdx/build.gradle b/flixelgdx/build.gradle index 474d702..f706a5c 100644 --- a/flixelgdx/build.gradle +++ b/flixelgdx/build.gradle @@ -8,6 +8,7 @@ dependencies { api "com.github.tommyettinger:libgdx-utils:$utilsVersion" api "io.github.libktx:ktx-freetype:$ktxVersion" api "games.rednblack.miniaudio:miniaudio:$miniaudioVersion" + api "org.fusesource.jansi:jansi:$jansiVersion" implementation "org.jetbrains:annotations:26.0.2-1" diff --git a/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/Flixel.java b/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/Flixel.java index 7a38623..eab5455 100644 --- a/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/Flixel.java +++ b/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/Flixel.java @@ -9,6 +9,7 @@ import games.rednblack.miniaudio.MASound; import games.rednblack.miniaudio.MiniAudio; import games.rednblack.miniaudio.loader.MASoundLoader; +import me.stringdotjar.flixelgdx.backend.FlixelPaths; import me.stringdotjar.flixelgdx.graphics.FlixelState; import me.stringdotjar.flixelgdx.graphics.FlixelCamera; import me.stringdotjar.flixelgdx.signal.FlixelSignal; @@ -63,7 +64,7 @@ public final class Flixel { */ public static void initialize(FlixelGame gameInstance) { if (initialized) { - throw new IllegalStateException("FlixelGDX has already been initialized!"); + throw new IllegalStateException("Flixel has already been initialized!"); } game = gameInstance; @@ -85,7 +86,7 @@ public static void initialize(FlixelGame gameInstance) { public static void switchState(FlixelState newState) { Signals.preStateSwitch.dispatch(new StateSwitchSignalData(newState)); if (!initialized) { - throw new IllegalStateException("Polyverse has not been initialized yet!"); + throw new IllegalStateException("Flixel has not been initialized yet!"); } if (newState == null) { throw new IllegalArgumentException("New state cannot be null!"); @@ -197,7 +198,7 @@ public static MASound playSound(String path, float volume, boolean looping, MAGr *
{@code
* // Notice how it uses the FlixelPaths class provided by Flixel'.
* // If null is passed down for the group, then the default sound group will be used.
- * // For the boolean attribuite "external", you only should make it true for mobile builds,
+ * // For the boolean attribute "external", you only should make it true for mobile builds,
* // otherwise just simply leave it be or make it "false" for other platforms like desktop.
* Flixel.playSound(FlixelPaths.external("your/path/here").path(), 1, false, null, true);
* }
@@ -213,7 +214,8 @@ public static MASound playSound(String path, float volume, boolean looping, MAGr
* @return The new sound instance.
*/
public static MASound playSound(@NotNull String path, float volume, boolean looping, MAGroup group, boolean external) {
- MASound sound = engine.createSound(path, (short) 0, (group != null) ? group : soundsGroup, external);
+ String resolvedPath = external ? path : FlixelPaths.resolveAudioPath(path);
+ MASound sound = engine.createSound(resolvedPath, (short) 0, (group != null) ? group : soundsGroup, external);
Signals.preSoundPlayed.dispatch(new SoundPlayedSignalData(sound));
sound.setVolume(volume);
sound.setLooping(looping);
@@ -290,7 +292,7 @@ public static void playMusic(String path, float volume, boolean looping) {
*
* {@code
* // Notice how it uses the FlixelPaths class provided by Flixel'.
- * // For the boolean attribuite "external", you only should make it true for mobile builds,
+ * // For the boolean attribute "external", you only should make it true for mobile builds,
* // otherwise just simply leave it be or make it "false" for other platforms like desktop.
* Flixel.playMusic(FlixelPaths.external("your/path/here").path(), 1, false, true);
* }
@@ -307,7 +309,8 @@ public static void playMusic(String path, float volume, boolean looping, boolean
if (music != null) {
music.stop();
}
- music = engine.createSound(path, (short) 0, soundsGroup, external);
+ String resolvedPath = external ? path : FlixelPaths.resolveAudioPath(path);
+ music = engine.createSound(resolvedPath, (short) 0, soundsGroup, external);
music.setVolume(volume);
music.setLooping(looping);
music.play();
@@ -463,15 +466,22 @@ private static void outputLog(String tag, Object message, FlixelLogLevel level)
};
boolean underline = (level == FlixelLogLevel.ERROR);
- String timeAndDate = colorText(LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")) + " ",
- color,
- true,
- false,
- underline);
- String formattedTag = colorText("[" + level + "] [" + tag + "] [" + file + "] [" + method + "] ", color, true, false, underline);
- String formattedMessage = colorText((message != null) ? message.toString() : null, color, false, true, underline);
-
- System.out.println(timeAndDate + formattedTag + formattedMessage);
+ String timestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
+ String rawTag = "[" + level + "] [" + tag + "] [" + file + "] [" + method + "] ";
+ String rawMessage = (message != null) ? message.toString() : "null";
+
+ // Plain text for the log file, no ANSI codes.
+ String plainLog = timestamp + " " + rawTag + rawMessage;
+
+ // ANSI-colored version for the console.
+ String coloredLog = colorText(timestamp + " ", color, true, false, underline)
+ + colorText(rawTag, color, true, false, underline)
+ + colorText(rawMessage, color, false, true, underline);
+
+ System.out.println(coloredLog);
+ if (game.canStoreLogs()) {
+ game.enqueueLog(plainLog);
+ }
}
private static String colorText(String text, String color, boolean bold, boolean italic, boolean underline) {
diff --git a/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/FlixelGame.java b/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/FlixelGame.java
index 0fb6ce3..adb4fa6 100644
--- a/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/FlixelGame.java
+++ b/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/FlixelGame.java
@@ -3,6 +3,7 @@
import com.badlogic.gdx.Application;
import com.badlogic.gdx.ApplicationListener;
import com.badlogic.gdx.Gdx;
+import com.badlogic.gdx.files.FileHandle;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.graphics.Pixmap;
import com.badlogic.gdx.graphics.Texture;
@@ -18,9 +19,14 @@
import me.stringdotjar.flixelgdx.graphics.text.FlixelFontRegistry;
import me.stringdotjar.flixelgdx.tween.FlixelTween;
import me.stringdotjar.flixelgdx.util.FlixelRuntimeUtil;
+import org.fusesource.jansi.AnsiConsole;
import static me.stringdotjar.flixelgdx.signal.FlixelSignalData.UpdateSignalData;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.concurrent.ConcurrentLinkedQueue;
+
/**
* Flixel's enhanced game object used for containing the main loop and core elements of Flixel.
*
@@ -59,9 +65,30 @@ public abstract class FlixelGame implements ApplicationListener {
/** Where all the global cameras are stored. */
protected SnapshotArrayOverride this method instead of {@link #dispose()} when you need to add
+ * cleanup logic. Do not override {@code dispose()} and add logging after
+ * {@code super.dispose()}, as the log thread will have already exited and logs
+ * will not be persisted to the file.
+ */
+ protected void close() {}
+
@Override
public void dispose() {
if (isClosing) {
@@ -295,7 +339,28 @@ public void dispose() {
Flixel.info("Disposing all registered fonts...");
FlixelFontRegistry.dispose();
+ close();
+
Flixel.Signals.postGameClose.dispatch();
+
+ // Signal the log thread to drain the queue and exit. Must happen AFTER all dispose-time
+ // logs are added, so they get written before the thread exits.
+ synchronized (logQueueLock) {
+ logWriterShutdownRequested = true;
+ logQueueLock.notify();
+ }
+
+ // Wait for the log thread to flush all remaining logs to disk before marking closed.
+ if (logThread != null && logThread.isAlive()) {
+ try {
+ logThread.join(5000);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ Flixel.warn("Interrupted while waiting for log thread to finish.");
+ }
+ }
+
+ isClosed = true;
}
/**
@@ -315,6 +380,73 @@ protected void configureCrashHandler() {
});
}
+ /**
+ * Sets up the log writer thread to write logs to a file.
+ */
+ protected void setupLogWriterThread() {
+ String path = FlixelRuntimeUtil.getWorkingDirectory();
+ if (path == null) {
+ return;
+ }
+ String logsFolder = path.substring(0, path.lastIndexOf('/')) + "/logs/";
+
+ if (canStoreLogs) {
+ logThread = new Thread(() -> {
+ try {
+ LocalDateTime now = LocalDateTime.now();
+ DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH-mm-ss");
+ String date = now.format(formatter);
+
+ // Create/get the path to the logs folder, which is inside the game's working directory.
+ Gdx.files.absolute(logsFolder).mkdirs();
+
+ // Check if the logs folder has too many log files, and if so, delete the oldest ones.
+ FileHandle[] logFiles = Gdx.files.absolute(logsFolder).list();
+ if (logFiles != null && logFiles.length > maxLogFiles - 1) {
+ for (int i = 0; i < logFiles.length - maxLogFiles - 1; i++) {
+ logFiles[i].delete();
+ }
+ }
+
+ FileHandle logFile = Gdx.files.absolute(logsFolder + "/flixel-" + date + ".log");
+
+ // Keep running until shutdown is requested AND the queue is fully drained.
+ // This ensures logs added during dispose() (e.g. "Disposing...") are written.
+ while (true) {
+ String log = logQueue.poll();
+ if (log != null) {
+ logFile.writeString(log + "\n", true);
+ } else {
+ synchronized (logQueueLock) {
+ if (logWriterShutdownRequested) {
+ break;
+ }
+ try {
+ logQueueLock.wait();
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ }
+ }
+ }
+ } catch (Exception e) {
+ Flixel.error("Failed to setup log thread and store logs to a file.", e);
+ }
+ });
+ logThread.setName("FlixelGDX Log Thread");
+ logThread.setDaemon(true);
+ logThread.start();
+ } else {
+ Flixel.warn("Log storage is disabled. Logs will not be stored to a file. Deleting all existing log files...");
+ FileHandle[] logFiles = Gdx.files.absolute(logsFolder).list();
+ if (logFiles != null) {
+ for (FileHandle logFile : logFiles) {
+ logFile.delete();
+ }
+ }
+ }
+ }
+
public String getTitle() {
return title;
}
@@ -335,6 +467,29 @@ public Stage getStage() {
return stage;
}
+ public boolean canStoreLogs() {
+ return canStoreLogs;
+ }
+
+ public void setCanStoreLogs(boolean canStoreLogs) {
+ this.canStoreLogs = canStoreLogs;
+ }
+
+ public ConcurrentLinkedQueue When running from the IDE the working directory is the {@code assets/} folder, so the raw
+ * relative path works as-is. When running from a packaged JAR the assets are embedded as
+ * classpath resources and MiniAudio cannot open them by name. In that case the resource is
+ * extracted to a temp file on first call, and the temp file's absolute path is returned. Results
+ * are cached so repeated calls for the same path do not produce extra temp files.
+ *
+ * @param path The internal asset path, e.g. {@code "shared/sounds/foo.ogg"}.
+ * @return An absolute filesystem path that MiniAudio can open.
+ */
+ public static String resolveAudioPath(String path) {
+ return audioPathCache.computeIfAbsent(path, FlixelPaths::extractAudioPath);
+ }
+
+ private static String extractAudioPath(String path) {
+ FileHandle handle = asset(path);
+ if (handle.file().exists()) {
+ return handle.file().getAbsolutePath();
+ }
+ // Asset is inside a JAR, copy it out to a temp file so MiniAudio can open it.
+ String ext = path.contains(".") ? path.substring(path.lastIndexOf('.')) : "";
+ try {
+ File temp = File.createTempFile("flixelaudio_", ext);
+ temp.deleteOnExit();
+ handle.copyTo(new FileHandle(temp));
+ return temp.getAbsolutePath();
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to extract audio asset from JAR: " + path, e);
+ }
+ }
+
private FlixelPaths() {}
}
diff --git a/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/graphics/FlixelCamera.java b/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/graphics/FlixelCamera.java
index 54dea1d..5bc2ae3 100644
--- a/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/graphics/FlixelCamera.java
+++ b/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/graphics/FlixelCamera.java
@@ -25,8 +25,7 @@
*