package de.geolykt.starloader.impl; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.PrintWriter; import java.io.StringWriter; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Deque; import java.util.List; import java.util.Objects; import java.util.Vector; import java.util.concurrent.ConcurrentLinkedDeque; import org.jetbrains.annotations.ApiStatus.AvailableSince; import org.jetbrains.annotations.ApiStatus.Internal; import org.jetbrains.annotations.ApiStatus.ScheduledForRemoval; import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.UnmodifiableView; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.badlogic.gdx.Gdx; import com.badlogic.gdx.graphics.GL20; import de.geolykt.starloader.DeprecatedSince; import de.geolykt.starloader.ExpectedObfuscatedValueException; import de.geolykt.starloader.Starloader; import de.geolykt.starloader.api.Galimulator; import de.geolykt.starloader.api.actor.Actor; import de.geolykt.starloader.api.actor.SpawnPredicatesContainer; import de.geolykt.starloader.api.actor.WeaponsManager; import de.geolykt.starloader.api.dimension.Empire; import de.geolykt.starloader.api.empire.Alliance; import de.geolykt.starloader.api.empire.Star; import de.geolykt.starloader.api.empire.War; import de.geolykt.starloader.api.empire.people.DynastyMember; import de.geolykt.starloader.api.gui.BackgroundTask; import de.geolykt.starloader.api.gui.Drawing; import de.geolykt.starloader.api.gui.MapMode; import de.geolykt.starloader.api.gui.MouseInputListener; import de.geolykt.starloader.api.resource.DataFolderProvider; import de.geolykt.starloader.api.serial.SavegameFormat; import de.geolykt.starloader.api.serial.SupportedSavegameFormat; import de.geolykt.starloader.api.sound.SoundHandler; import de.geolykt.starloader.api.utils.RandomNameType; import de.geolykt.starloader.api.utils.TickLoopLock; import de.geolykt.starloader.impl.actors.GlobalSpawningPredicatesContainer; import de.geolykt.starloader.impl.asm.SpaceASMTransformer; import de.geolykt.starloader.impl.gui.ForwardingListener; import de.geolykt.starloader.impl.gui.GLScissorState; import de.geolykt.starloader.impl.gui.VanillaBackgroundTask; import de.geolykt.starloader.impl.serial.BoilerplateSavegameFormat; import de.geolykt.starloader.impl.serial.VanillaSavegameFormat; import de.geolykt.starloader.mod.Extension; import snoddasmannen.galimulator.Galemulator; import snoddasmannen.galimulator.MapData; import snoddasmannen.galimulator.MapMode.MapModes; import snoddasmannen.galimulator.Player; import snoddasmannen.galimulator.ProceduralStarGenerator; import snoddasmannen.galimulator.Scenario; import snoddasmannen.galimulator.Space; import snoddasmannen.galimulator.SpaceState; import snoddasmannen.galimulator.VanityHolder; import snoddasmannen.galimulator.ui.ModUploadWidget; import snoddasmannen.galimulator.ui.OptionChooserWidget; import snoddasmannen.namegenerator.NameGenerator; // TODO split the unsafe impl and the game impl public class GalimulatorImplementation implements Galimulator.GameImplementation, Galimulator.Unsafe { /** * The logger that is used within this class. */ protected static final Logger LOGGER = LoggerFactory.getLogger(GalimulatorImplementation.class); /** * A small hack {@link Runnable} that serves as a marker to represent a tick barrier within {@link #SCHEDULED_TASKS_NEXT_TICK}. * That is all tasks before that barrier belong to the current tick, where as all tasks after the barrier belong to the next tick. * This behaviour is required in order for calls to {@link #runTaskOnNextTick(Runnable)} work as intended within * a {@link #runTaskOnNextTick(Runnable)} task. * *
Executing this runnable does nothing, although this is an implementation detail.
*
* @since 2.0.0
* @see #fireScheduledTasks()
*/
@NotNull
private static final Runnable NEXT_TICK_TASK = () -> {};
/**
* A {@link ThreadLocal} variable that stores whether the current thread is the main thread.
*
* @since 2.0.0
* @see #isRenderThread()
*/
private static final ThreadLocal As usual with anything in the impl package, this field is not official API.
* The fact that this is documented does not change that.
*
* @since 2.0.0
*/
public final List Warning: This is not public API. Use {@link Galimulator#panic(String, boolean)}
* instead.
*
* @param cause The description of the cause of the issue.
* @param save True if the current game state should be written to disk
* @since 2.0.0
*/
public static void crash(@NotNull String cause, boolean save) {
@NotNull
Throwable backtrace = new AssertionError("GalimulatorImplementation.crash() called: " + cause).fillInStackTrace();
GalimulatorImplementation.crash(backtrace, cause, save);
}
/**
* Renders a crash report to the screen and log. This action cannot be undone.
*
* Warning: This is not public API. Use {@link Galimulator#panic(String, boolean, Throwable)}
* instead.
*
* @param e The stacktrace that should be displayed. Stacktraces are powerful tools to debug issues
* @param cause The description of the cause of the issue.
* @param save True if the current game state should be written to disk
* @since 2.0.0
*/
public static void crash(@NotNull Throwable e, @NotNull String cause, boolean save) {
try {
if (!GalimulatorImplementation.isRenderThread()) {
Galimulator.setPaused(true);
} else {
Galimulator.setPaused(true); // Pause the game on crash so the simulation loop doesn't continue to run in the background.
Gdx.gl.glDisable(GL20.GL_SCISSOR_TEST); // Sometimes the game can crash while rendering, at which point a scissor might be applied. To render the entire crash message we might need to disable the scissor though.
GLScissorState.glScissor(0, 0, Gdx.graphics.getBackBufferWidth(), Gdx.graphics.getBackBufferHeight());
GLScissorState.forgetScissor();
}
} catch (Throwable ignored) {
}
Galemulator listener = (Galemulator) Gdx.app.getApplicationListener();
if (save) {
// TODO deobf
listener.h = "Game crashed! Saving what still can be saved... Please wait";
Thread thread = new Thread(() -> {
boolean threadDied = false;
try (FileOutputStream fos = new FileOutputStream(new File("crash-save.dat"))) {
Galimulator.getSavegameFormat(SupportedSavegameFormat.SLAPI_BOILERPLATE).saveGameState(fos, "Game crashed", "crash-save.dat", false);
} catch (Throwable t) {
if (t instanceof ThreadDeath) {
t.addSuppressed(e);
t.printStackTrace();
threadDied = true;
throw (ThreadDeath) t;
}
t.printStackTrace();
} finally {
if (!threadDied) {
GalimulatorImplementation.crash(e, cause, false);
}
}
}, "crash-saving-thread");
thread.start();
} else {
StringBuilder builder = new StringBuilder();
builder.append("This game is modded, report this crash report to the respective mod devs first, not snoddasmannen directly.\n\n");
builder.append("The crash report has also been printed to the log, give the FULL logs to the mod devs, not a screenshot of this.\n");
builder.append("Cause (for beginners): " + cause + "\n");
builder.append("Installed mods:\n");
for (Extension ext : Starloader.getExtensionManager().getExtensions()) {
builder.append(" " + ext.getDescription().getName() + " v" + ext.getDescription().getVersion() + "\n");
}
try {
Class.forName("com.badlogic.gdx.backends.lwjgl3.Lwjgl3Application");
builder.append("\n[RED]Alert: LWJGL 3 detected.[][LIME]\n");
} catch (ClassNotFoundException ignored) { }
builder.append("\nStacktrace:\n");
StringWriter sw = new StringWriter();
e.printStackTrace(new PrintWriter(sw));
sw.flush();
builder.append(sw.getBuffer().toString().replace("\t", " "));
listener.h = "[LIME]" + builder.toString();
for (String s : builder.toString().split("\n")) {
LoggerFactory.getLogger("CrashReporter").error(s);
}
try {
Galimulator.getSimulationLoopLock().acquireSoftControl();
Galimulator.getSimulationLoopLock().acquireHardControl();
} catch (InterruptedException interrupt) {
}
}
}
/**
* Execute all tasks that have been scheduled up to this point. All tasks that are scheduled while this method is called are
* delegated to the next time this method is called.
*
* As usual with anything in the impl package, this method is not official API.
* The fact that this is documented does not change that. This method is solely intended to be called from
* {@link SpaceASMTransformer#logicalTickEarly()}
*
* @since 2.0.0
*/
public static void fireScheduledTasks() {
GalimulatorImplementation.SCHEDULED_TASKS_NEXT_TICK.addLast(GalimulatorImplementation.NEXT_TICK_TASK);
Runnable r;
while ((r = GalimulatorImplementation.SCHEDULED_TASKS_NEXT_TICK.removeFirst()) != GalimulatorImplementation.NEXT_TICK_TASK) {
r.run();
}
}
/**
* Forcefully marks the current thread as the main rendering thread as per {@link Drawing#isRenderThread()}.
*
* This is mainly required when the name-based thread matching logic doesn't apply.
* In real-world usecases, roast will
* cause the main thread to be called "Thread-0" instead of "main", which is the expected name of the
* rendering thread under LWJGL 3.
* HOWEVER, since creating an unnamed thread outside of roast would cause that thread to be
* named "Thread-0", name-based matching logic may not be used else it might cause false positives.
*
* This method should really only be called by SLAPI, but {@link GalimulatorImplementation} is
* private API anyways - at least in theory.
*
* @since 2.0.0-a20260303
*/
@AvailableSince("2.0.0-a20260303")
@Contract(pure = false)
@Internal
public static void forceRenderThread() {
GalimulatorImplementation.RENDER_THREAD.set(true);
}
/**
* Obtains whether the current thread is the main thread.
* This is based on a ThreadLocal populated based on the Thread's name.
*
* @return True if the current thread is capable of rendering.
* @since 2.0.0
*/
public static boolean isRenderThread() {
return GalimulatorImplementation.RENDER_THREAD.get();
}
/**
* Converts a Galimulator map mode into a starloader API map mode.
* This is a clean cast and should never throw exception, except if there is an issue unrelated to this method.
*
* @param mode The map mode to convert
* @return The converted map mode
*/
@NotNull
private static MapMode toSLMode(@NotNull MapModes mode) {
return (MapMode) mode;
}
@Override
public void connectStars(@NotNull Star starA, @NotNull Star starB) {
Galimulator.getUniverse().connectStars(starA, starB);
}
@Override
public void disconnectStars(@NotNull Star starA, @NotNull Star starB) {
Galimulator.getUniverse().disconnectStars(starA, starB);
}
@SuppressWarnings({ "null", "unused" })
@Override
@NotNull
public String generateRandomName(@NotNull RandomNameType type) {
switch (type) {
case ADJECTIVE:
return NameGenerator.getRandomAdjective();
case FACTION_NAME:
return NameGenerator.generateRandomFactionName();
case IDENTIFIER:
return NameGenerator.generateRandomIdentifier();
case QUEST_NAME:
return NameGenerator.generateRandomQuestName();
case QUEST_NOMINATOR:
return NameGenerator.getRandomQuestNominator();
case REVOLT_NAME:
return NameGenerator.getRandomRevoltName();
case SHIP_NAME:
return NameGenerator.generateRandomShipName();
case VANITY_NAME:
return NameGenerator.getRandomVanityName();
default:
if (Objects.isNull(type)) {
throw new NullPointerException("type may not be null");
}
throw new IllegalStateException("Unknown enum value: " + type.name());
}
}
@SuppressWarnings("null")
@Override
public @NotNull MapMode getActiveMapmode() {
return toSLMode(snoddasmannen.galimulator.MapMode.getCurrentMode());
}
@SuppressWarnings("rawtypes")
@Override
public Vector