diff --git a/MRDX.Base.Mod.Interfaces/Definitions.cs b/MRDX.Base.Mod.Interfaces/Definitions.cs index eb17b07..0b210d4 100644 --- a/MRDX.Base.Mod.Interfaces/Definitions.cs +++ b/MRDX.Base.Mod.Interfaces/Definitions.cs @@ -1,4 +1,5 @@ using System; +using System.Runtime.InteropServices; namespace MRDX.Base.Mod.Interfaces; @@ -283,4 +284,72 @@ public enum ErrantyType : byte Withering, Skill, Special -} \ No newline at end of file +} + +[StructLayout(LayoutKind.Explicit)] +public struct Box +{ + [FieldOffset(0x0)] + public nint Next; + + [FieldOffset(0x4)] + public nint Previous; + + [FieldOffset(0x8)] + public nint Attribute; + + [FieldOffset(0xC)] + public short XCopy; + + [FieldOffset(0xE)] + public short YCopy; + + [FieldOffset(0x10)] + public short X; + + [FieldOffset(0x12)] + public short Y; + + [FieldOffset(0x1E)] + public short Z; + + [FieldOffset(0x2C)] + public short XOffset; + + [FieldOffset(0x2E)] + public short YOffset; + + [FieldOffset(0x40)] + public int unk1; + + [FieldOffset(0x44)] + public int unk2; + + [FieldOffset(0x48)] + public int unk3; +} + +[StructLayout(LayoutKind.Explicit)] +public struct BoxAttribute +{ + [FieldOffset(0x0)] + public byte unk1; + + [FieldOffset(0x10)] + public ushort Width; + + [FieldOffset(0x12)] + public ushort Height; + + [FieldOffset(0x14)] + public byte R; + + [FieldOffset(0x15)] + public byte G; + + [FieldOffset(0x16)] + public byte B; + + [FieldOffset(0x17)] + public byte IsSemiTransparent; +} diff --git a/MRDX.Base.Mod.Interfaces/IHooks.cs b/MRDX.Base.Mod.Interfaces/IHooks.cs index 95f7cdf..9bb3a10 100644 --- a/MRDX.Base.Mod.Interfaces/IHooks.cs +++ b/MRDX.Base.Mod.Interfaces/IHooks.cs @@ -81,6 +81,24 @@ namespace MRDX.Base.Mod.Interfaces; [Function(CallingConventions.Fastcall)] public delegate int DrawIntWithHorizontalSpacing(short x, short y, int number); +[HookDef(BaseGame.Mr2, Region.Us, "55 8B EC 83 E4 F8 56 57 8B 7D 08 6A 03")] +[Function(CallingConventions.Cdecl)] +public delegate int DrawLoyalty(nint unk1); + +[HookDef(BaseGame.Mr2, Region.Us, "55 8B EC 83 E4 F8 81 EC ?? ?? ?? ?? A1 ?? ?? ?? ?? 33 C4 89 84 24 ?? ?? ?? ?? 53 56 8B 75 08 8D 44 24 14")] +[Function(CallingConventions.Fastcall)] +public delegate int DrawTextWithPadding(short x, short y, nint text, short padding); + +// Function that is called every tick while you're in the farm. +// Hooking this causes various UI elements in the Farm to not function for some reason. +[HookDef(BaseGame.Mr2, Region.Us, "55 8B EC 6A FF 68 ?? ?? ?? ?? 64 A1 ?? ?? ?? ?? 50 53 56 57 A1 ?? ?? ?? ?? 33 C5 50 8D 45 F4 64 A3 ?? ?? ?? ?? 8B 75 08 8B 7D 10 F6 46 0C 03 0F 84")] +[Function(CallingConventions.Cdecl)] +public delegate void DrawFarmUiElements(nint unk1, nint unk2, nint unk3); + +[HookDef(BaseGame.Mr2, Region.Us, "80 79 38 01 75 38")] +[Function(CallingConventions.MicrosoftThiscall)] +public delegate void RemovesSomeUiElements(nint self); + /** * Called when setting up the battle controls. CCtrlBattle seems to store things like the battle timer among other things. * Its heap allocated so we can't just use a fixed memory address. diff --git a/MRDX.Base.Mod/BaseObject.cs b/MRDX.Base.Mod/BaseObject.cs index f9f057e..bc6485f 100644 --- a/MRDX.Base.Mod/BaseObject.cs +++ b/MRDX.Base.Mod/BaseObject.cs @@ -37,12 +37,7 @@ protected BaseObject(int baseOffset = 0) // If no base is provided, check the class for the offset attribute for static memory locations if (baseOffset == 0) - foreach (var offset in typeof(TParent).GetCustomAttributes().OfType()) - { - if (offset.Game != Base.Game || offset.Region != Base.Region) continue; - baseOffset = offset.Offset; - break; - } + baseOffset = BaseOffset(); BaseAddress = Base.ExeBaseAddress + baseOffset; @@ -217,6 +212,17 @@ protected void WriteStrOffset(string val, int offset) _memory.WriteRaw((nuint)(BaseAddress + offset), bytes); } + public static int BaseOffset() + { + foreach (var offset in typeof(TParent).GetCustomAttributes().OfType()) + { + if (offset.Game != Base.Game || offset.Region != Base.Region) continue; + return offset.Offset; + } + + return 0; + } + public static int Get([CallerMemberName] string propName = "") { diff --git a/MRDX.Base.Mod/Game.cs b/MRDX.Base.Mod/Game.cs index ac833ee..6c4da88 100644 --- a/MRDX.Base.Mod/Game.cs +++ b/MRDX.Base.Mod/Game.cs @@ -19,7 +19,7 @@ public Game(ModContext context) } [BaseOffset(BaseGame.Mr2, Region.Us, 0x97A0C)] - public IMonster Monster { get; set; } = new Monster(Get()); + public IMonster Monster { get; set; } = new Monster(Get() + BaseObject.BaseOffset()); public event IGame.MonsterChange? OnMonsterChanged; diff --git a/MRDX.Ui.ViewLifeIndex/.github/workflows/reloaded.yml b/MRDX.Ui.ViewLifeIndex/.github/workflows/reloaded.yml new file mode 100644 index 0000000..ff8ff0a --- /dev/null +++ b/MRDX.Ui.ViewLifeIndex/.github/workflows/reloaded.yml @@ -0,0 +1,149 @@ +# Script to build and publish a Reloaded Mod. +# by Sewer56 + +# Produces: +# - Build to Upload to GameBanana +# - Build to Upload to GitHub +# - Build to Upload to NuGet +# - Changelog + +# When pushing a tag +# - Upload to GitHub Releases +# - Upload to Reloaded NuGet Repository (if GitHub Secret RELOADED_NUGET_KEY is specified) + +name: Build and Publish Reloaded Mod + +on: + push: + branches: [ main ] + tags: + - '*' + pull_request: + branches: [ main ] + workflow_dispatch: + +env: + PUBLISH_COMMON_PATH: ./Publish/ToUpload/ + + PUBLISH_GAMEBANANA_PATH: ./Publish/ToUpload/GameBanana + PUBLISH_GITHUB_PATH: ./Publish/ToUpload/Generic + PUBLISH_NUGET_PATH: ./Publish/ToUpload/NuGet + + PUBLISH_CHANGELOG_PATH: ./Publish/Changelog.md + PUBLISH_PATH: ./Publish + + RELOADEDIIMODS: . + + # Default value is official Reloaded package server. + NUGET_URL: http://packages.sewer56.moe:5000/v3/index.json + + IS_RELEASE: ${{ startsWith(github.ref, 'refs/tags/') }} + RELEASE_TAG: ${{ github.ref_name }} + +jobs: + build: + runs-on: windows-latest + defaults: + run: + shell: pwsh + + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + submodules: 'recursive' + + - name: Setup .NET Core SDK (5.0) + uses: actions/setup-dotnet@v1.8.2 + with: + dotnet-version: 5.0.x + + - name: Setup .NET Core SDK (6.0) + uses: actions/setup-dotnet@v1.8.2 + with: + dotnet-version: 6.0.x + + - name: Setup Node.js + uses: actions/setup-node@v2 + with: + node-version: '14' + + - name: Setup AutoChangelog + run: npm install -g auto-changelog + + - name: Create Changelog + run: | + [System.IO.Directory]::CreateDirectory("$env:PUBLISH_PATH") + if ($env:IS_RELEASE -eq 'true') { + auto-changelog --sort-commits date --hide-credit --template keepachangelog --commit-limit false --starting-version "$env:RELEASE_TAG" --output "$env:PUBLISH_CHANGELOG_PATH" + } + else { + auto-changelog --sort-commits date --hide-credit --template keepachangelog --commit-limit false --unreleased --output "$env:PUBLISH_CHANGELOG_PATH" + } + + - name: Build + run: ./Publish.ps1 -ChangelogPath "$env:PUBLISH_CHANGELOG_PATH" + + - name: Upload GitHub Release Artifact + uses: actions/upload-artifact@v2.2.4 + with: + # Artifact name + name: GitHub Release + # A file, directory or wildcard pattern that describes what to upload + path: | + ${{ env.PUBLISH_GITHUB_PATH }}/* + + - name: Upload GameBanana Release Artifact + uses: actions/upload-artifact@v2.2.4 + with: + # Artifact name + name: GameBanana Release + # A file, directory or wildcard pattern that describes what to upload + path: | + ${{ env.PUBLISH_GAMEBANANA_PATH }}/* + + - name: Upload NuGet Release Artifact + uses: actions/upload-artifact@v2.2.4 + with: + # Artifact name + name: NuGet Release + # A file, directory or wildcard pattern that describes what to upload + path: | + ${{ env.PUBLISH_NUGET_PATH }}/* + + - name: Upload Changelog Artifact + uses: actions/upload-artifact@v2.2.4 + with: + # Artifact name + name: Changelog + # A file, directory or wildcard pattern that describes what to upload + path: ${{ env.PUBLISH_CHANGELOG_PATH }} + retention-days: 0 + + - name: Upload to GitHub Releases (on Tag) + uses: softprops/action-gh-release@v0.1.14 + if: env.IS_RELEASE == 'true' + with: + # Path to load note-worthy description of changes in release from + body_path: ${{ env.PUBLISH_CHANGELOG_PATH }} + # Newline-delimited list of path globs for asset files to upload + files: | + ${{ env.PUBLISH_GITHUB_PATH }}/* + + - name: Push to NuGet (on Tag) + env: + NUGET_KEY: ${{ secrets.RELOADED_NUGET_KEY }} + if: env.IS_RELEASE == 'true' + run: | + if ([string]::IsNullOrEmpty("$env:NUGET_KEY")) + { + Write-Host "NuGet Repository Key (GitHub Secrets -> RELOADED_NUGET_KEY) Not Specified. Skipping." + return + } + + $items = Get-ChildItem -Path "$env:PUBLISH_NUGET_PATH/*.nupkg" + Foreach ($item in $items) + { + Write-Host "Pushing $item" + dotnet nuget push "$item" -k "$env:NUGET_KEY" -s "$env:NUGET_URL" --skip-duplicate + } diff --git a/MRDX.Ui.ViewLifeIndex/BuildLinked.ps1 b/MRDX.Ui.ViewLifeIndex/BuildLinked.ps1 new file mode 100644 index 0000000..245ac33 --- /dev/null +++ b/MRDX.Ui.ViewLifeIndex/BuildLinked.ps1 @@ -0,0 +1,9 @@ +# Set Working Directory +Split-Path $MyInvocation.MyCommand.Path | Push-Location +[Environment]::CurrentDirectory = $PWD + +Remove-Item "$env:RELOADEDIIMODS/MRDX.Ui.ViewLifeIndex/*" -Force -Recurse +dotnet publish "./MRDX.Ui.ViewLifeIndex.csproj" -c Release -o "$env:RELOADEDIIMODS/MRDX.Ui.ViewLifeIndex" /p:OutputPath="./bin/Release" /p:ReloadedILLink="true" + +# Restore Working Directory +Pop-Location \ No newline at end of file diff --git a/MRDX.Ui.ViewLifeIndex/MRDX.Ui.ViewLifeIndex.csproj b/MRDX.Ui.ViewLifeIndex/MRDX.Ui.ViewLifeIndex.csproj new file mode 100644 index 0000000..2920b08 --- /dev/null +++ b/MRDX.Ui.ViewLifeIndex/MRDX.Ui.ViewLifeIndex.csproj @@ -0,0 +1,55 @@ + + + + net7.0-windows + false + true + 10.0 + enable + True + $(RELOADEDIIMODS)/MRDX.Ui.ViewLifeIndex + enable + + + false + + + + + + + + + + + + + + + + + + + + + + Always + + + PreserveNewest + + + + + + + + + + + + + + diff --git a/MRDX.Ui.ViewLifeIndex/Mod.cs b/MRDX.Ui.ViewLifeIndex/Mod.cs new file mode 100644 index 0000000..d102e88 --- /dev/null +++ b/MRDX.Ui.ViewLifeIndex/Mod.cs @@ -0,0 +1,322 @@ +using MRDX.Base.Mod; +using MRDX.Base.Mod.Interfaces; +using MRDX.Ui.ViewLifeIndex.Template; +using Reloaded.Hooks.Definitions; +using Reloaded.Mod.Interfaces; +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Threading; +using IReloadedHooks = Reloaded.Hooks.ReloadedII.Interfaces.IReloadedHooks; + +namespace MRDX.Ui.ViewLifeIndex +{ + /// + /// Your mod logic goes here. + /// + public class Mod : ModBase // <= Do not Remove. + { + /// + /// Provides access to the mod loader API. + /// + private readonly IModLoader _modLoader; + + /// + /// Provides access to the Reloaded.Hooks API. + /// + /// This is null if you remove dependency on Reloaded.SharedLib.Hooks in your mod. + private readonly IReloadedHooks? _hooks; + + /// + /// Provides access to the Reloaded logger. + /// + private readonly ILogger _logger; + + /// + /// Entry point into the mod, instance that created this class. + /// + private readonly IMod _owner; + + /// + /// The configuration of the currently executing mod. + /// + private readonly IModConfig _modConfig; + + private IHook? _loyaltyHook; + private DrawTextWithPadding? _drawText; + private IHook? _removeUiHook; + + private IMonster monster; + + private List allAddresses = new List(); + private List allBoxes = new List(); + + private nint rootBoxPtr; + private Box rootBox; + + private bool initialized = false; + + private nint stressTextAddr; + private nint fatigueTextAddr; + private nint lifeIndexTextAddr; + + public Mod(ModContext context) + { + _modLoader = context.ModLoader; + _hooks = context.Hooks; + _logger = context.Logger; + _owner = context.Owner; + _modConfig = context.ModConfig; + + _modLoader.GetController().TryGetTarget(out var hooks); + + hooks!.AddHook(RestoreUiLinkedList).ContinueWith(result => _removeUiHook = result.Result.Activate()); + hooks!.AddHook(DrawLifeIndex).ContinueWith(result => _loyaltyHook = result.Result.Activate()); + hooks!.CreateWrapper().ContinueWith(result => _drawText = result.Result); + + WeakReference _game = _modLoader.GetController(); + _game.TryGetTarget(out var g); + g!.OnMonsterChanged += MonsterChanged; + } + private void MonsterChanged(IMonsterChange mon) + { + monster = mon.Current; + + if (stressTextAddr != 0) + { + Marshal.FreeCoTaskMem(stressTextAddr); + Marshal.FreeCoTaskMem(fatigueTextAddr); + } + + AllocateText(monster); + } + + private void AllocateText(IMonster monster) + { + byte[] stress = $"Stress:{monster.Stress}".AsMr2().AsBytes(); + + stressTextAddr = Marshal.AllocCoTaskMem(stress.Length); + Marshal.Copy(stress, 0, stressTextAddr, stress.Length); + + byte[] fatigue = $"Fatigue:{monster.Fatigue}".AsMr2().AsBytes(); + + fatigueTextAddr = Marshal.AllocCoTaskMem(fatigue.Length); + Marshal.Copy(fatigue, 0, fatigueTextAddr, fatigue.Length); + + byte[] lifeIndex = getLifeIndexText(monster).AsMr2().AsBytes(); + + lifeIndexTextAddr = Marshal.AllocCoTaskMem(lifeIndex.Length); + Marshal.Copy(lifeIndex, 0, lifeIndexTextAddr, lifeIndex.Length); + } + + private int getLifeIndex(IMonster monster) + { + return monster.Fatigue + (monster.Stress * 2); + } + + private string getLifeIndexText(IMonster monster) + { + int lifeIndex = getLifeIndex(monster); + int lifespanHit = getLifespanHit(monster); + + return $"LI:{lifeIndex}({lifespanHit}w)"; + } + + private int getLifespanHit(IMonster monster) + { + int lifeIndex = getLifeIndex(monster); + + if (lifeIndex >= 280) + { + return -7; + } + + return -1 * ((lifeIndex - 70) / 35 + 1); + } + + private void RestoreUiLinkedList(nint self) + { + nint CSysFarmPtrPtr = Marshal.ReadInt32((nint)Base.Mod.Base.ExeBaseAddress + 0x372308); + + if (CSysFarmPtrPtr != 0) + { + nint CSysFarmPtr = Marshal.ReadInt32(CSysFarmPtrPtr + 0x3C); + byte isDisplayed = Marshal.ReadByte(CSysFarmPtr + 0x38); + + if (initialized == true && isDisplayed == 1) + { + rootBox.Next = allBoxes.Last().Next; + Marshal.StructureToPtr(rootBox, rootBoxPtr, false); + + allBoxes = new List(); + + for (int i = 0; i < allAddresses.Count; i++) + { + Marshal.FreeCoTaskMem(allAddresses[i]); + } + + allAddresses = new List(); + + initialized = false; + } + } + + _removeUiHook!.OriginalFunction(self); + } + + private void PrependToBoxList(Box box, nint boxAddr) + { + var rootBoxNext = rootBox.Next; + rootBox.Next = boxAddr; + box.Next = rootBoxNext; + box.Previous = rootBoxPtr; + + var next = Marshal.PtrToStructure(rootBoxNext); + next.Previous = boxAddr; + + Marshal.StructureToPtr(next, rootBoxNext, false); + Marshal.StructureToPtr(rootBox, rootBoxPtr, false); + Marshal.StructureToPtr(box, boxAddr, false); + } + + private BoxAttribute GetBackgroundBoxAttribute(ushort width, ushort height) + { + BoxAttribute backgroundAttr = new BoxAttribute(); + backgroundAttr.unk1 = 5; + backgroundAttr.Width = width; + backgroundAttr.Height = height; + backgroundAttr.R = 128; + backgroundAttr.G = 128; + backgroundAttr.B = 128; + backgroundAttr.IsSemiTransparent = 0; + + return backgroundAttr; + } + + private BoxAttribute GetForegroundBoxAttribute(ushort width, ushort height) + { + BoxAttribute backgroundAttr = new BoxAttribute(); + backgroundAttr.unk1 = 5; + backgroundAttr.Width = width; + backgroundAttr.Height = height; + backgroundAttr.R = 64; + backgroundAttr.G = 64; + backgroundAttr.B = 128; + backgroundAttr.IsSemiTransparent = 1; + + return backgroundAttr; + } + + private Box GetBox(short x, short y, short z, nint boxAttrPtr) + { + Box box = new Box(); + box.Attribute = boxAttrPtr; + box.X = x; + box.Y = y; + box.XCopy = box.X; + box.YCopy = box.Y; + box.Z = z; + box.XOffset = 0; + box.YOffset = 0; + + return box; + } + + private void DrawBox(ushort width, ushort height, short x, short y) + { + BoxAttribute backgroundAttr = GetBackgroundBoxAttribute(width, height); + + nint backgroundAttrPtr = Marshal.AllocCoTaskMem(Marshal.SizeOf(backgroundAttr)); + Marshal.StructureToPtr(backgroundAttr, backgroundAttrPtr, false); + + allAddresses = allAddresses.Prepend(backgroundAttrPtr).ToList(); + + Box box = GetBox(x, y, 2, backgroundAttrPtr); + + nint boxAddr = Marshal.AllocCoTaskMem(Marshal.SizeOf(box)); + + PrependToBoxList(box, boxAddr); + + allBoxes = allBoxes.Prepend(box).ToList(); + allAddresses = allAddresses.Prepend(boxAddr).ToList(); + + + BoxAttribute foregroundAttr = GetForegroundBoxAttribute((ushort)(width - 4), (ushort)(height - 4)); + + nint foregroundAttrPtr = Marshal.AllocCoTaskMem(Marshal.SizeOf(foregroundAttr)); + Marshal.StructureToPtr(foregroundAttr, foregroundAttrPtr, false); + + allAddresses = allAddresses.Prepend(foregroundAttrPtr).ToList(); + + Box foregroundBox = GetBox((short)(x + 2), (short)(y + 2), 3, foregroundAttrPtr); + + nint foregroundBoxAddr = Marshal.AllocCoTaskMem(Marshal.SizeOf(foregroundBox)); + + PrependToBoxList(foregroundBox, foregroundBoxAddr); + + allBoxes = allBoxes.Prepend(foregroundBox).ToList(); + allAddresses = allAddresses.Prepend(foregroundBoxAddr).ToList(); + } + + private void DrawStressBox() + { + DrawBox(75, 18, -130, 56); + } + + private void DrawFatigueBox() + { + DrawBox(78, 18, -52, 56); + } + + private void DrawLifeIndexBox() + { + DrawBox(100, 18, 29, 56); + } + + private void Init() + { + nint rootBoxPtrPtr; + // There can be up to 4 pointers stored in an array, where each element + // will point to a linked list of UI elements to draw. + // We're reading the array from behind as a hack to get around an issue + // with the item shop, where if you back out of the item shop + // the linked list that we're interested in would be stored at the back + // of the array because for a split second multiple linked list of UI + // elements are rendered. + for (int i = 3; i >= 0; i--) + { + rootBoxPtrPtr = (nint)Base.Mod.Base.ExeBaseAddress + 0x369900 + 4 * i; + rootBoxPtr = Marshal.ReadInt32(rootBoxPtrPtr); + + if (rootBoxPtr != 0) + { + break; + } + } + + rootBox = Marshal.PtrToStructure(rootBoxPtr); + DrawStressBox(); + DrawFatigueBox(); + DrawLifeIndexBox(); + initialized = true; + } + + private int DrawLifeIndex(nint unk1) + { + if (initialized == false) + { + Init(); + } + + _drawText!(-92, 57, stressTextAddr, 0); + _drawText!(-14, 57, fatigueTextAddr, 0); + _drawText!(74, 57, lifeIndexTextAddr, 0); + + return _loyaltyHook!.OriginalFunction(unk1); + } + #region For Exports, Serialization etc. +#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. + public Mod() { } +#pragma warning restore CS8618 + #endregion + } +} \ No newline at end of file diff --git a/MRDX.Ui.ViewLifeIndex/Preview.png b/MRDX.Ui.ViewLifeIndex/Preview.png new file mode 100644 index 0000000..7cdba80 Binary files /dev/null and b/MRDX.Ui.ViewLifeIndex/Preview.png differ diff --git a/MRDX.Ui.ViewLifeIndex/Publish.ps1 b/MRDX.Ui.ViewLifeIndex/Publish.ps1 new file mode 100644 index 0000000..1188a34 --- /dev/null +++ b/MRDX.Ui.ViewLifeIndex/Publish.ps1 @@ -0,0 +1,408 @@ +<# +.SYNOPSIS + Builds and Publishes a Reloaded II Mod +.DESCRIPTION + Windows script to Build and Publish a Reloaded Mod. + By default, published items will be output to a directory called `Publish/ToUpload`. + + If you acquired this script by creating a new Reloaded Mod in VS. Then most likely everything + (aside from delta updates) should be preconfigured here. + +.PARAMETER ProjectPath + Path to the project to be built. + Useful if using this script from another script for the purpose of building multiple mods. + +.PARAMETER PackageName + Name of the package to be built. + Affects the name of the output files of the publish. + +.PARAMETER PublishOutputDir + Default: "Publish/ToUpload" + Declares the directory for placing the output files. + +.PARAMETER BuildR2R + Default: $False + + Builds the mod using an optimisation called `Ready to Run`, which sacrifices file size for potentially + faster startup time. This is only worth enabling on mods with a lot of code, usually it is best left disabled. + + For more details see: https://docs.microsoft.com/en-us/dotnet/core/deploying/ready-to-run + +.PARAMETER ChangelogPath + Full or relative path to a file containing the changelog for the mod. + The changelog should be written in Markdown format. + +.PARAMETER ReadmePath + Full or relative path to a file containing the changelog for the mod. + The changelog should be written in Markdown format. + +.PARAMETER IsPrerelease + Default: $False + + If set to true, the version downloaded for delta package generation will be the latest pre-release + as opposed to the latest stable version. + +.PARAMETER MakeDelta + Default: $False + + Set to true to create Delta packages. + Usually this is true in a CI/CD environment when creating a release, else false in development. + + If this is true, you should set UseGitHubDelta, UseGameBananaDelta, UseNuGetDelta or equivalent to true. + +.PARAMETER MetadataFileName + Default: Sewer56.Update.ReleaseMetadata.json + Name of the release metadata file used to download the delta package. + +.PARAMETER UseGitHubDelta + Default: $False + If true, sources the last version of the package to publish from GitHub. + +.PARAMETER UseGameBananaDelta + Default: $False + If true, sources the last version of the package to publish from GameBanana. + +.PARAMETER UseNuGetDelta + Default: $False + If true, sources the last version of the package to publish from NuGet. + +.PARAMETER GitHubUserName + [Use if UseGitHubDelta is true] + Sets the username used for obtaining Deltas from GitHub. + +.PARAMETER GitHubRepoName + [Use if UseGitHubDelta is true] + Sets the repository used for obtaining Deltas from GitHub. + +.PARAMETER GitHubFallbackPattern + [Use if UseGitHubDelta is true] + Allows you to specify a Wildcard pattern (e.g. *Update.zip) for the file to be downloaded. + This is a fallback used in cases no Release Metadata file can be found. + +.PARAMETER GitHubInheritVersionFromTag + [Use if UseGitHubDelta is true] + Uses version determined from release tag (in GitHub Releases) as opposed to the + Release Metadata file in latest release. + +.PARAMETER GameBananaItemId + [Use if UseGameBananaDelta is true] + Example: 150118 + + Unique identifier for the individual mod. This is the last number of a GameBanana Mod Page URL + e.g. https://gamebanana.com/mods/150118 -> 150118 + +.PARAMETER NuGetPackageId + [Use if UseNuGetDelta is true] + Example: reloaded.sharedlib.hooks + + The ID of the package to use as delta. + +.PARAMETER NuGetFeedUrl + [Use if UseNuGetDelta is true] + Example: http://packages.sewer56.moe:5000/v3/index.json + + The URL of the NuGet feed to download the delta from. + +.PARAMETER NuGetAllowUnlisted + [Use if UseNuGetDelta is true] + Default: $False + + Allows for the downloading of unlisted packages. + +.PARAMETER PublishGeneric + Default: $True + + Publishes a generic package that can be uploaded to any other website. + +.PARAMETER PublishNuGet + Default: $True + + Publishes a package that can be uploaded to any NuGet Source. + +.PARAMETER PublishGameBanana + Default: $True + + Publishes a package that can be uploaded to GameBanana. + +.PARAMETER Build + Default: $True + + Whether the project should be built. + Setting this to false lets you use the publish part of the script standalone in a non .NET environment. + +.PARAMETER RemoveExe + Default: $True + + Removes executables from build output. Useful when performing R2R Optimisation. + +.PARAMETER UseScriptDirectory + Default: $True + + Uses script directory for performing build. Otherwise uses current directory. + +.EXAMPLE + .\Publish.ps1 -ProjectPath "Reloaded.Hooks.ReloadedII/Reloaded.Hooks.ReloadedII.csproj" -PackageName "Reloaded.Hooks.ReloadedII" -PublishOutputDir "Publish/ToUpload" + +.EXAMPLE + .\Publish.ps1 -MakeDelta true -BuildR2R true -UseGitHubDelta True + +.EXAMPLE + .\Publish.ps1 -BuildR2R true + +#> +[cmdletbinding()] +param ( + $IsPrerelease=$False, + $MakeDelta=$False, + $ChangelogPath="", + $ReadmePath="", + $Build=$True, + $BuildR2R=$False, + $RemoveExe=$True, + $UseScriptDirectory=$True, + + ## => User Config <= ## + $ProjectPath = "MRDX.Ui.ViewLifeIndex.csproj", + $PackageName = "MRDX.Ui.ViewLifeIndex", + $PublishOutputDir = "Publish/ToUpload", + + ## => User: Delta Config + # Pick one and configure settings below. + $MetadataFileName = "Sewer56.Update.ReleaseMetadata.json", + $UseGitHubDelta = $False, # GitHub Releases + $UseGameBananaDelta = $False, + $UseNuGetDelta = $False, + + $GitHubUserName = "", # Name of the GitHub user where the mod is contained + $GitHubRepoName = "", # Name of the GitHub repo where the mod is contained + $GitHubFallbackPattern = "", # For migrating from legacy build script. + $GitHubInheritVersionFromTag = $True, # Uses version determined from release tag as opposed to metadata file in latest release. + + $GameBananaItemId = 333681, # From mod page URL. + + $NuGetPackageId = "MRDX.Ui.ViewLifeIndex", + $NuGetFeedUrl = "http://packages.sewer56.moe:5000/v3/index.json", + $NuGetAllowUnlisted = $False, + + ## => User: Publish Config + $PublishGeneric = $True, + $PublishNuGet = $True, + $PublishGameBanana = $True +) + +## => User: Publish Output +$publishBuildDirectory = "Publish/Builds/CurrentVersion" # Build directory for current version of the mod. +$deltaDirectory = "Publish/Builds/LastVersion" # Path to last version of the mod. + +$PublishGenericDirectory = "$PublishOutputDir/Generic" # Publish files for any target not listed below. +$PublishNuGetDirectory = "$PublishOutputDir/NuGet" # Publish files for NuGet +$PublishGameBananaDirectory = "$PublishOutputDir/GameBanana" # Publish files for GameBanana + +## => User Config <= ## +# Tools +$reloadedToolsPath = "./Publish/Tools/Reloaded-Tools" # Used to check if tools are installed. +$updateToolsPath = "./Publish/Tools/Update-Tools" # Used to check if update tools are installed. +$reloadedToolPath = "$reloadedToolsPath/Reloaded.Publisher.exe" # Path to Reloaded publishing tool. +$updateToolPath = "$updateToolsPath/Sewer56.Update.Tool.dll" # Path to Update tool. +$changelogFullPath = $null +$readmeFullPath = $null +if ($ChangelogPath) { $changelogFullPath = [System.IO.Path]::GetFullPath($ChangelogPath) } +if ($ReadmePath) { $readmeFullPath = [System.IO.Path]::GetFullPath($ReadmePath) } + +## => Script <= ## +# Set Working Directory +$UseScriptDirectory = [bool]::Parse($UseScriptDirectory) +if ($UseScriptDirectory) { + Split-Path $MyInvocation.MyCommand.Path | Push-Location + [Environment]::CurrentDirectory = $PWD +} + +# Convert Booleans +$IsPrerelease = [bool]::Parse($IsPrerelease) +$MakeDelta = [bool]::Parse($MakeDelta) +$Build = [bool]::Parse($Build) +$BuildR2R = [bool]::Parse($BuildR2R) +$RemoveExe = [bool]::Parse($RemoveExe) +$UseGitHubDelta = [bool]::Parse($UseGitHubDelta) +$UseGameBananaDelta = [bool]::Parse($UseGameBananaDelta) +$UseNuGetDelta = [bool]::Parse($UseNuGetDelta) +$NuGetAllowUnlisted = [bool]::Parse($NuGetAllowUnlisted) +$PublishGeneric = [bool]::Parse($PublishGeneric) +$PublishNuGet = [bool]::Parse($PublishNuGet) +$PublishGameBanana = [bool]::Parse($PublishGameBanana) +$GitHubInheritVersionFromTag = [bool]::Parse($GitHubInheritVersionFromTag) +$TempDirectory = [System.IO.Path]::GetTempPath() + [System.IO.Path]::GetRandomFileName() +$TempDirectoryBuild = "$TempDirectory/build" + +function Get-Tools { + # Download Tools (if needed) + $ProgressPreference = 'SilentlyContinue' + if (-not(Test-Path -Path $reloadedToolsPath -PathType Any)) { + Write-Host "Downloading Reloaded Tools" + Invoke-WebRequest -Uri "https://github.com/Reloaded-Project/Reloaded-II/releases/latest/download/Tools.zip" -OutFile "$TempDirectory/Tools.zip" + Expand-Archive -LiteralPath "$TempDirectory/Tools.zip" -DestinationPath $reloadedToolsPath + + # Remove Items + Remove-Item "$TempDirectory/Tools.zip" -ErrorAction SilentlyContinue + } + + if ($MakeDelta -and -not(Test-Path -Path $updateToolsPath -PathType Any)) { + Write-Host "Downloading Update Library Tools" + Invoke-WebRequest -Uri "https://github.com/Sewer56/Update/releases/latest/download/Sewer56.Update.Tool.zip" -OutFile "$TempDirectory/Sewer56.Update.Tool.zip" + Expand-Archive -LiteralPath "$TempDirectory/Sewer56.Update.Tool.zip" -DestinationPath $updateToolsPath + + # Remove Items + Remove-Item "$TempDirectory/Sewer56.Update.Tool.zip" -ErrorAction SilentlyContinue + } +} + +# Publish for targets +function Build { + # Clean anything in existing Release directory. + Remove-Item $publishBuildDirectory -Recurse -ErrorAction SilentlyContinue + New-Item $publishBuildDirectory -ItemType Directory -ErrorAction SilentlyContinue + + # Build + dotnet restore $ProjectPath + dotnet clean $ProjectPath + + if ($BuildR2R) { + dotnet publish $ProjectPath -c Release -r win-x86 --self-contained false -o "$publishBuildDirectory/x86" /p:PublishReadyToRun=true /p:OutputPath="$TempDirectoryBuild/x86" + dotnet publish $ProjectPath -c Release -r win-x64 --self-contained false -o "$publishBuildDirectory/x64" /p:PublishReadyToRun=true /p:OutputPath="$TempDirectoryBuild/x64" + + # Remove Redundant Files + Move-Item -Path "$publishBuildDirectory/x86/ModConfig.json" -Destination "$publishBuildDirectory/ModConfig.json" -ErrorAction SilentlyContinue + Move-Item -Path "$publishBuildDirectory/x86/Preview.png" -Destination "$publishBuildDirectory/Preview.png" -ErrorAction SilentlyContinue + Remove-Item "$publishBuildDirectory/x64/Preview.png" -ErrorAction SilentlyContinue + Remove-Item "$publishBuildDirectory/x64/ModConfig.json" -ErrorAction SilentlyContinue + } + else { + dotnet publish $ProjectPath -c Release --self-contained false -o "$publishBuildDirectory" /p:OutputPath="$TempDirectoryBuild" + } + + # Cleanup Unnecessary Files + Remove-Item $TempDirectoryBuild -Recurse -ErrorAction SilentlyContinue + if ($RemoveExe) { + Get-ChildItem $publishBuildDirectory -Include *.exe -Recurse | Remove-Item -Force -Recurse + } + + Get-ChildItem $publishBuildDirectory -Include *.pdb -Recurse | Remove-Item -Force -Recurse + Get-ChildItem $publishBuildDirectory -Include *.xml -Recurse | Remove-Item -Force -Recurse +} + +function Get-Last-Version { + + Remove-Item $deltaDirectory -Recurse -ErrorAction SilentlyContinue + New-Item $deltaDirectory -ItemType Directory -ErrorAction SilentlyContinue + $arguments = "DownloadPackage --extract --outputpath `"$deltaDirectory`" --allowprereleases `"$IsPrerelease`" --metadatafilename `"$MetadataFileName`"" + + if ($UseGitHubDelta) { + $arguments += " --source GitHub --githubusername `"$GitHubUserName`" --githubrepositoryname `"$GitHubRepoName`" --githublegacyfallbackpattern `"$GitHubFallbackPattern`" --githubinheritversionfromtag `"$GitHubInheritVersionFromTag`"" + } + elseif ($UseNuGetDelta) { + $arguments += " --source NuGet --nugetpackageid `"$NuGetPackageId`" --nugetfeedurl `"$NuGetFeedUrl`" --nugetallowunlisted `"$NuGetAllowUnlisted`"" + } + elseif ($UseGameBananaDelta) { + $arguments += " --source GameBanana --gamebananaitemid `"$GameBananaItemId`"" + } + + Invoke-Expression "dotnet `"$updateToolPath`" $arguments" +} + +function Get-Common-Publish-Args { + + param ( + $AllowDeltas=$True + ) + + $arguments = "--modfolder `"$publishBuildDirectory`" --packagename `"$PackageName`"" + if ($ChangelogPath) { + $arguments += " --changelogpath `"$changelogFullPath`"" + } + + if ($ReadmePath) { + $arguments += " --readmepath `"$readmeFullPath`"" + } + + if ($AllowDeltas -and $MakeDelta) { + $arguments += " --olderversionfolders `"$deltaDirectory`"" + } + + return $arguments +} + +function Publish-Common { + + param ( + $Directory="", + $AllowDeltas=$True, + $PublishTarget="" + ) + + Remove-Item $Directory -Recurse -ErrorAction SilentlyContinue + New-Item $Directory -ItemType Directory -ErrorAction SilentlyContinue + $arguments = "$(Get-Common-Publish-Args -AllowDeltas $AllowDeltas) --outputfolder `"$Directory`" --publishtarget $PublishTarget" + $command = "$reloadedToolPath $arguments" + Write-Host "$command`r`n`r`n" + Invoke-Expression $command +} + +function Publish-GameBanana { + Publish-Common -Directory $PublishGameBananaDirectory -PublishTarget GameBanana +} + +function Publish-NuGet { + Publish-Common -Directory $PublishNuGetDirectory -PublishTarget NuGet -AllowDeltas $False +} + +function Publish-Generic { + Publish-Common -Directory $PublishGenericDirectory -PublishTarget Default +} + +function Cleanup { + Remove-Item $PublishOutputDir -Recurse -ErrorAction SilentlyContinue + Remove-Item $PublishNuGetDirectory -Recurse -ErrorAction SilentlyContinue + Remove-Item $PublishGenericDirectory -Recurse -ErrorAction SilentlyContinue + Remove-Item $publishBuildDirectory -Recurse -ErrorAction SilentlyContinue + Remove-Item $deltaDirectory -Recurse -ErrorAction SilentlyContinue +} + +# Build & Publish +New-Item $TempDirectory -ItemType Directory -ErrorAction SilentlyContinue +Cleanup +Get-Tools + +if ($MakeDelta) { + Write-Host "Downloading Delta (Last Version)" + Get-Last-Version +} + +if ($Build) { + Write-Host "Building Mod" + Build +} + +if ($PublishGeneric) { + Write-Host "Publishing Mod for Default Target" + Publish-Generic +} + +if ($PublishNuGet) { + Write-Host "Publishing Mod for NuGet Target" + Publish-NuGet +} + +if ($PublishGameBanana) { + Write-Host "Publishing Mod for GameBanana Target" + Publish-GameBanana +} + +# Remove Temp Folder +Remove-Item $TempDirectory -Recurse -ErrorAction SilentlyContinue + +# Restore Working Directory +Write-Host "Done." +Write-Host "Upload the files in folder `"$PublishOutputDir`" to respective location or website." +if ($UseScriptDirectory) { + Pop-Location +} \ No newline at end of file diff --git a/MRDX.Ui.ViewLifeIndex/Reloaded.Checks.targets b/MRDX.Ui.ViewLifeIndex/Reloaded.Checks.targets new file mode 100644 index 0000000..71a17e8 --- /dev/null +++ b/MRDX.Ui.ViewLifeIndex/Reloaded.Checks.targets @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/MRDX.Ui.ViewLifeIndex/Reloaded.Trimming.targets b/MRDX.Ui.ViewLifeIndex/Reloaded.Trimming.targets new file mode 100644 index 0000000..b4a56d8 --- /dev/null +++ b/MRDX.Ui.ViewLifeIndex/Reloaded.Trimming.targets @@ -0,0 +1,149 @@ + + + + + + + + false + + + false + + + + + + + + + + + + + <__PDBToLink Include="@(ResolvedFileToPublish)" Exclude="@(ManagedAssemblyToLink->'%(RelativeDir)%(Filename).pdb')" /> + <_PDBToLink Include="@(ResolvedFileToPublish)" Exclude="@(__PDBToLink)" /> + + + + <_LinkedResolvedFileToPublishCandidate Include="@(ManagedAssemblyToLink->'$(IntermediateLinkDir)%(Filename)%(Extension)')" /> + <_LinkedResolvedFileToPublishCandidate Include="@(_PDBToLink->'$(IntermediateLinkDir)%(Filename)%(Extension)')" /> + + + + + + + true + + + + + link + + copy + $(TreatWarningsAsErrors) + <_ExtraTrimmerArgs>--skip-unresolved true $(_ExtraTrimmerArgs) + true + + + + + + + + + + + + + + + + + + + + + $(TrimmerDefaultAction) + + + + $(TrimMode) + + + + + + + + Input Assembly: %(filename) [Mode: %(ManagedAssemblyToLink.TrimMode)] + + + + + + + + + + + + + + + + <_LinkedResolvedFileToPublish Include="@(_LinkedResolvedFileToPublishCandidate)" Condition="Exists('%(Identity)')" /> + + + + + + + \ No newline at end of file diff --git a/MRDX.Ui.ViewLifeIndex/Template/ModBase.cs b/MRDX.Ui.ViewLifeIndex/Template/ModBase.cs new file mode 100644 index 0000000..f3aeb82 --- /dev/null +++ b/MRDX.Ui.ViewLifeIndex/Template/ModBase.cs @@ -0,0 +1,65 @@ + +namespace MRDX.Ui.ViewLifeIndex.Template +{ + /// + /// Base class for implementing mod functionality. + /// + public class ModBase + { + /// + /// Returns true if the suspend functionality is supported, else false. + /// + public virtual bool CanSuspend() => false; + + /// + /// Returns true if the unload functionality is supported, else false. + /// + public virtual bool CanUnload() => false; + + /// + /// Suspends your mod, i.e. mod stops performing its functionality but is not unloaded. + /// + public virtual void Suspend() + { + /* Some tips if you wish to support this (CanSuspend == true) + + A. Undo memory modifications. + B. Deactivate hooks. (Reloaded.Hooks Supports This!) + */ + } + + /// + /// Unloads your mod, i.e. mod stops performing its functionality but is not unloaded. + /// + /// In most cases, calling suspend here is sufficient. + public virtual void Unload() + { + /* Some tips if you wish to support this (CanUnload == true). + + A. Execute Suspend(). [Suspend should be reusable in this method] + B. Release any unmanaged resources, e.g. Native memory. + */ + } + + /// + /// Automatically called by the mod loader when the mod is about to be unloaded. + /// + public virtual void Disposing() + { + + } + + /// + /// Automatically called by the mod loader when the mod is about to be unloaded. + /// + public virtual void Resume() + { + /* Some tips if you wish to support this (CanSuspend == true) + + A. Redo memory modifications. + B. Re-activate hooks. (Reloaded.Hooks Supports This!) + */ + } + + } +} \ No newline at end of file diff --git a/MRDX.Ui.ViewLifeIndex/Template/ModContext.cs b/MRDX.Ui.ViewLifeIndex/Template/ModContext.cs new file mode 100644 index 0000000..b06243d --- /dev/null +++ b/MRDX.Ui.ViewLifeIndex/Template/ModContext.cs @@ -0,0 +1,36 @@ +using Reloaded.Mod.Interfaces; +using IReloadedHooks = Reloaded.Hooks.ReloadedII.Interfaces.IReloadedHooks; + +namespace MRDX.Ui.ViewLifeIndex.Template +{ + /// + /// Represents information passed in from the mod loader template to the implementing mod. + /// + public class ModContext + { + /// + /// Provides access to the mod loader API. + /// + public IModLoader ModLoader { get; set; } = null!; + + /// + /// Provides access to the Reloaded.Hooks API. + /// + public IReloadedHooks? Hooks { get; set; } = null!; + + /// + /// Provides access to the Reloaded logger. + /// + public ILogger Logger { get; set; } = null!; + + /// + /// Configuration of this mod. + /// + public IModConfig ModConfig { get; set; } = null!; + + /// + /// Instance of the IMod interface that created this mod instance. + /// + public IMod Owner { get; set; } = null!; + } +} \ No newline at end of file diff --git a/MRDX.Ui.ViewLifeIndex/Template/Startup.cs b/MRDX.Ui.ViewLifeIndex/Template/Startup.cs new file mode 100644 index 0000000..064ab15 --- /dev/null +++ b/MRDX.Ui.ViewLifeIndex/Template/Startup.cs @@ -0,0 +1,77 @@ +/* + * This file and other files in the `Template` folder are intended to be left unedited (if possible), + * to make it easier to upgrade to newer versions of the template. +*/ + +using Reloaded.Hooks.ReloadedII.Interfaces; +using Reloaded.Mod.Interfaces; +using Reloaded.Mod.Interfaces.Internal; + +namespace MRDX.Ui.ViewLifeIndex.Template +{ + public class Startup : IMod + { + /// + /// Used for writing text to the Reloaded log. + /// + private ILogger _logger = null!; + + /// + /// Provides access to the mod loader API. + /// + private IModLoader _modLoader = null!; + + + /// + /// An interface to Reloaded's the function hooks/detours library. + /// See: https://github.com/Reloaded-Project/Reloaded.Hooks + /// for documentation and samples. + /// + private IReloadedHooks? _hooks; + + /// + /// Configuration of the current mod. + /// + private IModConfig _modConfig = null!; + + /// + /// Encapsulates your mod logic. + /// + private ModBase _mod = new Mod(); + + /// + /// Entry point for your mod. + /// + public void StartEx(IModLoaderV1 loaderApi, IModConfigV1 modConfig) + { + _modLoader = (IModLoader)loaderApi; + _modConfig = (IModConfig)modConfig; + _logger = (ILogger)_modLoader.GetLogger(); + _modLoader.GetController()?.TryGetTarget(out _hooks!); + + // Please put your mod code in the class below, + // use this class for only interfacing with mod loader. + _mod = new Mod(new ModContext() + { + Logger = _logger, + Hooks = _hooks, + ModLoader = _modLoader, + ModConfig = _modConfig, + Owner = this, + }); + } + /* Mod loader actions. */ + public void Suspend() => _mod.Suspend(); + public void Resume() => _mod.Resume(); + public void Unload() => _mod.Unload(); + + /* If CanSuspend == false, suspend and resume button are disabled in Launcher and Suspend()/Resume() will never be called. + If CanUnload == false, unload button is disabled in Launcher and Unload() will never be called. + */ + public bool CanUnload() => _mod.CanUnload(); + public bool CanSuspend() => _mod.CanSuspend(); + + /* Automatically called by the mod loader when the mod is about to be unloaded. */ + public Action Disposing => () => _mod.Disposing(); + } +} \ No newline at end of file diff --git a/mrdx_reloaded.sln b/mrdx_reloaded.sln index 8b541b8..b7058ea 100644 --- a/mrdx_reloaded.sln +++ b/mrdx_reloaded.sln @@ -23,11 +23,13 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MRDX.Base.Mod", "MRDX.Base. EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MRDX.Game.MonsterEditor", "MRDX.Game.MonsterEditor\MRDX.Game.MonsterEditor.csproj", "{B684E5AF-F799-4417-BD7D-F5FE089DC011}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MRDX.Ui.RawTechValues", "MRDX.Ui.RawTechValues\MRDX.Ui.RawTechValues.csproj", "{F318C5F6-4AF7-467D-B877-927AED7F02AA}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MRDX.Ui.RawTechValues", "MRDX.Ui.RawTechValues\MRDX.Ui.RawTechValues.csproj", "{F318C5F6-4AF7-467D-B877-927AED7F02AA}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MRDX.Qol.TurboInput", "MRDX.Qol.TurboInput\MRDX.Qol.TurboInput.csproj", "{E23A6883-BCDF-4386-87F7-49DCE0085E9B}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MRDX.Qol.TurboInput", "MRDX.Qol.TurboInput\MRDX.Qol.TurboInput.csproj", "{E23A6883-BCDF-4386-87F7-49DCE0085E9B}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MRDX.Qol.BattleTimer", "MRDX.Qol.BattleTimer\MRDX.Qol.BattleTimer.csproj", "{9F9B2A4D-0C39-473B-9CCA-09EF5854F697}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MRDX.Qol.BattleTimer", "MRDX.Qol.BattleTimer\MRDX.Qol.BattleTimer.csproj", "{9F9B2A4D-0C39-473B-9CCA-09EF5854F697}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MRDX.Ui.ViewLifeIndex", "MRDX.Ui.ViewLifeIndex\MRDX.Ui.ViewLifeIndex.csproj", "{9C0AF1A7-4D8B-414F-AC5B-5FC34C6CFB6A}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -75,18 +77,22 @@ Global {B684E5AF-F799-4417-BD7D-F5FE089DC011}.Debug|Any CPU.Build.0 = Debug|Any CPU {B684E5AF-F799-4417-BD7D-F5FE089DC011}.Release|Any CPU.ActiveCfg = Release|Any CPU {B684E5AF-F799-4417-BD7D-F5FE089DC011}.Release|Any CPU.Build.0 = Release|Any CPU - {E23A6883-BCDF-4386-87F7-49DCE0085E9B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {E23A6883-BCDF-4386-87F7-49DCE0085E9B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {E23A6883-BCDF-4386-87F7-49DCE0085E9B}.Release|Any CPU.ActiveCfg = Release|Any CPU - {E23A6883-BCDF-4386-87F7-49DCE0085E9B}.Release|Any CPU.Build.0 = Release|Any CPU {F318C5F6-4AF7-467D-B877-927AED7F02AA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {F318C5F6-4AF7-467D-B877-927AED7F02AA}.Debug|Any CPU.Build.0 = Debug|Any CPU {F318C5F6-4AF7-467D-B877-927AED7F02AA}.Release|Any CPU.ActiveCfg = Release|Any CPU {F318C5F6-4AF7-467D-B877-927AED7F02AA}.Release|Any CPU.Build.0 = Release|Any CPU + {E23A6883-BCDF-4386-87F7-49DCE0085E9B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E23A6883-BCDF-4386-87F7-49DCE0085E9B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E23A6883-BCDF-4386-87F7-49DCE0085E9B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E23A6883-BCDF-4386-87F7-49DCE0085E9B}.Release|Any CPU.Build.0 = Release|Any CPU {9F9B2A4D-0C39-473B-9CCA-09EF5854F697}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {9F9B2A4D-0C39-473B-9CCA-09EF5854F697}.Debug|Any CPU.Build.0 = Debug|Any CPU {9F9B2A4D-0C39-473B-9CCA-09EF5854F697}.Release|Any CPU.ActiveCfg = Release|Any CPU {9F9B2A4D-0C39-473B-9CCA-09EF5854F697}.Release|Any CPU.Build.0 = Release|Any CPU + {9C0AF1A7-4D8B-414F-AC5B-5FC34C6CFB6A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9C0AF1A7-4D8B-414F-AC5B-5FC34C6CFB6A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9C0AF1A7-4D8B-414F-AC5B-5FC34C6CFB6A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9C0AF1A7-4D8B-414F-AC5B-5FC34C6CFB6A}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE