diff --git a/AGENTS.md b/AGENTS.md index 8cefb69..b6bc1ef 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -3,12 +3,14 @@ ## Repo - Modding workflow and references live in `docs/pc-modding.md` (Linux + Cursor + `dotnet` CLI; no VS/Rider BSMT extension). +- `docs/pc-modding.md` §References (wiki paths, BSMT templates, BSIPA, SongCore, etc.) match local git checkouts under `~/src/…` on this machine (same layout as the guide’s `../../../src/…` links from here). Read those directories first; only fetch upstream (raw GitHub, bsmg.wiki) if a checkout is missing. +- BSMG wiki (Modding section): Available on disk at `~/src/bsmg/wiki` (also opened via `bs-modding-tools.code-workspace`). Prefer that tree over web mirrors for static wiki content. ## Game install (BSManager) -- **Path:** `/home/pleb/.local/share/BSManager/BSInstances/1.40.8` -- **Version pin:** `1.40.8` (managed copy; launch modded build from BSManager, not Steam’s live folder). -- **BSIPA:** Present (`IPA/`, `IPA.exe`, `winhttp.dll`, `Plugins/`). +- Path: `/home/pleb/.local/share/BSManager/BSInstances/1.40.8` +- Version pin: `1.40.8` (managed copy; launch modded build from BSManager, not Steam’s live folder). +- BSIPA: Present (`IPA/`, `IPA.exe`, `winhttp.dll`, `Plugins/`). ## Plugins currently in `Plugins/` @@ -16,11 +18,11 @@ BeatSaverDownloader, BeatSaverUpdater, BSML, BS_Utils, PlaylistManager, SiraUtil ## Host toolchain -- **dotnet:** `9.0.312` (SDK 6+ is fine for `net472` plugin builds per guide). -- **ilspycmd:** `9.1.0.0` (decompile/reference game or plugin assemblies from CLI). -- **NuGet:** Installed (per user setup). +- dotnet: `9.0.312` (SDK 6+ is fine for `net472` plugin builds per guide). +- ilspycmd: `9.1.0.0` (decompile/reference game or plugin assemblies from CLI). +- NuGet: Installed (per user setup). ## Conventions agents should respect -- Plugin projects are **.NET Framework 4.7.2** class libraries loaded by BSIPA; builds are **CIL** — Linux `dotnet build` output is valid for the Proton game instance. +- Plugin projects are .NET Framework 4.7.2 class libraries loaded by BSIPA; builds are CIL — Linux `dotnet build` output is valid for the Proton game instance. - Point `BeatSaberDir` / game references at the BSManager instance path above when editing project user files or HintPaths. diff --git a/Setlist/Directory.Build.props b/Setlist/Directory.Build.props new file mode 100644 index 0000000..e53d942 --- /dev/null +++ b/Setlist/Directory.Build.props @@ -0,0 +1,8 @@ + + + + + True + BSIPA + + diff --git a/Setlist/Plugin.cs b/Setlist/Plugin.cs new file mode 100644 index 0000000..2879303 --- /dev/null +++ b/Setlist/Plugin.cs @@ -0,0 +1,34 @@ +using IPA; +using IPALogger = IPA.Logging.Logger; + +namespace Setlist +{ + [Plugin(RuntimeOptions.SingleStartInit)] + public class Plugin + { + internal static Plugin Instance { get; private set; } + + /// + /// BSIPA logger (shows in BSIPA console / game logs when verbose). + /// + internal static IPALogger Log { get; private set; } + + [Init] + public Plugin(IPALogger logger) + { + Instance = this; + Log = logger; + } + + [OnStart] + public void OnApplicationStart() + { + Log.Info("Hello World"); + } + + [OnExit] + public void OnApplicationQuit() + { + } + } +} diff --git a/Setlist/Properties/AssemblyInfo.cs b/Setlist/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..e60cfb0 --- /dev/null +++ b/Setlist/Properties/AssemblyInfo.cs @@ -0,0 +1,15 @@ +using System.Reflection; +using System.Runtime.InteropServices; + +[assembly: AssemblyTitle("Setlist")] +[assembly: AssemblyDescription("Playlist sync plugin for Beat Saber")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Setlist")] +[assembly: AssemblyCopyright("")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] +[assembly: ComVisible(false)] +[assembly: Guid("50F53E6E-21D5-4780-8E67-273877DAA28C")] +[assembly: AssemblyVersion("0.0.1.0")] +[assembly: AssemblyFileVersion("0.0.1.0")] diff --git a/Setlist/Setlist.csproj b/Setlist/Setlist.csproj new file mode 100644 index 0000000..d1bb5e8 --- /dev/null +++ b/Setlist/Setlist.csproj @@ -0,0 +1,123 @@ + + + + Debug + AnyCPU + 8.0.30703 + 2.0 + {18417954-9A66-445B-A3E1-F1E4C216E79D} + Library + Properties + Setlist + Setlist + v4.7.2 + 512 + true + portable + ..\Refs + $(LocalRefsDir) + $(MSBuildProjectDirectory)\ + prompt + 4 + + + false + bin\Debug\ + DEBUG;TRACE + + + true + bin\Release\ + prompt + 4 + + + True + + + True + True + + + + + + + + + + $(BeatSaberDir)\Beat Saber_Data\Managed\Main.dll + False + + + $(BeatSaberDir)\Beat Saber_Data\Managed\HMLib.dll + False + + + $(BeatSaberDir)\Beat Saber_Data\Managed\HMUI.dll + False + + + $(BeatSaberDir)\Beat Saber_Data\Managed\IPA.Loader.dll + False + + + $(BeatSaberDir)\Beat Saber_Data\Managed\Unity.TextMeshPro.dll + False + + + $(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.dll + False + + + $(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.CoreModule.dll + False + + + $(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.UI.dll + False + + + $(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.UIElementsModule.dll + False + + + $(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.UIModule.dll + False + + + $(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.VRModule.dll + False + + + + + + + + + + + + + + + + 2.0.0-beta7 + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + 1.0.3 + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + diff --git a/Setlist/manifest.json b/Setlist/manifest.json new file mode 100644 index 0000000..131ad9d --- /dev/null +++ b/Setlist/manifest.json @@ -0,0 +1,12 @@ +{ + "$schema": "https://raw.githubusercontent.com/bsmg/BSIPA-MetadataFileSchema/master/Schema.json", + "id": "Setlist", + "name": "Setlist", + "author": "", + "version": "0.0.1", + "description": "Syncs playlists with external sources.", + "gameVersion": "1.40.8", + "dependsOn": { + "BSIPA": "^4.3.0" + } +} diff --git a/docs/bootstrap.md b/docs/bootstrap.md new file mode 100644 index 0000000..1aedc58 --- /dev/null +++ b/docs/bootstrap.md @@ -0,0 +1,95 @@ +# Bootstrapping the Setlist BSIPA plugin + +This documents how the `Setlist/` project was created (aligned with `docs/pc-modding.md` §5). Official references: [BSMT templates repo](https://github.com/Zingabopp/UnityModdingTools.Templates.BeatSaber), [BeatSaberModdingTools.Tasks on NuGet](https://www.nuget.org/packages/BeatSaberModdingTools.Tasks), [BSIPA user install](https://nike4613.github.io/BeatSaber-IPA-Reloaded/articles/start-user.html). + +## 1. Choose template + +We used the **BSIPA Plugin (Bare)** template (smallest layout: `Plugin.cs`, `manifest.json`, `AssemblyInfo`, MSBuild wiring). The upstream folder is `BSIPA Plugin (Bare)/` in the templates repository. + +The guide’s relative paths (e.g. `~/src/Zingabopp/UnityModdingTools.Templates.BeatSaber`) assume a local clone; this machine had no clone, so files were taken from raw GitHub with `curl` (same content as a copy from a clone). + +## 2. Project layout + +Created under repo root: + +| File | Role | +|------|------| +| `Setlist.csproj` | `net472` library, game assembly references via `$(BeatSaberDir)`, `BeatSaberModdingTools.Tasks` package | +| `Directory.Build.props` | `ImportBSMTTargets` + `BSMTProjectType` = BSIPA (from `Directory.Build.props.template`) | +| `Setlist.csproj.user` | **Local only** (gitignored via `*.user`): `BeatSaberDir` → BSManager managed instance | +| `manifest.json` | BSIPA metadata; embedded at build | +| `Plugin.cs` | `[Plugin]` entry; `Log.Info("Hello World")` in `[OnStart]` | +| `Properties/AssemblyInfo.cs` | Assembly identity / version | + +## 3. Placeholders + +Bare template uses `$safeprojectname$`, `$guid1$`, `$targetframeworkversion$`, etc. These were expanded by hand to: + +- `Setlist` (assembly / namespace / plugin id) +- A new `ProjectGuid` and `[assembly: Guid(...)]` +- `TargetFrameworkVersion` → `4.7.2` + +## 4. `manifest.json` + +- **id / name:** `Setlist` +- **description:** Short line describing playlist sync intent +- **gameVersion:** `1.40.8` — taken from the install’s `BeatSaberVersion.txt` (use the `major.minor.patch` prefix; the file may include a `_build` suffix) +- **dependsOn.BSIPA:** `^4.3.0` (per guide; adjust if your BSIPA major differs) + +## 5. NuGet packages (`dotnet restore`) + +No separate `nuget install` was required; `dotnet restore` / `dotnet build` resolved everything from nuget.org. + +| Package | Purpose | +|---------|---------| +| `BeatSaberModdingTools.Tasks` | BSMT MSBuild targets: manifest embedding, copy to game, release zip, etc. Template used `2.0.0-beta1`; we pinned **`2.0.0-beta7`** (latest beta on NuGet at bootstrap time). | +| `Microsoft.NETFramework.ReferenceAssemblies.net472` **`1.0.3`** | **Required on this Linux host** (Nix-provided .NET SDK): first build failed with **MSB3644** (missing .NET Framework 4.7.2 reference assemblies). Adding this package fixes that without a Windows targeting pack. The guide notes BSMT may pull reference assemblies transitively; that did not satisfy MSBuild here, so the package was added **explicitly** to `Setlist.csproj`. | + +## 6. `BeatSaberDir` / `GameDirectory` + +`Setlist.csproj.user` sets: + +```xml +/home/pleb/.local/share/BSManager/BSInstances/1.40.8 +``` + +BSMT resolves the game root as `GameDirectory` and expects `Beat Saber.exe` (or `Beat Saber`) there. Point at the **BSManager managed copy**, not the Steam tree, if that is what you mod. + +## 7. Build + +```bash +cd Setlist +dotnet restore +dotnet build -c Debug +``` + +Expect: + +- `bin/Debug/Setlist.dll` (+ `.pdb`) +- BSMT artifact layout under `bin/Debug/Artifact/Plugins/` + +## 8. Where the DLL lands (Linux caveat) + +`BeatSaberModdingTools.Tasks` runs an `IsProcessRunning` check before copying. **On Unix the task is unsupported**; with `Fallback="True"` BSMT behaves as if the game were running and copies to: + +`$GameDirectory/IPA/Pending/Plugins/` + +So you may see **`IPA/Pending/Plugins/Setlist.dll`** even when Beat Saber is not running. + +This repo adds a small **`CopyToPluginsOnUnixHost`** target at the end of `Setlist.csproj` that, on `$(OS) == 'Unix'`, also copies `$(OutputPath)Setlist.dll` (and `.pdb`) into **`$(BeatSaberDir)/Plugins/`**, so a normal Debug build matches the layout the game loads from **`Plugins/`**. + +After a successful `dotnet build -c Debug`, verify: + +```bash +test -f "$BEAT_SABER_DIR/Plugins/Setlist.dll" && echo OK +``` + +(Replace `$BEAT_SABER_DIR` with your `BeatSaberDir`.) + +## 9. Seeing “Hello World” + +BSIPA logging uses `IPA.Logging.Logger` (exposed as `Log` in `Plugin.cs`). With **`--verbose`** (and optionally **`--debug`** for Unity logs), start the game from BSManager and check the BSIPA console or logs under the game folder (see guide §6). + +## 10. CI note + +Pass **`-p:ContinuousIntegrationBuild=true`** (or define `CIBuild` per template) to disable copying into the game directory on build agents. diff --git a/docs/modding/index.md b/docs/modding/index.md new file mode 100644 index 0000000..40e0713 --- /dev/null +++ b/docs/modding/index.md @@ -0,0 +1,30 @@ +--- +prev: false +next: false +description: Learn how to create your own mods! +--- + +# Making Mods + +Beat Saber _**does not**_ have built in mod support. + +Development for [PC](#pc-mod-development) and [Quest standalone](#quest-mod-development) are two vastly different workflows. + +## PC Mod Development + +If you want to make mods for the PC version of the game, the following guide will cover a multitude of different +processes involved in making mods from scratch, as well as some of the different APIs you have access to. + +Visit the [PC Mod Development](./pc/index.md) page to begin. + +## Quest Mod Development + +The following guide covers most of the concepts you will need for creating mods for the Quest. This includes but is not +limited to: + +- Hooking +- Configuration using `config-utils` +- User Interfaces using `bsml` +- Custom types + +Visit the [Quest Mod Development Intro](./quest/intro.md) page for more information on getting started! diff --git a/docs/modding/pc/bsml.md b/docs/modding/pc/bsml.md new file mode 100644 index 0000000..e2c5ffd --- /dev/null +++ b/docs/modding/pc/bsml.md @@ -0,0 +1,362 @@ +--- +prev: Harmony Patching +next: Zenject and SiraUtil +--- + +# Creating Beat Saber UI + +[BeatSaberMarkupLanguage (BSML)](https://github.com/monkeymanboy/BeatSaberMarkupLanguage) is the most common way to +create customized UI in Beat Saber. BSML is effectively a tag-based language that mimics the GameObject hierarchy +of Unity. It parses tags into GameObjects, and attaches the relevant Unity and Beat Saber UI elements to them. + +The documentation for all BSML components can be found [here](https://monkeymanboy.github.io/BSML-Docs/). + +## Getting Set Up + +Of course, if you want to add BSML in your mod, make sure that you have it installed in your game, and your project +is referencing BSML. + +### Creating the BSML file + +You can name the file anything you want, just make sure that its file extension is `.bsml`. + +![BSML File Screenshot](/.assets/images/modding/pc-mod-bsml-file.jpg 'BSML File Screenshot') + +BSML will require that the bsml file be embedded in the assembly. You can do this by right-clicking the file in the +explorer, going to properties, and then changing the build action to `EmbeddedResource`. + +![Embedded Resource Property Screenshot](/.assets/images/modding/pc-mod-bsml-embeddedresource.jpg 'Embedded Resource Property Screenshot') + +### Writing in BSML + +If you're using Rider, you may have to add a file association for `.bsml` files to get basic syntax highlighting. To +do this, go to `File | Settings | Editor | File Types` and search for `XML`. Add a new file name pattern as `*.bsml`. +This will make Rider accept `.bsml` files as XML files and do highlighting accordingly. + +To get autocompletion in a BSML file, you will need to provide a schema. A way to do this is to use the +[background tag](https://monkeymanboy.github.io/BSML-Docs/Tags/BackgroundTag/) and add the schema to it: + +```xml + + + +``` + +Rider may prompt you that the resource is not found. Simply right click on the URL, or press `Alt+Enter`, and select +fetch external resource. + +Once set up, you should have basic autocompletion for tags if you start typing inside the `` tag. + +## Running Code In The Menu + +There are a couple different ways you can display your BSML in game, however, it is first important to note that +you should not call any of the methods mentioned below outside of the main menu. You should make sure the game has finished +loading the main menu before doing anything. + +- The recommended method, if you don't already use SiraUtil, is BSML's own `MainMenuAwaiter` class that has an + [event](https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/events/) called `MainMenuInitializing` + that is invoked when the main menu loads +- If you are using SiraUtil, it is recommended to bind a type with a `Location.Menu`, or on the `MainSettingsMenuViewControllersInstaller` +- BS Utils also provides events in `BSEvents` and they are called `earlyMenuSceneLoadedFresh` and `lateMenuSceneLoadedFresh` +- You can use the game's `GameScenesManager` and the `transitionDidFinishEvent`, then check if the output `ScenesTransitionSetupDataSO` + is a `MenuScenesTransitionSetupDataSO` using a + [type test expression](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/operators/type-testing-and-cast) +- If you want to get an event when the main menu loads yourself, you can use Unity's + [SceneManager](https://docs.unity3d.com/6000.0/Documentation/ScriptReference/SceneManagement.SceneManager.html) + and check the name of the loaded scene manually, however this is a lot more effort than all of the other methods +- You could also use a Harmony patch into a method that will run every time the menu reinitializes, but this is also unnecessarily + complicated + +## Adding Menus + +Once you have code running in the main menu, it's time to decide where you want to display your UI. + +### Mod Settings + +The mod settings menu is added by BSML and can be accessed from a custom button in the main menu settings. To register +your own tab, check the `BSMLSettings` class. `TutorialMenu` is just a normal class. + +```c# +private readonly TutorialMenu tutorialMenu = new TutorialMenu(); + +public void AddSettingsMenu() +{ + BSMLSettings.Instance.AddSettingsMenu( + name: "Tutorial Mod", + resource: "TutorialMod.tutorial.bsml", + host: tutorialMenu); +} +``` + +![Mod Settings Screenshot](/.assets/images/modding/pc-mod-bsml-settings.jpg 'Mod Settings Screenshot') + +### Gameplay Setup + +The mods tab is added by BSML in the Gameplay Setup menu, which is found to the left of the song list, where +you can normally find player settings and gameplay modifiers. To register a new tab, check the `GameplaySetup` class. +`TutorialMenu` is just a normal class. + +```c# +private readonly TutorialMenu tutorialMenu = new TutorialMenu(); + +public void AddTab() +{ + GameplaySetup.Instance.AddTab( + name: "Tutorial Mod", + resource: "TutorialMod.tutorial.bsml", + host: tutorialMenu); +} +``` + +![Mod Tabs Screenshot](/.assets/images/modding/pc-mod-bsml-tabs.jpg 'Mod Tabs Screenshot') + +### Custom Flow Coordinator + +BSML gives you a way to create a button in the left screen of the main menu. This button can do anything you want it +to do, but most modders make it present their mod's UI. This is done by using a `FlowCoordinator`, and by adding one +or more `ViewController` objects to it. + +BSML provides methods to create both flow coordinators and view controllers, which makes this process a lot cleaner. + +BSML has a few choices of view controller types you can inherit; we are going to use the `BSMLAutomaticViewController` +because it has the option of hot reloading the menu when you make changes to the bsml file. + +```c# +[ViewDefinition("TutorialMod.tutorial.bsml")] +public class TutorialViewController : BSMLAutomaticViewController { } +``` + +The flow coordinator is responsible for managing view controllers. `FlowCoordinator` has many members that you can use +or override, so it's worth checking out the code for it. + +```c# +public class TutorialFlowCoordinator : FlowCoordinator +{ + private readonly TutorialViewController tutorialViewController = BeatSaberUI.CreateViewController(); + + // Called immediately when the flow coordinator is activated + protected override void DidActivate(bool firstActivation, bool addedToHierarchy, bool screenSystemEnabling) + { + if (firstActivation) + { + // Sets the title text in the top bar + SetTitle("Tutorial Mod"); + showBackButton = true; + } + + if (addedToHierarchy) + { + ProvideInitialViewControllers(tutorialViewController); + } + } + + protected override void BackButtonWasPressed(ViewController topViewController) + { + BeatSaberUI.MainFlowCoordinator.DismissFlowCoordinator(this); + } +} +``` + +The code below is related to managing the menu button. We tell the `MainFlowCoordinator` to present our own +flow coordinator. You can also have your own way to dismiss your flow coordinator but, in the example above, +we are relying on the back button to do this. + +```c# +private readonly TutorialFlowCoordinator tutorialFlowCoordinator; +private readonly MenuButton menuButton; + +public MenuManager() +{ + tutorialFlowCoordinator = BeatSaberUI.CreateFlowCoordinator(); + menuButton = new MenuButton("Tutorial Mod", ShowFlowCoordinator); +} + +public void AddMenuButton() +{ + MenuButtons.Instance.RegisterButton(menuButton); +} + +private void ShowFlowCoordinator() +{ + BeatSaberUI.MainFlowCoordinator.PresentFlowCoordinator(tutorialFlowCoordinator); +} +``` + +![Mod Buttons Screenshot](/.assets/images/modding/pc-mod-bsml-buttons.jpg 'Mod Buttons Screenshot') + +### Floating Screen + +If you want to place your UI components anywhere, you can create a floating screen. This will allow you to have a view controller +anywhere in the world. You can also create a handle for the floating screen which will allow the player to move the screen +around. + +The example below creates just creates a small screen near the ground in front of the player's place. + +```c# +private readonly TutorialViewController tutorialViewController = BeatSaberUI.CreateViewController(); + +public void CreateFloatingScreen() +{ + var floatingScreen = FloatingScreen.CreateFloatingScreen( + screenSize: new Vector2(25f, 10f), + createHandle: false, + position: new Vector3(0f, 0.5f, 2f), + rotation: Quaternion.Euler(45f, 0f, 0f)); + + floatingScreen.SetRootViewController(tutorialViewController, ViewController.AnimationType.None); +} +``` + +Since floating screens aren't part of the screen system, and because the menu persists during gameplay, you can have +the floating screen active in the game scene. The below screenshot is of the floating screen from +[SliceDetails](https://github.com/qqrz997/SliceDetails), which is activated when the game is paused. + +![Floating Screen Screenshot](/.assets/images/modding/pc-mod-bsml-floating-screen.jpg 'Floating Screen Screenshot') + +## Interacting With The Menu + +Now let's take a look at some of the ways you can make use of your UI. Again, to find out more about the components +that we will talk about in the following sections, check the [BSML documentation](https://monkeymanboy.github.io/BSML-Docs/). + +### Buttons And Actions + +We are going to add a [button](https://monkeymanboy.github.io/BSML-Docs/Tags/ButtonTag/) to the menu: + +```xml +