Skip to content
Merged
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
5 changes: 3 additions & 2 deletions .github/workflows/create-draft-preview.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
name: Prepare Preview
on:
workflow_dispatch:

workflow_dispatch:
inputs:
name:
description: "Release name"
Expand All @@ -23,7 +24,7 @@ jobs:
steps:
- name: Checkout Repo
uses: actions/checkout@v6

- name: Checkout AppCast Repo
uses: actions/checkout@v6
with:
Expand Down
34 changes: 34 additions & 0 deletions Docs/ML Format.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
> All byte values are little-endian

### ModList Format

| Segment | Value / Range | Length | Type | Encoding / Details |
|:---------------|:--------------|:---------|:----------|:-----------------------------------------------------------|
| Signature | "SRMM_ML" | 7 bytes | char[] | ASCII text, hex: `53 52 4D 4D 5F 4D 4C` |
| Version | 0 - 255 | 1 byte | uint8 | File format version |
| Active Profile | 0 - 7 | 1 byte | uint8 | Active profile index (value + 1 = profile, 0 -> Profile 1) |
| Mod Count | 0 - 65,535 | 2 bytes | uint16 | Number of mod entries |
| Mod Entries | Variable | Variable | `ModInfo` | Repeats `Mod Count` times |


### ModInfo Format

| Segment | Value / Range | Length | Type | Encoding / Details |
|:-----------------|:-------------------|:---------|:-------|:-------------------------------------------------------|
| Enabled Profiles | `Enabled Profiles` | 1 byte | uint8 | Bitmask representing profiles where the mod is enabled |
| Name Length | 0 - 65,535 | 2 bytes | utin16 | Length of mod name in bytes |
| Mod Name | Variable | Variable | char[] | UTF8 encoded text, length = `Name Length` |


### Enabled Profiles Format

| Bit | Mask | Profile |
|:----|:-----|:----------|
| 0 | 0x01 | Profile 1 |
| 1 | 0x02 | Profile 2 |
| 2 | 0x04 | Profile 3 |
| 3 | 0x08 | Profile 4 |
| 4 | 0x10 | Profile 5 |
| 5 | 0x20 | Profile 6 |
| 6 | 0x40 | Profile 7 |
| 7 | 0x80 | Profile 8 |
6 changes: 6 additions & 0 deletions ShinRyuModManager-CE/Attributes/DescriptionAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace ShinRyuModManager.Attributes;

[AttributeUsage(AttributeTargets.Field)]
public class DescriptionAttribute(string name) : Attribute {
public string Name { get; } = name;
}
9 changes: 9 additions & 0 deletions ShinRyuModManager-CE/Exceptions/ModListFileLoadException.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace ShinRyuModManager.Exceptions;

public class ModListFileLoadException : Exception {
public ModListFileLoadException() { }

public ModListFileLoadException(string message) : base(message) { }

public ModListFileLoadException(string message, Exception inner) : base(message, inner) { }
}
22 changes: 22 additions & 0 deletions ShinRyuModManager-CE/Extensions/ProfileExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using System.Reflection;
using ShinRyuModManager.Attributes;
using ShinRyuModManager.ModLoadOrder.Mods;

namespace ShinRyuModManager.Extensions;

public static class ProfileExtensions {
public static ProfileMask ToMask(this Profile profile) {
return (ProfileMask)(1 << (int)profile);
}

public static bool AppliesTo(this ProfileMask mask, Profile profile) {
return mask.HasFlag(profile.ToMask());
}

public static string GetDescription(this Profile profile) {
var field = profile.GetType().GetField(profile.ToString());
var attr = field?.GetCustomAttribute<DescriptionAttribute>();

return attr?.Name ?? profile.ToString();
}
}
54 changes: 54 additions & 0 deletions ShinRyuModManager-CE/Language/Views/MainWindow.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

27 changes: 27 additions & 0 deletions ShinRyuModManager-CE/Language/Views/MainWindow.resx
Original file line number Diff line number Diff line change
Expand Up @@ -99,4 +99,31 @@
<data name="ModUp_Tooltip" xml:space="preserve">
<value>Move the selected mod(s) up</value>
</data>
<data name="Menu_Profiles" xml:space="preserve">
<value>Profiles</value>
</data>
<data name="Menu_Profile1" xml:space="preserve">
<value>Profile 1</value>
</data>
<data name="Menu_Profile2" xml:space="preserve">
<value>Profile 2</value>
</data>
<data name="Menu_Profile3" xml:space="preserve">
<value>Profile 3</value>
</data>
<data name="Menu_Profile4" xml:space="preserve">
<value>Profile 4</value>
</data>
<data name="Menu_Profile5" xml:space="preserve">
<value>Profile 5</value>
</data>
<data name="Menu_Profile6" xml:space="preserve">
<value>Profile 6</value>
</data>
<data name="Menu_Profile7" xml:space="preserve">
<value>Profile 7</value>
</data>
<data name="Menu_Profile8" xml:space="preserve">
<value>Profile 8</value>
</data>
</root>
12 changes: 7 additions & 5 deletions ShinRyuModManager-CE/ModLoadOrder/Generator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
namespace ShinRyuModManager.ModLoadOrder;

public static class Generator {
public static async Task<MLO> GenerateModeLoadOrder(List<string> mods, bool looseFilesEnabled, bool cpkRepackingEnabled) {
public static async Task<MLO> GenerateModeLoadOrder(List<ModInfo> mods, bool looseFilesEnabled, bool cpkRepackingEnabled) {
List<int> modIndices = [0];
var files = new OrderedSet<string>();
var modsWithFoldersNotFound = new Dictionary<string, List<string>>(); // Dict of Mod, ListOfFolders
Expand Down Expand Up @@ -42,7 +42,7 @@ public static async Task<MLO> GenerateModeLoadOrder(List<string> mods, bool loos
// Use a reverse loop to be able to remove items from the list when necessary
for (var i = mods.Count - 1; i >= 0; i--) {
var mod = new Mod(mods[i]);
var modPath = GamePath.GetModDirectory(mods[i]);
var modPath = GamePath.GetModDirectory(mod.Name);

mod.AddFiles(modPath, "");

Expand Down Expand Up @@ -104,17 +104,19 @@ public static async Task<MLO> GenerateModeLoadOrder(List<string> mods, bool loos
mods.Reverse();

Log.Information($"Generating {Constants.MLO} file...");

var modNames = mods.Select(x => x.Name).ToList();

// Generate MLO
var mlo = new MLO(modIndices, mods, files, loose.ParlessFolders, cpkDictionary);
var mlo = new MLO(modIndices, modNames, files, loose.ParlessFolders, cpkDictionary);

mlo.WriteMLO(Path.Combine(GamePath.FullGamePath, Constants.MLO));

Log.Information("Finished generating MLO.");

// Check if a mod has a par that will override the repacked par, and skip repacking it in that case
foreach (var key in parDictionary.Keys.ToList()) {
var value = parDictionary[key];
var value = new ModInfo(parDictionary[key][0], 0);

// Faster lookup by checking in the OrderedSet
if (!files.Contains($"{key}.par"))
Expand All @@ -124,7 +126,7 @@ public static async Task<MLO> GenerateModeLoadOrder(List<string> mods, bool loos
var matchIndex = mlo.Files.Find(f => f.Name == Utils.NormalizeToNodePath($"{key}.par")).Index;

// Avoid repacking pars which exist as a file in mods that have a higher priority that the first mod in the par to be repacked
if (mods.IndexOf(value[0]) > matchIndex) {
if (mods.IndexOf(value) > matchIndex) {
parDictionary.Remove(key);
}
}
Expand Down
2 changes: 2 additions & 0 deletions ShinRyuModManager-CE/ModLoadOrder/Mods/Mod.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ public class Mod {
/// Folders that need to be repacked.
/// </summary>
public List<string> RepackCpKs { get; }

public Mod(ModInfo modInfo) : this(modInfo.Name) { }

public Mod(string name) {
Name = name;
Expand Down
26 changes: 17 additions & 9 deletions ShinRyuModManager-CE/ModLoadOrder/Mods/ModInfo.cs
Original file line number Diff line number Diff line change
@@ -1,24 +1,32 @@
using CommunityToolkit.Mvvm.ComponentModel;
using Utils;
using ShinRyuModManager.Extensions;

namespace ShinRyuModManager.ModLoadOrder.Mods;

public sealed partial class ModInfo : ObservableObject, IEquatable<ModInfo> {
[ObservableProperty] private string _name;
[ObservableProperty] private bool _enabled;

public ModInfo(string name, bool enabled = true) {
public ProfileMask EnabledProfiles { get; set; }

public bool Enabled {
get => EnabledProfiles.AppliesTo(Program.ActiveProfile);
set {
if (value) {
EnabledProfiles |= Program.ActiveProfile.ToMask();
} else {
EnabledProfiles &= ~Program.ActiveProfile.ToMask();
}
}
}

public ModInfo(string name, ProfileMask enabledMask = ProfileMask.All) {
Name = name;
Enabled = enabled;
EnabledProfiles = enabledMask;
}

public void ToggleEnabled() {
Enabled = !Enabled;
}

public static bool IsValid(ModInfo info) {
return (info != null) && Directory.Exists(Path.Combine(GamePath.MODS, info.Name));
}

public bool Equals(ModInfo other) {
if (other is null)
Expand All @@ -44,6 +52,6 @@ public override bool Equals(object obj) {
}

public override int GetHashCode() {
return HashCode.Combine(Name, Enabled);
return Name.GetHashCode();
}
}
22 changes: 22 additions & 0 deletions ShinRyuModManager-CE/ModLoadOrder/Mods/Profile.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using ShinRyuModManager.Attributes;

namespace ShinRyuModManager.ModLoadOrder.Mods;

public enum Profile : byte {
[Description("Profile 1")]
Profile1 = 0,
[Description("Profile 2")]
Profile2 = 1,
[Description("Profile 3")]
Profile3 = 2,
[Description("Profile 4")]
Profile4 = 3,
[Description("Profile 5")]
Profile5 = 4,
[Description("Profile 6")]
Profile6 = 5,
[Description("Profile 7")]
Profile7 = 6,
[Description("Profile 8")]
Profile8 = 7
}
14 changes: 14 additions & 0 deletions ShinRyuModManager-CE/ModLoadOrder/Mods/ProfileMask.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
namespace ShinRyuModManager.ModLoadOrder.Mods;

[Flags]
public enum ProfileMask : byte {
Profile1 = 1 << 0,
Profile2 = 1 << 1,
Profile3 = 1 << 2,
Profile4 = 1 << 3,
Profile5 = 1 << 4,
Profile6 = 1 << 5,
Profile7 = 1 << 6,
Profile8 = 1 << 7,
All = Profile1 | Profile2 | Profile3 | Profile4 | Profile5 | Profile6 | Profile7 | Profile8
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using Utils;

namespace ShinRyuModManager.ModLoadOrder.Mods.Serialization;

// Old mod list format (ModLoadOrder.txt)
public static partial class ModListSerializer {
private static List<ModInfo> ReadV0(string path) {
var mods = new List<ModInfo>();

foreach (var line in File.ReadLines(path)) {
if (line.StartsWith(';'))
continue;

var sanitizedLine = line.Split(';', 1, StringSplitOptions.TrimEntries)[0];

if (string.IsNullOrEmpty(sanitizedLine) || !Directory.Exists(GamePath.GetModDirectory(sanitizedLine)))
continue;

var entry = new ModInfo(sanitizedLine);

if (mods.Contains(entry))
continue;

mods.Add(entry);
}

return mods;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
using Utils;

namespace ShinRyuModManager.ModLoadOrder.Mods.Serialization;

// Original SRMM mod list format (ModList.txt)
public static partial class ModListSerializer {
private static List<ModInfo> ReadV1(string path) {
var mods = new List<ModInfo>();

var modContent = File.ReadAllText(path);

if (string.IsNullOrWhiteSpace(modContent))
return mods;

foreach (var mod in modContent.Split('|', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)) {
if (!mod.StartsWith('<') && !mod.StartsWith('>'))
continue;

var enabledMask = mod[0] == '<' ? ProfileMask.All : 0;
var modName = mod[1..];

if (!Directory.Exists(GamePath.GetModDirectory(modName)))
continue;

var entry = new ModInfo(modName, enabledMask);

if (mods.Contains(entry))
continue;

mods.Add(entry);
}

return mods;
}
}
Loading
Loading