package de.geolykt.starloader.impl.asm; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.util.Arrays; import java.util.Objects; import java.util.concurrent.Semaphore; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.objectweb.asm.Opcodes; import org.objectweb.asm.tree.AbstractInsnNode; import org.objectweb.asm.tree.ClassNode; import org.objectweb.asm.tree.FieldInsnNode; import org.objectweb.asm.tree.InsnList; import org.objectweb.asm.tree.InsnNode; import org.objectweb.asm.tree.JumpInsnNode; import org.objectweb.asm.tree.LabelNode; import org.objectweb.asm.tree.LineNumberNode; import org.objectweb.asm.tree.MethodInsnNode; import org.objectweb.asm.tree.MethodNode; import org.objectweb.asm.tree.TypeInsnNode; import org.objectweb.asm.tree.VarInsnNode; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import de.geolykt.starloader.DebugNagException; import de.geolykt.starloader.DeprecatedSince; import de.geolykt.starloader.api.Galimulator; import de.geolykt.starloader.api.event.EventManager; import de.geolykt.starloader.api.event.empire.EmpireCollapseEvent; import de.geolykt.starloader.api.event.empire.EmpireCollapseEvent.EmpireCollapseCause; import de.geolykt.starloader.api.event.lifecycle.GalaxyGeneratedEvent; import de.geolykt.starloader.api.event.lifecycle.LogicalTickEvent; import de.geolykt.starloader.api.serial.SupportedSavegameFormat; import de.geolykt.starloader.impl.GalimulatorImplementation; import de.geolykt.starloader.starplane.annotations.MethodDesc; import de.geolykt.starloader.starplane.annotations.ReferenceSource; import de.geolykt.starloader.starplane.annotations.RemapClassReference; import de.geolykt.starloader.starplane.annotations.RemapMemberReference; import de.geolykt.starloader.starplane.annotations.RemapMemberReference.ReferenceFormat; import de.geolykt.starloader.transformers.ASMTransformer; import snoddasmannen.galimulator.GalimulatorGestureListener; import snoddasmannen.galimulator.MapData; import snoddasmannen.galimulator.MapMode.MapModes; import snoddasmannen.galimulator.Settings; import snoddasmannen.galimulator.Space; import snoddasmannen.galimulator.Star; import snoddasmannen.galimulator.interface_10; /** * Transformers targeting the Space class. * Also transforms other classes because we shouldn't have 200 highly specialised ASM transformers. * This might be changed once we move to the actual ASM Transformer class instead of the out-dated and deprecated * CodeModifier class which is less performant for higher numbers due to it never getting purged */ public class SpaceASMTransformer extends ASMTransformer { /** * The internal name of the {@link ActiveEmpire} class. */ private static final String ACTIVE_EMPIRE_CLASS = "de/geolykt/starloader/api/empire/ActiveEmpire"; /** * A field which can be used by more invasive mods to disable transformers that expect * the {@link Star#renderRegion()} method to be laid out like in vanilla galimulator. * *
As it is often the case, this field is mostly a hack and shouldn't be used * unless absolutely necessary. In laymen's terms: It is not public API. * Handle with care. * * @since 2.0.0-a20240509 */ private static boolean assumeVanillaRegionRenderingLogic = true; @NotNull @RemapMemberReference(ownerType = snoddasmannen.galimulator.factions.Faction.class, name = "d", desc = "()V", format = ReferenceFormat.NAME) private static final String FACTION_REBEL_METHOD_NAME = ReferenceSource.getStringValue(); /** * The remapped name of the "generateGalaxy" method. * * @since 2.0.0 * @see RemapMemberReference */ @RemapMemberReference(ownerType = Space.class, name = "generateGalaxy", methodDesc = @MethodDesc(args = {int.class, MapData.class}, ret = void.class), format = ReferenceFormat.COMBINED_LEGACY) @NotNull public static String generateGalaxyMethod = ReferenceSource.getStringValue(); @RemapClassReference(type = GalimulatorGestureListener.class) @NotNull public static String gestureListenerClass = ReferenceSource.getStringValue(); @NotNull @RemapMemberReference(ownerType = Space.class, name = "isPaused", desc = "()Z", format = ReferenceFormat.NAME) private static final String IS_PAUSED_METHOD_NAME = ReferenceSource.getStringValue(); /** * The logger object that should be used used throughout this class. */ private static final Logger LOGGER = LoggerFactory.getLogger(SpaceASMTransformer.class); @NotNull @RemapMemberReference(ownerType = MapModes.class, name = "getShowsActors" /* Interesting name */, methodDesc = @MethodDesc(args = {}, ret = boolean.class), format = ReferenceFormat.COMBINED_LEGACY) public static String mapModeShowsActorsMethod = ReferenceSource.getStringValue(); @NotNull @RemapMemberReference(ownerType = Space.class, name = "f", methodDesc = @MethodDesc(args = {snoddasmannen.galimulator.Empire.class}, ret = void.class), format = ReferenceFormat.NAME) private static final String ON_EMPIRE_COLLAPSE_METHOD_NAME = ReferenceSource.getStringValue(); /** * The remapped name of the "saveSync" method. * * @since 2.0.0 * @see RemapMemberReference */ @RemapMemberReference(ownerType = Space.class, name = "saveSync", methodDesc = @MethodDesc(args = {String.class, String.class}, ret = void.class), format = ReferenceFormat.COMBINED_LEGACY) @NotNull public static String saveSyncMethod = ReferenceSource.getStringValue(); /** * The remapped name of the "MAIN_TICK_LOOP_LOCK" / Simulation loop lock field. * * @since 2.0.0 * @see RemapMemberReference */ @RemapMemberReference(ownerType = Space.class, name = "MAIN_TICK_LOOP_LOCK", descType = Semaphore.class, format = ReferenceFormat.COMBINED_LEGACY) @NotNull public static String simLoopLockField = ReferenceSource.getStringValue(); /** * The internal name of the class that this transformer seeks to modify. */ private static final String SPACE_CLASS = "snoddasmannen/galimulator/Space"; @RemapMemberReference(ownerType = Star.class, name = "renderRegion", desc = "()V", format = ReferenceFormat.COMBINED_LEGACY) @NotNull public static String starRenderOverlayMethod = ReferenceSource.getStringValue(); /** * The remapped name of the "tick" method. * * @since 2.0.0 * @see RemapMemberReference */ @RemapMemberReference(ownerType = Space.class, name = "tick", desc = "()I", format = ReferenceFormat.COMBINED_LEGACY) @NotNull public static String tickMethod = ReferenceSource.getStringValue(); /** * The internal name of the class you are viewing right now right here. */ private static final String TRANSFORMER_CLASS = "de/geolykt/starloader/impl/asm/SpaceASMTransformer"; /** * This method can be used by more invasive mods to disable transformers that expect * the {@link Star#renderRegion()} method to be laid out like in vanilla galimulator. * *
As it is often the case, this method is mostly a hack and shouldn't be used * unless absolutely necessary. In laymen's terms: It is not public API. * Handle with care. * *
Further, this method does not perform any sanity check what the {@link Star}
* class hasn't yet been classloaded, so setting this flag may not have an effect
* if the transformation already occurred.
*
* @since 2.0.0-a20240509
*/
public static void assumeVanillaRegionRenderingLogic(boolean flag) {
if (flag == SpaceASMTransformer.assumeVanillaRegionRenderingLogic) {
return;
}
SpaceASMTransformer.assumeVanillaRegionRenderingLogic = flag;
}
/**
* Emits the {@link EmpireCollapseEvent}. A call to this method is automatically injected by the {@link #addEmpireCollapseListener(MethodNode)} method.
*
* @param empire The empire that collapsed.
* @return True if the collapse should be cancelled
* @deprecated The ActiveEmpire API is deprecated, deprecating this method by extension.
*/
@Deprecated
@ApiStatus.ScheduledForRemoval // Not official API, will be removed at any time
@DeprecatedSince("4.0.0-a20241102")
public static final boolean emitCollapseEvent(de.geolykt.starloader.api.empire.ActiveEmpire empire) {
EmpireCollapseEvent e = new EmpireCollapseEvent(empire, empire.getStarCount() == 0 ? EmpireCollapseCause.NO_STARS : EmpireCollapseCause.UNKNOWN);
if (e.getCause() == EmpireCollapseCause.UNKNOWN) {
// I have never seen this nag yet, so it can likely be assumed that the assumption is right.
DebugNagException.nag("This method is thought to be only used for GC after a empire has no stars!");
}
return e.isCancelled();
}
public static final void generateGalaxy(boolean finished) {
if (finished) {
EventManager.handleEvent(new GalaxyGeneratedEvent());
} else {
DebugNagException.nag();
}
}
/**
* Called at the very beginning of the global tick method.
*/
public static final void logicalTickEarly() {
GalimulatorImplementation.fireScheduledTasks();
EventManager.handleEvent(new LogicalTickEvent(LogicalTickEvent.Phase.PRE_GRAPHICAL));
}
/**
* Called at the end of the global tick method.
*/
public static final void logicalTickPost() {
EventManager.handleEvent(new LogicalTickEvent(LogicalTickEvent.Phase.POST));
}
/**
* Called at the beginning of the pause-sensitive portion of the global tick method.
*/
public static final void logicalTickPre() {
EventManager.handleEvent(new LogicalTickEvent(LogicalTickEvent.Phase.PRE_LOGICAL));
}
public static final void save(String cause, String location) {
if (location == null) {
throw new IllegalArgumentException("\"location\" may not be null.");
}
Space.getMainTickLoopLock().acquireUninterruptibly(2);
Space.backgroundTaskDescription = "Saving galaxy: " + cause;
LOGGER.info("Saving state to disk.");
try (FileOutputStream fos = new FileOutputStream(new File(location))) {
Galimulator.getSavegameFormat(SupportedSavegameFormat.SLAPI_BOILERPLATE).saveGameState(fos, cause, location, false);
} catch (IOException e) {
LOGGER.error("IO Error while saving the state of the game", e);
} catch (Throwable e) {
if (e instanceof OutOfMemoryError) {
System.gc();
}
LOGGER.error("Error while saving the state of the game", e);
if (e instanceof ThreadDeath) {
throw e;
}
} finally {
Settings.b("StartedLoading", false);
Space.getMainTickLoopLock().release(2);
}
// No idea what this does.
// Apparently there is no implementation of "interface_10" so we cannot really know.
// Upcoming feature perhaps?
// Apparently this has been there for quite a while too
for (interface_10 var1 : Space.w) {
var1.f();
}
}
@Override
public boolean accept(@NotNull ClassNode source) {
if (source.name.equals(SpaceASMTransformer.SPACE_CLASS)) {
String generateGalaxyMethodName = SpaceASMTransformer.generateGalaxyMethod.split("[\\.\\(]", 3)[1];
String tickMethodName = SpaceASMTransformer.tickMethod.split("[\\.\\(]", 3)[1];
String tickMethodDesc = '(' + SpaceASMTransformer.tickMethod.split("[\\.\\(]", 3)[2];
String saveSyncMethodName = SpaceASMTransformer.saveSyncMethod.split("[\\.\\(]", 3)[1];
String simLoopLockFieldName = SpaceASMTransformer.simLoopLockField.split("[ \\.]", 3)[1];
boolean foundTickMethod = false;
boolean foundEmpireCollapseMethod = false;
boolean foundSaveSyncMethod = false;
boolean foundSaveGalaxyMethodName = false;
boolean foundLoopLockFieldInit = false;
for (MethodNode method : source.methods) {
if (method.name.equals(SpaceASMTransformer.ON_EMPIRE_COLLAPSE_METHOD_NAME)
&& method.desc.equals("(Lsnoddasmannen/galimulator/Empire;)V")) {
this.addEmpireCollapseListener(method);
foundEmpireCollapseMethod = true;
} else if (method.name.equals(tickMethodName) && method.desc.equals(tickMethodDesc)) {
this.addLogicalListener(method);
foundTickMethod = true;
} else if (method.name.equals(saveSyncMethodName) && method.desc.equals("(Ljava/lang/String;Ljava/lang/String;)V")) {
method.instructions.clear();
method.instructions.add(new VarInsnNode(Opcodes.ALOAD, 0));
method.instructions.add(new VarInsnNode(Opcodes.ALOAD, 1));
method.instructions.add(new MethodInsnNode(Opcodes.INVOKESTATIC, TRANSFORMER_CLASS, "save", "(Ljava/lang/String;Ljava/lang/String;)V"));
method.instructions.add(new InsnNode(Opcodes.RETURN));
method.tryCatchBlocks.clear();
foundSaveSyncMethod = true;
} else if (method.name.equals(generateGalaxyMethodName) && method.desc.equals("(ILsnoddasmannen/galimulator/MapData;)V")) {
AbstractInsnNode returnInsn = null;
for (AbstractInsnNode insn : method.instructions) {
if (insn.getOpcode() == Opcodes.RETURN) {
if (returnInsn != null) {
throw new IllegalStateException("Bytecode is no longer laid out as expected");
}
returnInsn = insn;
}
}
if (returnInsn == null) {
throw new IllegalStateException("There is no return opcode in this method. Is this even valid java?");
}
MethodInsnNode insn = new MethodInsnNode(Opcodes.INVOKESTATIC, SpaceASMTransformer.TRANSFORMER_CLASS, "generateGalaxy", "(Z)V");
method.instructions.insertBefore(returnInsn, new InsnNode(Opcodes.ICONST_1)); // load true into the stack
method.instructions.insertBefore(returnInsn, insn);
foundSaveGalaxyMethodName = true;
} else if (method.name.equals("