Skip to content

Commit

Permalink
Almost ready for pre-release
Browse files Browse the repository at this point in the history
  • Loading branch information
slxdy committed Apr 2, 2024
1 parent 97ab743 commit 0304ec8
Show file tree
Hide file tree
Showing 47 changed files with 1,102 additions and 696 deletions.
146 changes: 146 additions & 0 deletions Guides/DeveloperGuide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
# TweaksLauncher Developer Guide

## Getting started

### In order to build your own mod, you will need:
- Basic C# knowledge
- Some experience with Unity
- A coffee

## Creating your first mod project
### There are 2 ways to create a new mod project:
- Using our dev tools (recommended)
- By creating a new C# Class Library project and referencing all dependencies manually

### Create a mod project using our dev tools
We recommend using this option because it will allow you to enable auto-build (which will automatically build and load new versions of your code). Next to that, all required libraries (including game libraries) will be automatically referenced.<br>
This will automatically put your project in the `Dev` folder, next to the launcher.<br>
For more advanced developers, we recommend this option when creating git repositories, as relative paths to referenced assemblies will be kept for everyone who clones your repository to the `Dev` directory, removing the need to ship your repository will all the game assemblies.

- Run the `CreateMod` batch script (located next to the launcher).
- Follow the instructions displayed on the console.
- You're ready to code!

### Create a mod project manually
If you do not want to store your project in the `Dev` directory, this is the way to go. Keep in mind that there is no auto-build for external projects.

- Create a new Class Library project (targetting .NET 8.0).
- Specify the mod version by giving the assembly a version in your csproj (using the `AssemblyVersion` tag).
- Manually reference `TweaksLauncher.API.dll` and `0harmony.dll` from the `libs` folder. You can find game proxy assemblies in `Games/Your Game Name/Proxies`. Make sure you've ran the game at least once using the launcher.
- Create a new mod class by adding the `TweaksLauncher.IMod` interface to an existing class.
- You're ready to code!

## Coding Your Mod
### Basic Structure
Your main mod class should now look somewhat like this:
```cs
using TweaksLauncher;

namespace MyMod;

public class Main : IMod
{
public static void Initialize(ModInstance mod)
{
ModLogger.Log("Hello World!");
}
}
```

For now, this will only print `Hello World!` to the console.<br>
- The `Initialize` method is ran when Unity has initialized, which is right before the Unity splash screen is shown.

Sometimes we want to initialize our mod right when when the first game scene is loaded. There are 2 ways to receive a callback when that happens:
- Using the `UnityEvents::FirstSceneLoad` event
- Through our own `MonoBehaviour` (a.k.a. Unity component)

## Events
The `UnityEvents` class provides common Unity events that can be used by mods. Currently, there is only one event, but more might get added in the future.

### FirstSceneLoad
Occurs when the first game scene is loaded.

The following code will print `Hello Game!` to the console once the first game scene is loaded:
```cs
public static void Initialize(ModInstance mod)
{
UnityEvents.FirstSceneLoad += OnFirstSceneLoad;
}

private static void OnFirstSceneLoad()
{
ModLogger.Log("Hello Game!");
}
```

## Creating Unity components
Creating Unity components in a mod is as simple as in Unity itself.

To create a new Unity component, derive your class from `MonoBehaviour`. Now you can create instances of your component using Unity's API (for example `new GameObject().AddComponent<MyComponent>()`).

TweaksLauncher provides a `UnitySingleton` attribute, which automatically loads your component once the game starts. The component will stay alive forever, unless manually destroyed.

The following code will print `Hello World!` to the console once the component is loaded:
```cs
using TweaksLauncher;
using UnityEngine;

namespace MyMod;

[UnitySingleton] // This will load the component automatically
public class MyComponent : MonoBehaviour
{
public void Start()
{
ModLogger.Log("Hello World!");
}
}
```

## Patching Methods with Harmony
Method patching is essencial for mods. It allows you to control the way the game behaves by replacing or adding code to existing game methods.

Harmony is a library that allows just that. Harmony patches can be created through simple attributes. Learn more here: https://harmony.pardeike.net/articles/annotations.html.
- The launcher will automatically apply all the `HarmonyPatch` attributes in your mod. Do NOT apply patches manually using the `PatchAll` method.

Here is an example of a Harmony patch:
```cs
using HarmonyLib;

// In this attribute you need to specify the class of the target method, the target method name, and the target method parameters.
[HarmonyPatch(typeof(ClassOfTheMethodToPatch), "TargetMethodName", [ typeof(int) ])]
private static class Patch
{
private static void Prefix()
{
// The code inside this method will run before 'TargetMethodName' is executed
}

private static void Postfix()
{
// The code inside this method will run after 'TargetMethodName' has executed
}
}
```

You can also modify the arguments and return values of the method call. Learn more here: https://harmony.pardeike.net/articles/patching-injections.html

## Debugging
### This guide primarily focuses on Visual Studio 2022 and up.

You can easily debug your mod through your IDE. This will allow you to start the game through the IDE, set breakpoints and inspect unhandled exceptions.

- First, you will need to add the launcher's executable as an existing project to your solution.<br>
![Add the launcher's executable as an existing project](img/debug1.png)

- Your solution should now look somewhat like this:<br>
![Solution explorer](img/debug2.png)

- Right click on the `TweaksLauncher` project and `Set as Startup Project`.

- Right click on the project again and go to `Properties`

- Set the `Arguments` property to the path of your game. Make sure to put it between double quotes. Example:<br>
![Project Properties](image.png)

- You're all set! You can now start debugging your mod by simply clicking the `Start` button at the top of your IDE.
Binary file added Guides/image.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added Guides/img/debug1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added Guides/img/debug2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
31 changes: 25 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,32 @@
# W.I.P.
This project is still in development.

# TweaksLauncher
A lightweight Unity Il2Cpp mod launcher.
A lightweight Unity mod launcher for IL2CPP games built for windows x64 and x86.

## How it works
TweaksLauncher is a game launcher, meaning it does NOT inject itself into the game, but rather starts it manually through `UnityPlayer.dll`.
TweaksLauncher is a game launcher, meaning it does NOT inject itself into the game, but rather starts it manually through its own executable.
Mods can interact with the game through the generated proxy assemblies from [Il2CppInterop](https://github.com/BepInEx/Il2CppInterop), just like with any other known Il2Cpp mod loaders.

## Requirements
- [.NET 8.0 Runtime](https://dotnet.microsoft.com/en-us/download/dotnet/thank-you/runtime-8.0.3-windows-x64-installer)

## How to use
**If you've already installed the launcher before, skip steps 1 and 2.**
1. Grab the latest zip from the [Releases Page](https://github.com/slxdy/TweaksLauncher/releases).
2. Unzip it to an empty folder (NOT IN A GAME FOLDER).
3. Start the launcher, and follow the instruction displayed in the console. Do not use the x86 version unless you know what you're doing. This will allow you to start the game through the launcher.

## How to install mods
Depending on the mod, the installation instructions may vary. However, most zipped mods should be extracted to the launcher's folder. **Make sure to extract the entire zip, including all folders within it. Do NOT create any extra folders if prompted.** If the mod is a single DLL, follow instructions provided by the developer.

## Developing Mods
TweaksLauncher provides useful features for developers, such as auto-build and debugging (through your preferred IDE).<br>
To get started, refer to the [Developer Guide](Guides/DeveloperGuide.md).

## Compilation Guide
The project should be compiled using Visual Studio. Make sure to build the entire solution. The builds are saved to the `output` directory.

## Credits
[Il2CppInterop](https://github.com/BepInEx/Il2CppInterop) - A tool interoperate between CoreCLR and Il2Cpp at runtime<br>
[BepInEx/Dobby](https://github.com/BepInEx/Dobby) - Fork of jmpews/Dobby with stability edits for Windows<br>
[Cpp2IL](https://github.com/SamboyCoding/Cpp2IL) - Work-in-progress tool to reverse unity's IL2CPP toolchain.<br>
[BepInEx/Dobby](https://github.com/BepInEx/Dobby) - Fork of jmpews/Dobby with stability edits for Windows<br>
[HarmonyX](https://github.com/BepInEx/HarmonyX) - Harmony built on top of MonoMod.RuntimeDetours with additional features<br>
[Pastel](https://github.com/silkfire/Pastel) - A tiny utility class that makes colorizing console output a breeze.
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
namespace TweaksLauncher;
using System.Drawing;

namespace TweaksLauncher;

internal static class CrashHandler
{
private static ModuleLogger logger = new("Crash Handler");
private static readonly ModuleLogger logger = new("Crash Handler");

private static bool inited;

Expand All @@ -18,7 +20,7 @@ public static void Init()

private static void OnUnhandledException(object sender, UnhandledExceptionEventArgs e)
{
logger.Log(e.ExceptionObject, "red");
logger.Log(e.ExceptionObject, Color.Red);

if (e.IsTerminating)
Console.ReadKey();
Expand Down
29 changes: 15 additions & 14 deletions TweaksLauncher/DevTools.cs → TweaksLauncher.API/DevTools.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Diagnostics;
using System.Drawing;
using System.Text;
using System.Text.Json;
using TweaksLauncher.Modding;
Expand All @@ -10,8 +11,8 @@ internal static class DevTools
{
private const string projectConfigFileName = "TweaksLauncher.ModConfig.json";

private static ModuleLogger logger = new("Dev Tools");
private static string devDir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Dev");
private static readonly ModuleLogger logger = new("Dev Tools");
private static readonly string devDir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Dev");

public static List<ModProject> GetProjectsForGame(string gameName)
{
Expand All @@ -34,11 +35,11 @@ public static List<ModProject> GetProjectsForGame(string gameName)

public static void BuildProjectsForCurrentGame()
{
var mods = GetProjectsForGame(Program.Context.GameName);
var mods = GetProjectsForGame(Launcher.Context.GameName);

foreach (var mod in mods)
{
var outputDir = Path.Combine(string.IsNullOrEmpty(mod.Config.GameName) ? ModHandler.GlobalModsDirectory : Program.Context.ModsDirectory, mod.Name);
var outputDir = Path.Combine(string.IsNullOrEmpty(mod.Config.GameName) ? ModHandler.GlobalModsDirectory : Launcher.Context.ModsDirectory, mod.Name);
mod.TryBuild(outputDir);
}

Expand All @@ -55,15 +56,15 @@ public static bool CreateMod()

if (!isGlobal)
{
if (!Program.InitContext(path!))
if (!Launcher.InitContext(path!))
{
logger.Log("There is no valid Unity game located at the given path.", "red");
logger.Log("There is no valid Unity game located at the given path.", Color.Red);
return false;
}

if (!ProxyGenerator.Generate())
{
logger.Log("Could not create a mod project because proxy generation failed.", "red");
logger.Log("Could not create a mod project because proxy generation failed.", Color.Red);
return false;
}
}
Expand All @@ -79,7 +80,7 @@ public static bool CreateMod()
var invalidChars = Path.GetInvalidFileNameChars();
if (possibleName.Any(x => x == ' ' || invalidChars.Contains(x)))
{
logger.Log("The name contains illegal characters. Please try again.", "red");
logger.Log("The name contains illegal characters. Please try again.", Color.Red);
continue;
}

Expand All @@ -92,7 +93,7 @@ public static bool CreateMod()

if (Directory.Exists(solutionDir) && Directory.EnumerateFiles(solutionDir, "*.*", SearchOption.AllDirectories).Any())
{
logger.Log("A project with the same name already exists. Please remove it first before Retrying.", "red");
logger.Log("A project with the same name already exists. Please remove it first before Retrying.", Color.Red);
return false;
}

Expand All @@ -103,7 +104,7 @@ public static bool CreateMod()

if (!isGlobal)
{
foreach (var asm in Directory.EnumerateFiles(Program.Context.ProxiesDirectory, "*.dll"))
foreach (var asm in Directory.EnumerateFiles(Launcher.Context.ProxiesDirectory, "*.dll"))
{
var asmName = Path.GetFileNameWithoutExtension(asm);
if (asmName.StartsWith('_'))
Expand Down Expand Up @@ -138,15 +139,15 @@ public static bool CreateMod()

var config = new ModConfig()
{
GameName = isGlobal ? null : Program.Context.GameName,
GameName = isGlobal ? null : Launcher.Context.GameName,
DefaultBuildConfig = "Debug",
ModCsprojPath = Path.GetRelativePath(solutionDir, csprojPath)
};

var jsonOptions = new JsonSerializerOptions() { WriteIndented = true };
File.WriteAllText(configPath, JsonSerializer.Serialize(config, jsonOptions));

logger.Log($"Project '{name}' has been created.", "green");
logger.Log($"Project '{name}' has been created.", Color.Green);
logger.Log($"You can find it at '{solutionDir}'");

Process.Start("explorer", ["/select,", solutionPath]);
Expand Down Expand Up @@ -203,7 +204,7 @@ public bool TryBuild(string outputDir)

if (!Dotnet(false, "build", CsprojPath, "-v", "q", "-c", buildConfig, "-o", outputDir))
{
logger.Log($"Failed to build project at '{CsprojPath}'. Ignoring.", "yellow");
logger.Log($"Failed to build project at '{CsprojPath}'. Ignoring.", Color.Yellow);
return false;
}

Expand Down Expand Up @@ -238,7 +239,7 @@ public bool TryBuild(string outputDir)

if (config == null || config.ModCsprojPath == null)
{
logger.Log($"{projectConfigFileName} from project '{directory}' is invalid.", "yellow");
logger.Log($"{projectConfigFileName} from project '{directory}' is invalid.", Color.Yellow);
return null;
}

Expand Down
Loading

0 comments on commit 0304ec8

Please sign in to comment.