Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 2 additions & 2 deletions Ignitron.Loader/Ignitron.Loader.csproj
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AssemblyName>Loader</AssemblyName>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Lib.Harmony" Version="2.4.1" />
<PackageReference Include="Lib.Harmony" Version="2.4.2" />
</ItemGroup>

<Import Project="$(MSBuildThisFileDirectory)..\build\CommonAssemblies.props"/>
Expand Down
28 changes: 20 additions & 8 deletions Ignitron.Loader/IgnitronLoader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public sealed partial class IgnitronLoader : IExternalLoader
/// <summary>
/// Installed version of the mod loader
/// </summary>
public static Version Version { get; } = new(0, 4, 0);
public static Version Version { get; } = new(0, 5, 0);

/// <summary>
/// Current instance of the mod loader
Expand All @@ -31,6 +31,9 @@ public sealed partial class IgnitronLoader : IExternalLoader
[GeneratedRegex(@"(\d+)\.(\d+)(?:\.(\d+))?(?:\.(\d+))?")]
private static partial Regex VersionRegex();

[GeneratedRegex(@"MP TEST v(\d+)")]
private static partial Regex MpTestVersionRegex();

/// <summary>
/// Version of the game installation
/// </summary>
Expand Down Expand Up @@ -95,13 +98,22 @@ void IExternalLoader.Init()
Instance = this;

// resolve game version
Match versionMatch = VersionRegex().Match(Game.VERSION);
GameVersion = new Version(
int.Parse(versionMatch.Groups[1].ValueSpan),
int.Parse(versionMatch.Groups[2].ValueSpan),
versionMatch.Groups.Count > 5 ? int.Parse(versionMatch.Groups[3].ValueSpan) : 0,
versionMatch.Groups.Count > 6 ? int.Parse(versionMatch.Groups[4].ValueSpan) : 0
);
if (Game.VERSION.StartsWith("MP TEST v"))
{
// edge case for obscure version format
Match versionMatch = MpTestVersionRegex().Match(Game.VERSION);
GameVersion = new Version(0, 12, 0, int.Parse(versionMatch.Groups[1].ValueSpan));
}
else
{
Match versionMatch = VersionRegex().Match(Game.VERSION);
GameVersion = new Version(
int.Parse(versionMatch.Groups[1].ValueSpan),
int.Parse(versionMatch.Groups[2].ValueSpan),
versionMatch.Groups.Count > 5 ? int.Parse(versionMatch.Groups[3].ValueSpan) : 0,
versionMatch.Groups.Count > 6 ? int.Parse(versionMatch.Groups[4].ValueSpan) : 0
);
}

// leave loader watermark
Game.VERSION = $"{Game.VERSION}/ignitron {Version}";
Expand Down
3 changes: 3 additions & 0 deletions Ignitron.Loader/Networking/ClientModDescription.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
namespace Ignitron.Loader.Networking;

public readonly record struct ClientModDescription(string Id, Version Version);
44 changes: 44 additions & 0 deletions Ignitron.Loader/Networking/NetworkingHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
namespace Ignitron.Loader.Networking;

internal static class NetworkingHelper
{
public static void DecodeMods(BinaryReader reader, ref List<ClientModDescription>? destination)
{
if (destination == null)
{
destination = [];
}
else
{
destination.Clear();
}

int count = reader.ReadInt32();
for (int i = 0; i < count ; i++)
{
string id = reader.ReadString();
int major = reader.ReadInt32(),
minor = reader.ReadInt32(),
build = reader.ReadInt32(),
revision = reader.ReadInt32();
destination.Add(new ClientModDescription(
id,
build >= 0 && revision >= 0 ? new Version(major, minor, build, revision)
: build >= 0 ? new Version(major, minor, build)
: new Version(major, minor)));
}
}

public static void EncodeMods(BinaryWriter writer, List<ClientModDescription> source)
{
writer.Write(source.Count);
foreach (ClientModDescription mod in source)
{
writer.Write(mod.Id);
writer.Write(mod.Version.Major);
writer.Write(mod.Version.Minor);
writer.Write(mod.Version.Build);
writer.Write(mod.Version.Revision);
}
}
}
65 changes: 65 additions & 0 deletions Ignitron.Loader/Networking/PacketModdedRequest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
using System.Text;
using Allumeria.Networking;
using Allumeria.Networking.Packets;

namespace Ignitron.Loader.Networking;

public struct PacketModdedRequest(List<ClientModDescription> mods) : IPacket
{
public PlayerConnection sender { get; set; }

private List<ClientModDescription>? _mods = mods;

public void Decode(BinaryReader reader)
{
NetworkingHelper.DecodeMods(reader, ref _mods);
}

public void Encode(BinaryWriter writer)
{
if (_mods == null)
{
throw new InvalidOperationException();
}

NetworkingHelper.EncodeMods(writer, _mods);
}

public void PerformAction()
{
if (!NetworkManager.IsServer())
{
return;
}

if (_mods == null)
{
throw new InvalidOperationException();
}

// verify that all mods are present
List<ClientModDescription>? missingMods = null;

foreach (ModBox mod in IgnitronLoader.Instance.Mods)
{
bool missed = true;

foreach (ClientModDescription other in _mods)
{
if (other.Id == mod.Metadata.Id && other.Version == mod.Metadata.Version)
{
missed = false;
break;
}
}

if (missed)
{
missingMods ??= [];
missingMods.Add(new ClientModDescription(mod.Metadata.Id, mod.Metadata.Version));
}
}

NetworkManager.server.SendPacketTo(new PacketModdedResponse(missingMods == null, missingMods), sender);
}
}
61 changes: 61 additions & 0 deletions Ignitron.Loader/Networking/PacketModdedResponse.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
using System.Text;
using Allumeria;
using Allumeria.Networking;
using Allumeria.Networking.Packets;
using Allumeria.UI.Menus;

namespace Ignitron.Loader.Networking;

public struct PacketModdedResponse(bool success, List<ClientModDescription>? missingMods) : IPacket
{
public PlayerConnection sender { get; set; }

private bool _success = success;
private List<ClientModDescription>? _missingMods = missingMods;

public void Decode(BinaryReader reader)
{
_success = reader.ReadBoolean();
if (!_success)
{
NetworkingHelper.DecodeMods(reader, ref _missingMods);
}
}

public void Encode(BinaryWriter writer)
{
writer.Write(_success);
if (!_success)
{
NetworkingHelper.EncodeMods(writer, _missingMods);
}
}

public void PerformAction()
{
if (!NetworkManager.IsClient())
{
return;
}

if (_success)
{
NetworkManager.client.SendPacketToServer(new PacketHandshake());
}
else
{
StringBuilder sb = new();
sb.Append("Ignitron handshake failure.\nYou're missing the following mods:");
foreach (ClientModDescription mod in _missingMods)
{
sb.AppendLine($" - {mod.Id} ({mod.Version})");
}

PauseMenu.LeaveGame();
NetworkManager.client.Stop();
Game.menu_mainMenu.show = false;
Game.menu_kicked.show = true;
Game.menu_kicked.card_reason.displayText = sb.ToString();
}
}
}
50 changes: 50 additions & 0 deletions Ignitron.Loader/Patches/ClientPatches.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
using System.Reflection;
using System.Reflection.Emit;
using Allumeria.Networking;
using Allumeria.Networking.Packets;
using HarmonyLib;
using Ignitron.Loader.Networking;

namespace Ignitron.Loader.Patches;

[HarmonyPatch]
internal static class ClientPatches
{
private static readonly FieldInfo NetworkManagerClient = AccessTools.DeclaredField(typeof(NetworkManager), nameof(NetworkManager.client));
private static readonly MethodInfo ClientSendPacketToServer = AccessTools.DeclaredMethod(typeof(Client), nameof(Client.SendPacketToServer));

private static readonly ConstructorInfo PacketHandshakeCtor = AccessTools.DeclaredConstructor(typeof(PacketHandshake));

[HarmonyPatch(typeof(Client), nameof(Client.Start))]
private static class StartPatch
{
private static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> instructions)
{
return new CodeMatcher(instructions)
.MatchStartForward(
new CodeMatch(OpCodes.Ldsfld, NetworkManagerClient),
new CodeMatch(OpCodes.Newobj, PacketHandshakeCtor),
new CodeMatch(OpCodes.Box, typeof(PacketHandshake)),
new CodeMatch(OpCodes.Callvirt, ClientSendPacketToServer))
.ThrowIfInvalid("couldn't find NetworkManager.client.SendPacketToServer(new PacketHandshake())")
.Advance()
.RemoveInstructions(2)
.Insert(
CodeInstruction.Call(() => CreateRequest()),
new CodeInstruction(OpCodes.Box, typeof(PacketModdedRequest)))
.Instructions();
}

private static PacketModdedRequest CreateRequest()
{
List<ClientModDescription> mods = [];

foreach (ModBox mod in IgnitronLoader.Instance.Mods)
{
mods.Add(new ClientModDescription(mod.Metadata.Id, mod.Metadata.Version));
}

return new PacketModdedRequest(mods);
}
}
}
19 changes: 19 additions & 0 deletions Ignitron.Loader/Patches/NetworkManagerPatches.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using Allumeria.Networking;
using HarmonyLib;
using Ignitron.Loader.Networking;

namespace Ignitron.Loader.Patches;

[HarmonyPatch]
internal static class NetworkManagerPatches
{
[HarmonyPatch(typeof(NetworkManager), nameof(NetworkManager.RegisterPackets))]
private static class RegisterPacketsPatch
{
private static void Postfix()
{
IPacket.RegisterPacket(new PacketModdedRequest());
IPacket.RegisterPacket(new PacketModdedResponse());
}
}
}
2 changes: 1 addition & 1 deletion Ignitron.TestMod/Ignitron.TestMod.csproj
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
Expand Down
2 changes: 1 addition & 1 deletion Ignitron.TestMod/Metadata.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
"dependencies": [
{
"id": "allumeria",
"version": "0.11"
"version": "0.12.0.*"
},
{
"id": "harmony",
Expand Down
18 changes: 7 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,24 +14,20 @@ now look for `Ignitron.Loader.zip` link, under version changelog, and click on i

make `mods` folder in the game directory and unpack `Ignitron.Loader.zip` into it.

done!

### for developers

> [!CAUTION]
> ***until 0.10***, you can ***NOT*** use Harmony patches or MonoMod in production environment.
> see https://github.com/MonoMod/MonoMod/issues/129 for more information.
>
> go [here](https://github.com/danilwhale/ignitron/blob/7a70196e36a65ebd7c7378ba54ad2c6dd738d1f3/README.md) to view
> steps for pre-0.10 setup
> since 0.5.0, you **CAN NOT** use modloader with versions before 0.12 or MP TEST due to upgrade to .NET 10.0 from .NET 9.0

> [!IMPORTANT]
> `MP TEST vX` versions are parsed as `0.12.0.X`

#### workspace configuration

> [!IMPORTANT]
> you need to have [.NET SDK 9.0](https://dotnet.microsoft.com/en-us/download/dotnet/9.0) installed!
> you need to have [.NET SDK **10.0**](https://dotnet.microsoft.com/en-us/download/dotnet/10.0) installed!

1. get and download [allumeria](https://store.steampowered.com/app/3516590/Allumeria/) (modloader is targetted for 0.11,
which is latest as of 06-11-2025)
1. get and download [allumeria](https://store.steampowered.com/app/3516590/Allumeria/) (modloader is targetted for MP TEST, which is in closed beta as of 2025-11-28)
2. set environment variable `ALLUMERIA_GAME_DIR` to game installation directory

> [!TIP]
Expand All @@ -49,4 +45,4 @@ dotnet build Ignitron.Loader/Ignitron.Loader.csproj

#### developing a mod

use [the mod template](https://github.com/danilwhale/ignitron-mod-template) to make a new mod!
use [the mod template](https://github.com/danilwhale/ignitron-mod-template) to make a new mod!
5 changes: 5 additions & 0 deletions build/CommonAssemblies.props
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@

<!-- soloud -->
<Reference Include="$([System.IO.Path]::Combine('$(GameDirectory)', 'SoLoudLib.dll'))" Private="false"/>
<Reference Include="$([System.IO.Path]::Combine('$(GameDirectory)', 'NVorbis.dll'))" Private="false"/>

<!-- steamworks -->
<!-- TODO: use .Posix for non-Windows when needed -->
<Reference Include="$([System.IO.Path]::Combine('$(GameDirectory)', 'Facepunch.Steamworks.Win64.dll'))" Private="false"/>

<!-- sql -->
<Reference Include="$([System.IO.Path]::Combine('$(GameDirectory)', 'SQLitePCLRaw.batteries_v2.dll'))" Private="false"/>
Expand Down