Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Revive headless mode #855

Draft
wants to merge 20 commits into
base: dev
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
964d303
Introduce FastMode for MiniInstaller
Wartori54 Nov 1, 2024
2f506ce
Remove PathEverestExe in favour of PathCelesteExe
Wartori54 Nov 1, 2024
f832a09
Split Program.cs into multiple files
Wartori54 Nov 2, 2024
7761543
Cache coreified dll in fastmode
Wartori54 Nov 2, 2024
551ab1f
Add linux build scripts
Wartori54 Nov 2, 2024
932b22e
Adjust linux build scripts according the given feedback
Wartori54 Nov 5, 2024
b8b2b83
Add MMR flag to stub out FNA's window for headless operations
psyGamer Nov 24, 2024
1b0c408
Update FNA3D Linux binary for headless support
psyGamer Nov 24, 2024
ee34ba3
Properly set Everest.Flags.IsHeadless
psyGamer Nov 24, 2024
864118b
Forward "headless" CLI flag from MiniInstaller to MMR
psyGamer Nov 24, 2024
0f048a9
Fix selecting headless FNA3D driver implicitly instead of explicitly
psyGamer Nov 24, 2024
a479285
Force "Headless" driver and disable splash in headless mode
psyGamer Nov 24, 2024
dca748b
Reduce Game.Tick() to only Update()
psyGamer Nov 25, 2024
66ff6a5
Stub-out FMOD functions and disable audio in headless mode
psyGamer Nov 27, 2024
66bbd6c
Disable graphics loading in headless mode
psyGamer Nov 27, 2024
eb43b8b
Only load width and height from textures in headless mode
psyGamer Nov 30, 2024
fbea436
Disable DiscordSDK and error log opening in headless mode
psyGamer Nov 30, 2024
573d5c4
Skip out-of-box-experience in headless mode
psyGamer Dec 15, 2024
0adc56c
Always skip game intro in headless mode
psyGamer Dec 15, 2024
a35b464
Prevent mod auto-updating in headless mode
psyGamer Dec 15, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions Celeste.Mod.mm/Celeste.Mod.mm.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -83,13 +83,14 @@
<ProjectReference Include="..\DiscordGameSDK\DiscordGameSDK.csproj" />
<ProjectReference Include="..\NETCoreifier\NETCoreifier.csproj" />
<ProjectReference Include="..\EverestSplash\EverestSplash.csproj" /> <!-- Forces correct build dependency order -->
<!-- Ideally EverestSplash would not be copied to the build directory, but that cannot be easily disabled -->
</ItemGroup>

<Target Name="CopyEverestSplashFiles" AfterTargets="Publish">
<ItemGroup> <!-- For some reason to use wildcards you have to use a property -->
<SplashBinaries Include="$(OutputPath)\publish\EverestSplash*;$(OutputPath)\publish\piton-runtime.yaml" />
<SplashBinaries Include="$(PublishDir)\EverestSplash*;$(PublishDir)\piton-runtime.yaml" />
</ItemGroup>
<Move SourceFiles="@(SplashBinaries)" DestinationFolder="$(OutputPath)\publish\EverestSplash\" />
<Move SourceFiles="@(SplashBinaries)" DestinationFolder="$(PublishDir)\EverestSplash\" />
</Target>

</Project>
2 changes: 1 addition & 1 deletion Celeste.Mod.mm/Mod/Core/CoreModule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ public override void LoadSettings() {
base.LoadSettings();

// The field can be set to true by default without the setter being called by YamlDotNet.
if (Settings.DiscordRichPresence) {
if (Settings.DiscordRichPresence && !Everest.Flags.IsHeadless) {
Everest.DiscordSDK.CreateInstance();
}

Expand Down
2 changes: 1 addition & 1 deletion Celeste.Mod.mm/Mod/Core/CoreModuleSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -586,7 +586,7 @@ public void CreateDiscordRichPresenceEntry(TextMenu menu, bool inGame) {
TextMenu.Item masterSwitch = new TextMenu.OnOff(Dialog.Clean("modoptions_coremodule_discordrichpresence"), DiscordRichPresence)
.Change(value => {
DiscordRichPresence = value;
if (DiscordRichPresence) {
if (DiscordRichPresence && !Everest.Flags.IsHeadless) {
Everest.DiscordSDK.CreateInstance()?.UpdatePresence(session);
} else {
Everest.DiscordSDK.Instance?.Dispose();
Expand Down
5 changes: 2 additions & 3 deletions Celeste.Mod.mm/Mod/Everest/Everest.Flags.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,9 @@ public static class Flags {
public static bool VanillaIsFNA { get; private set;}

/// <summary>
/// Is Everest running headlessly?
/// Is Everest running without a window?
/// </summary>
public static bool IsHeadless { get; private set; }
public static bool IsHeadless { get; internal set; }

/// <summary>
/// Is the game running using Mono - always false on .NET Core Everest.
Expand Down Expand Up @@ -75,7 +75,6 @@ internal static void Initialize() {
VanillaIsXNA = !VanillaIsFNA;
}

IsHeadless = Environment.GetEnvironmentVariable("EVEREST_HEADLESS") == "1";
AvoidRenderTargets = Environment.GetEnvironmentVariable("EVEREST_NO_RT") == "1";
PreferLazyLoading = false;

Expand Down
12 changes: 4 additions & 8 deletions Celeste.Mod.mm/Mod/Everest/Everest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -425,10 +425,8 @@ static void UnregisterModDetour(object detour) {
// Before even initializing anything else, make sure to prepare any static flags.
Flags.Initialize();

if (!Flags.IsHeadless) {
// Initialize the content helper.
Content.Initialize();
}
// Initialize the content helper.
Content.Initialize();

MainThreadHelper.Instance = new MainThreadHelper(Celeste.Instance);
STAThreadHelper.Instance = new STAThreadHelper(Celeste.Instance);
Expand Down Expand Up @@ -457,10 +455,8 @@ static void UnregisterModDetour(object detour) {

Loader.LoadAuto();

if (!Flags.IsHeadless) {
// Load stray .bins afterwards.
Content.Crawl(new MapBinsInModsModContent(Path.Combine(PathEverest, "Mods")));
}
// Load stray .bins afterwards.
Content.Crawl(new MapBinsInModsModContent(Path.Combine(PathEverest, "Mods")));

// Also let all mods parse the arguments.
Queue<string> args = new Queue<string>(Args);
Expand Down
2 changes: 1 addition & 1 deletion Celeste.Mod.mm/Mod/UI/OuiOOBE.cs
Original file line number Diff line number Diff line change
Expand Up @@ -327,7 +327,7 @@ private IEnumerator FadeBgTo(float to) {
}

public override bool IsStart(Overworld overworld, Overworld.StartMode start) {
if (start != Overworld.StartMode.Titlescreen)
if (start != Overworld.StartMode.Titlescreen || Everest.Flags.IsHeadless)
return false;
if (CoreModule.Settings.CurrentVersion != null) {
if (CoreModule.Settings.SaveDataFlush ?? false)
Expand Down
20 changes: 17 additions & 3 deletions Celeste.Mod.mm/MonoModRules.Game.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using Mono.Cecil;
using Mono.Cecil.Cil;
using Mono.Collections.Generic;
using Monocle;
using MonoMod.Cil;
using MonoMod.InlineRT;
using MonoMod.Utils;
Expand Down Expand Up @@ -154,20 +155,33 @@ public static void GamePreProcessor(MonoModder modder) {
if (IsRelinkingXNAInstall)
modder.Log("[Celeste.Mod.mm] Converting XNA game install to FNA");

static void VisitType(TypeDefinition type) {
MethodReference stubAttrCtor = RulesModule.GetType($"MonoMod.{nameof(PatchStubExternAttribute)}").Methods.First(m => m.IsConstructor);
stubAttrCtor = modder.Module.ImportReference(stubAttrCtor);

static void VisitType(TypeDefinition type, MethodReference stubAttrCtor) {
// Remove readonly attribute from all static fields
// This "fixes" https://github.com/dotnet/runtime/issues/11571, which breaks some mods
foreach (FieldDefinition field in type.Fields)
if ((field.Attributes & FieldAttributes.Static) != 0)
field.Attributes &= ~FieldAttributes.InitOnly;

// Stub out extern FMOD methods in headless mode
if (MonoModRule.Flag.Get("Headless") && type.Namespace.StartsWith("FMOD")) {
foreach (MethodDefinition method in type.Methods) {
if ((method.Attributes & MethodAttributes.Static) != 0 && method.Name.StartsWith("FMOD_")) {
// Console.WriteLine($"ADDING ATTR {method}");
method.CustomAttributes.Add(new CustomAttribute(stubAttrCtor));
}
}
}

// Visit nested types
foreach (TypeDefinition nestedType in type.NestedTypes)
VisitType(nestedType);
VisitType(nestedType, stubAttrCtor);
}

foreach (TypeDefinition type in modder.Module.Types)
VisitType(type);
VisitType(type, stubAttrCtor);
}

public static void GamePostProcessor(MonoModder modder) {
Expand Down
86 changes: 86 additions & 0 deletions Celeste.Mod.mm/MonoModRules.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,13 @@ public ForceNameAttribute(string name) {}
/// </summary>
[MonoModCustomMethodAttribute(nameof(MonoModRules.PatchInitblk))]
class PatchInitblkAttribute : Attribute { }

/// <summary>
/// Patches an extern method to be stubbed. Uses default values for return type / out-parameters
/// </summary>
[MonoModIfFlag("Headless")]
[MonoModCustomMethodAttribute(nameof(MonoModRules.PatchStubExtern))]
class PatchStubExternAttribute : Attribute { }
#endregion

static partial class MonoModRules {
Expand Down Expand Up @@ -102,6 +109,9 @@ private static void InitCommonRules(MonoModder modder) {
if (MonoModder.Version < monoModderAsmRef.Version)
throw new Exception($"Unexpected version of MonoMod patcher: {MonoModder.Version} (expected {monoModderAsmRef.Version}+)");

// Data is set by MiniInstaller
MonoModRule.Flag.Set("Headless", (bool?)AppDomain.CurrentDomain.GetData("Everest_IsHeadless") ?? false);

// Add common post processor
modder.PostProcessors += CommonPostProcessor;
}
Expand Down Expand Up @@ -220,6 +230,82 @@ public static void PatchInitblk(ILContext il, CustomAttribute attrib) {
}
}

public static void PatchStubExtern(MethodDefinition method, CustomAttribute attrib) {
// Console.WriteLine($"RUNNING STUB {method} {method.Attributes}");
method.Attributes &= ~(MethodAttributes.PInvokeImpl | MethodAttributes.CompilerControlled | MethodAttributes.HideBySig);

ILContext il = new ILContext(method);
ILCursor cursor = new ILCursor(il);

TypeDefinition returnType = method.ReturnType.Resolve();

// Insert 'outParam = default;'
foreach (ParameterDefinition param in method.Parameters) {
if (!param.IsOut) continue;

TypeDefinition paramType = param.ParameterType.Resolve();
TypeReference enumType = paramType.IsEnum ? GetEnumUnderlyingType(paramType) : null;

if (paramType.FullName is "System.Byte" or "System.SByte" || enumType?.FullName is "System.Byte" or "System.SByte") {
cursor.EmitLdarg(param.Index);
cursor.EmitLdcI4(0);
cursor.EmitStindI1();
} else if (paramType.FullName is "System.Int16" or "System.UInt16" || enumType?.FullName is "System.Int16" or "System.UInt16") {
cursor.EmitLdarg(param.Index);
cursor.EmitLdcI4(0);
cursor.EmitStindI2();
} else if (paramType.FullName is "System.Int32" or "System.UInt32" || enumType?.FullName is "System.Int32" or "System.UInt32") {
cursor.EmitLdarg(param.Index);
cursor.EmitLdcI4(0);
cursor.EmitStindI4();
} else if (paramType.FullName is "System.Int64" or "System.UInt64" || enumType?.FullName is "System.Int64" or "System.UInt64") {
cursor.EmitLdarg(param.Index);
cursor.EmitLdcI8(0);
cursor.EmitStindI8();
} else if (paramType.FullName is "System.IntPtr" or "System.UIntPtr" || enumType?.FullName is "System.IntPtr" or "System.UIntPtr") {
cursor.EmitLdarg(param.Index);
cursor.EmitLdcI4(0);
cursor.EmitConvI();
cursor.EmitStindI();
} else if (paramType.FullName is "System.Single" || enumType?.FullName is "System.Single") {
cursor.EmitLdarg(param.Index);
cursor.EmitLdcR4(0.0f);
cursor.EmitStindR4();
} else if (paramType.FullName is "System.Double" || enumType?.FullName is "System.Double") {
cursor.EmitLdarg(param.Index);
cursor.EmitLdcR8(0.0);
cursor.EmitStindR8();
} else if (paramType.IsValueType) {
cursor.EmitLdarg(param.Index);
cursor.EmitInitobj(MonoModRule.Modder.Module.ImportReference(paramType));
} else {
cursor.EmitLdarg(param.Index);
cursor.EmitLdnull();
cursor.EmitStindRef();
}
}

// Insert 'return default;'
if (returnType.FullName == "System.Void") {
cursor.EmitRet();
} else if (returnType.IsPrimitive || returnType.IsEnum) {
cursor.EmitLdcI4(0);
cursor.EmitRet();
} else if (!returnType.IsValueType) {
cursor.EmitLdnull();
cursor.EmitRet();
} else {
throw new Exception($"Unsupported return type '{returnType.FullName}' of method '{method.FullName}' to be stubbed");
}

return;

// Adapted from Mono.Cecil.Rocks.TypeDefinitionsRock.GetEnumUnderlyingType
static TypeReference GetEnumUnderlyingType(TypeDefinition enumType) {
return enumType.Fields.First(f => !f.IsStatic).FieldType;
}
}

/// <summary>
/// Fix ILSpy unable to decompile enumerator after MonoMod patching<br />
/// (<code>yield-return decompiler failed: Unexpected instruction in Iterator.Dispose()</code>)
Expand Down
40 changes: 35 additions & 5 deletions Celeste.Mod.mm/Patches/Audio.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,15 @@ internal static void CheckFmod(RESULT result) {

[MonoModReplace]
public static void Init() {
if (Everest.Flags.IsHeadless) {
// Stub out audio system
system = new FMOD.Studio.System(IntPtr.Zero);
return;
}

bool fmodLiveUpdate = Settings.Instance.LaunchWithFMODLiveUpdate;
Settings.Instance.LaunchWithFMODLiveUpdate |= CoreModule.Settings.LaunchWithFMODLiveUpdateInEverest;

// Original initialization code
{
FMOD.Studio.INITFLAGS flags = FMOD.Studio.INITFLAGS.NORMAL;
Expand Down Expand Up @@ -111,6 +117,10 @@ public static void Init() {
}

public static void IngestNewBanks() {
if (Everest.Flags.IsHeadless) {
return;
}

lock (Everest.Content.Map) {
foreach (ModAsset asset in Everest.Content.Map.Values.Where(asset => asset.Type == typeof(AssetTypeBank))) {
if (!ingestedModBankPaths.Contains(asset.PathVirtual)) {
Expand All @@ -122,6 +132,10 @@ public static void IngestNewBanks() {

[MonoModReplace]
public static void Unload() {
if (Everest.Flags.IsHeadless) {
return;
}

if (system == null)
return;

Expand All @@ -137,6 +151,10 @@ public static void Unload() {
/// Loads an FMOD Bank from the given asset.
/// </summary>
public static Bank IngestBank(ModAsset asset) {
if (Everest.Flags.IsHeadless) {
return new Bank(IntPtr.Zero);
}

Logger.Verbose("Audio.IngestBank", asset.PathVirtual);
ingestedModBankPaths.Add(asset.PathVirtual);

Expand Down Expand Up @@ -188,6 +206,10 @@ public static Bank IngestBank(ModAsset asset) {
/// Loads an FMOD GUID table from the given asset.
/// </summary>
public static void IngestGUIDs(ModAsset asset) {
if (Everest.Flags.IsHeadless) {
return;
}

Logger.Verbose("Audio.IngestGUIDs", asset.PathVirtual);
using (Stream stream = asset.Stream)
using (StreamReader reader = new StreamReader(asset.Stream)) {
Expand Down Expand Up @@ -222,15 +244,15 @@ public static void IngestGUIDs(ModAsset asset) {

public static extern void orig_ReleaseUnusedDescriptions();
public static void ReleaseUnusedDescriptions() {
if (!CoreModule.Settings.UnloadUnusedAudio)
if (Everest.Flags.IsHeadless || !CoreModule.Settings.UnloadUnusedAudio)
return;
orig_ReleaseUnusedDescriptions();
}


[MonoModReplace]
public static string GetEventName(EventInstance instance) {
if (instance == null)
if (Everest.Flags.IsHeadless || instance == null)
return "";

instance.getDescription(out EventDescription desc);
Expand All @@ -241,7 +263,7 @@ public static string GetEventName(EventInstance instance) {
}

public static string GetEventName(EventDescription desc) {
if (desc == null)
if (Everest.Flags.IsHeadless || desc == null)
return "";

desc.getID(out Guid id);
Expand All @@ -256,7 +278,7 @@ public static string GetEventName(EventDescription desc) {
}

public static string GetBankName(Bank bank) {
if (bank == null)
if (Everest.Flags.IsHeadless || bank == null)
return "";

bank.getID(out Guid id);
Expand All @@ -270,6 +292,10 @@ public static string GetBankName(Bank bank) {

[MonoModReplace]
public static EventDescription GetEventDescription(string path) {
if (Everest.Flags.IsHeadless) {
return new EventDescription(IntPtr.Zero);
}

EventDescription desc = null;
if (path == null || Audio.cachedEventDescriptions.TryGetValue(path, out desc))
return desc;
Expand Down Expand Up @@ -311,6 +337,10 @@ public static class patch_Banks {

[MonoModReplace]
public static Bank Load(string name, bool loadStrings) {
if (Everest.Flags.IsHeadless) {
return new Bank(IntPtr.Zero);
}

if (Banks == null)
Banks = new Dictionary<string, Bank>();

Expand Down
Loading