Skip to content

Commit a8ca378

Browse files
committed
MSBuild task for compressing resources
1 parent ad69944 commit a8ca378

15 files changed

Lines changed: 506 additions & 68 deletions

File tree

.github/workflows/build.yml

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ on:
1010
- BloonsTD6 Mod Helper/**
1111
- UpdaterPlugin/**
1212
- SourceGenerators/**
13+
- MSBuildTasks/**
1314
- .github/workflows/build.yml
1415
pull_request:
1516
branches:
@@ -31,7 +32,6 @@ env:
3132
BLOONSTD6: ./BloonsTD6
3233
MOD_HELPER_NAME: Btd6ModHelper
3334
UPDATER_PLUGIN_NAME: UpdaterPlugin
34-
SOURCE_GENERATORS_NAME: SourceGenerators
3535

3636
IS_RELEASE: ${{ startsWith(github.ref, 'refs/tags/') || (github.event_name == 'workflow_dispatch' && inputs.tag != '') }}
3737
RELEASE_TAG: ${{ inputs.tag || (startsWith(github.ref, 'refs/tags/') && github.ref_name) || '' }}
@@ -86,8 +86,8 @@ jobs:
8686
with:
8787
dotnet-version: 10.0.x
8888

89-
- name: Build Mod
90-
run: dotnet build -c Release -p:BloonsTD6="../${{ env.BLOONSTD6 }}" "${{ env.PROJECT_NAME }}/${{ env.PROJECT_NAME }}.csproj"
89+
- name: Build Solution
90+
run: dotnet build "BTD Mod Helper.sln" -c Release -p:BloonsTD6="${{ github.workspace }}/BloonsTD6"
9191

9292
- name: Upload Mod DLL
9393
uses: actions/upload-artifact@v7
@@ -101,23 +101,18 @@ jobs:
101101
name: ${{ env.MOD_HELPER_NAME }}.xml
102102
path: ${{ env.BLOONSTD6 }}/Mods/${{ env.MOD_HELPER_NAME }}.xml
103103

104-
- name: Build Plugin
105-
run: dotnet build -c Release -p:BloonsTD6="../${{ env.BLOONSTD6 }}" "${{ env.UPDATER_PLUGIN_NAME }}/${{ env.UPDATER_PLUGIN_NAME }}.csproj"
106-
107104
- name: Upload Plugin DLL
108105
uses: actions/upload-artifact@v7
109106
with:
110107
name: ${{ env.UPDATER_PLUGIN_NAME }}.dll
111108
path: ${{ env.BLOONSTD6 }}/Plugins/${{ env.UPDATER_PLUGIN_NAME }}.dll
112109

113-
- name: Build Source Generators
114-
run: dotnet build -c Release -p:BloonsTD6="../${{ env.BLOONSTD6 }}" "${{ env.SOURCE_GENERATORS_NAME }}/${{ env.SOURCE_GENERATORS_NAME }}.csproj"
115-
116-
- name: Publish Source Generators
110+
- name: Publish Nuget Packages
117111
if: env.IS_RELEASE == 'true' && env.ACT != 'true'
118112
env:
119113
NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }}
120-
run: dotnet nuget push "./${{ env.SOURCE_GENERATORS_NAME }}/bin/Release/*.nupkg" --api-key "$NUGET_API_KEY" --source nuget.org --skip-duplicate
114+
run: dotnet nuget push "./*/bin/Release/*.nupkg" --api-key "$NUGET_API_KEY" --source nuget.org --skip-duplicate
115+
121116

122117
- name: Process description input
123118
if: env.IS_RELEASE == 'true' && env.RELEASE_DESC != ''
@@ -173,6 +168,7 @@ jobs:
173168
echo "RELEASE_SHA=$(git rev-parse HEAD)" >> "$GITHUB_ENV"
174169
fi
175170
171+
176172
- name: Download Template Mod Zip
177173
if: env.IS_RELEASE == 'true'
178174
run: curl -L "https://github.com/doombubbles/btd6-template-mod/archive/refs/heads/main.zip" -o "${{ env.PROJECT_NAME }}/Resources/btd6-template-mod-main.zip"

BTD Mod Helper.sln

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestRunner", "TestRunner\Te
1111
EndProject
1212
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SourceGenerators", "SourceGenerators\SourceGenerators.csproj", "{B4BF81E7-459F-44B9-849D-E52103677775}"
1313
EndProject
14+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MSBuildTasks", "MSBuildTasks\MSBuildTasks.csproj", "{4FBEFA37-221C-4C0E-B8C8-F9A9361ED2E3}"
15+
EndProject
1416
Global
1517
GlobalSection(SolutionConfigurationPlatforms) = preSolution
1618
Debug|Any CPU = Debug|Any CPU
@@ -38,6 +40,11 @@ Global
3840
{B4BF81E7-459F-44B9-849D-E52103677775}.Release|Any CPU.ActiveCfg = Release|Any CPU
3941
{B4BF81E7-459F-44B9-849D-E52103677775}.Release|Any CPU.Build.0 = Release|Any CPU
4042
{B4BF81E7-459F-44B9-849D-E52103677775}.ReleaseLite|Any CPU.ActiveCfg = Release|Any CPU
43+
{4FBEFA37-221C-4C0E-B8C8-F9A9361ED2E3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
44+
{4FBEFA37-221C-4C0E-B8C8-F9A9361ED2E3}.Debug|Any CPU.Build.0 = Debug|Any CPU
45+
{4FBEFA37-221C-4C0E-B8C8-F9A9361ED2E3}.Release|Any CPU.ActiveCfg = Release|Any CPU
46+
{4FBEFA37-221C-4C0E-B8C8-F9A9361ED2E3}.Release|Any CPU.Build.0 = Release|Any CPU
47+
{4FBEFA37-221C-4C0E-B8C8-F9A9361ED2E3}.ReleaseLite|Any CPU.ActiveCfg = Release|Any CPU
4148
EndGlobalSection
4249
GlobalSection(SolutionProperties) = preSolution
4350
HideSolutionNode = FALSE

BloonsTD6 Mod Helper/Api/Internal/ResourceHandler.cs

Lines changed: 122 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
11
using System;
22
using System.Buffers;
33
using System.Collections.Generic;
4+
using System.IO;
5+
using System.IO.Compression;
46
using System.Linq;
7+
using System.Reflection;
58
using BTD_Mod_Helper.Api.UI;
69
using Il2CppAssets.Scripts.Unity.Audio;
710
using Il2CppNinjaKiwi.Common.ResourceUtils;
8-
using Il2CppSystem.IO;
911
using NAudio.Vorbis;
1012
using NAudio.Wave;
1113
using UnityEngine;
1214
using UnityEngine.AddressableAssets;
13-
using Stream = System.IO.Stream;
1415
namespace BTD_Mod_Helper.Api.Internal;
1516

1617
/// <summary>
@@ -67,22 +68,64 @@ public static class ResourceHandler
6768

6869
internal static void LoadEmbeddedResources(BloonsMod mod)
6970
{
70-
LoadEmbeddedTextures(mod);
71-
LoadEmbeddedAudio(mod);
72-
LoadEmbeddedBundles(mod);
71+
var zipArchives = LoadEmbeddedZipArchives(mod);
72+
try
73+
{
74+
var zipEntries = zipArchives
75+
.SelectMany(archive => archive.Entries)
76+
.Where(entry => !string.IsNullOrEmpty(entry.Name))
77+
.ToList();
78+
79+
LoadEmbeddedTextures(mod, zipEntries);
80+
LoadEmbeddedAudio(mod, zipEntries);
81+
LoadEmbeddedBundles(mod, zipEntries);
82+
}
83+
finally
84+
{
85+
foreach (var zipArchive in zipArchives)
86+
{
87+
zipArchive.Dispose();
88+
}
89+
}
7390
}
7491

75-
internal static void LoadEmbeddedTextures(BloonsMod mod)
92+
private static List<ZipArchive> LoadEmbeddedZipArchives(BloonsMod mod)
93+
{
94+
var assembly = mod.GetAssembly();
95+
var archives = new List<ZipArchive>();
96+
97+
foreach (var zipName in assembly.GetManifestResourceNames().Where(s => s.EndsWith(".zip")))
98+
{
99+
Stream zipStream = null;
100+
try
101+
{
102+
zipStream = assembly.GetManifestResourceStream(zipName);
103+
if (zipStream == null) continue;
104+
105+
archives.Add(new ZipArchive(zipStream, ZipArchiveMode.Read));
106+
zipStream = null;
107+
}
108+
catch (Exception e)
109+
{
110+
zipStream?.Dispose();
111+
ModHelper.Warning("Failed to load embedded resource zip " + zipName);
112+
ModHelper.Warning(e);
113+
}
114+
}
115+
116+
return archives;
117+
}
118+
119+
internal static void LoadEmbeddedTextures(BloonsMod mod, IEnumerable<ZipArchiveEntry> zipEntries = null)
76120
{
77121
mod.Resources = new Dictionary<string, byte[]>();
78-
foreach (var fileName in mod.GetAssembly().GetManifestResourceNames().Where(s => ImageExtensions.Any(s.EndsWith)))
122+
123+
foreach (var (name, ext, stream) in GetEmbeddedResourceStreams(
124+
mod.GetAssembly(), zipEntries, ImageExtensions))
79125
{
80-
var resource = mod.GetAssembly().GetManifestResourceStream(fileName).GetByteArray();
126+
var resource = stream.GetByteArray();
81127
if (resource == null) continue;
82128

83-
var split = fileName.Split('.');
84-
var name = split[^2];
85-
var ext = split[^1];
86129
var id = ModContent.GetId(mod, name);
87130
Resources[id] = resource;
88131
mod.Resources[name] = resource;
@@ -91,21 +134,18 @@ internal static void LoadEmbeddedTextures(BloonsMod mod)
91134
}
92135
}
93136

94-
internal static void LoadEmbeddedAudio(BloonsMod mod)
137+
internal static void LoadEmbeddedAudio(BloonsMod mod, IEnumerable<ZipArchiveEntry> zipEntries = null)
95138
{
96139
mod.AudioClips = new Dictionary<string, AudioClip>();
97140

98-
foreach (var fileName in mod.GetAssembly().GetManifestResourceNames().Where(s => AudioExtensions.Any(s.EndsWith)))
141+
foreach (var (name, ext, stream) in GetEmbeddedResourceStreams(
142+
mod.GetAssembly(), zipEntries, AudioExtensions))
99143
{
100-
var split = fileName.Split('.');
101-
var extension = split[^1];
102-
var name = split[^2];
103144
var id = ModContent.GetId(mod, name);
104145

105146
try
106147
{
107-
using var stream = mod.GetAssembly().GetManifestResourceStream(fileName)!;
108-
using var waveStream = GetWaveStream(stream, extension);
148+
using var waveStream = GetWaveStream(stream, ext);
109149

110150
if (mod.NormalizeAllAudioVolume)
111151
{
@@ -120,49 +160,85 @@ internal static void LoadEmbeddedAudio(BloonsMod mod)
120160
}
121161
catch (Exception e)
122162
{
123-
ModHelper.Warning("Failed to load audio clip " + fileName);
163+
ModHelper.Warning($"Failed to load audio clip {name}.{ext}");
124164
ModHelper.Warning(e);
125165
}
126166
}
127167
}
128168

129-
internal static void LoadEmbeddedBundles(BloonsMod mod)
169+
internal static void LoadEmbeddedBundles(BloonsMod mod, IEnumerable<ZipArchiveEntry> zipEntries = null)
130170
{
131-
foreach (var name in mod.GetAssembly().GetManifestResourceNames().Where(s => s.EndsWith("bundle")))
171+
foreach (var (name, _, stream) in GetEmbeddedResourceStreams(mod.GetAssembly(), zipEntries, [".bundle"]))
132172
{
133-
var bytes = mod.GetAssembly().GetManifestResourceStream(name).GetByteArray();
173+
var bytes = stream.GetByteArray();
134174
if (bytes == null) continue;
135175

136-
var stream = new MemoryStream(bytes);
137-
var bundle = AssetBundle.LoadFromStream(stream);
138-
stream.Dispose();
139-
var guid = mod.IDPrefix;
140-
if (bundle == null)
141-
{
142-
ModHelper.Log($"The bundle {name} is null!");
143-
continue;
144-
}
176+
LoadBundle(mod, name, bytes);
177+
}
178+
}
145179

146-
if (string.IsNullOrEmpty(bundle.name))
147-
{
148-
ModHelper.Log($"The bundle {name} has no name!");
149-
continue;
150-
}
180+
private static void LoadBundle(BloonsMod mod, string name, byte[] bytes)
181+
{
182+
var stream = new Il2CppSystem.IO.MemoryStream(bytes);
183+
var bundle = AssetBundle.LoadFromStream(stream);
184+
stream.Dispose();
185+
var guid = mod.IDPrefix;
186+
if (bundle == null)
187+
{
188+
ModHelper.Log($"The bundle {name} is null!");
189+
return;
190+
}
151191

152-
if (bundle.name.EndsWith(".bundle"))
153-
{
154-
guid += bundle.name.Substring(0, bundle.name.LastIndexOf(".", StringComparison.Ordinal));
155-
}
156-
else
157-
{
158-
guid += bundle.name;
159-
}
192+
if (string.IsNullOrEmpty(bundle.name))
193+
{
194+
ModHelper.Log($"The bundle {name} has no name!");
195+
return;
196+
}
197+
198+
if (bundle.name.EndsWith(".bundle"))
199+
{
200+
guid += bundle.name.Substring(0, bundle.name.LastIndexOf(".", StringComparison.Ordinal));
201+
}
202+
else
203+
{
204+
guid += bundle.name;
205+
}
206+
207+
Bundles[guid] = bundle;
208+
// ModHelper.Msg("Successfully loaded bundle " + guid);
209+
}
210+
211+
private static IEnumerable<(string Name, string Ext, Stream Stream)> GetEmbeddedResourceStreams(
212+
Assembly assembly, IEnumerable<ZipArchiveEntry> zipEntries, string[] extensions)
213+
{
214+
foreach (var fileName in assembly.GetManifestResourceNames().Where(s => extensions.Any(s.EndsWith)))
215+
{
216+
using var stream = assembly.GetManifestResourceStream(fileName);
217+
if (stream == null) continue;
160218

161-
Bundles[guid] = bundle;
162-
// ModHelper.Msg("Successfully loaded bundle " + guid);
219+
var split = fileName.Split('.');
220+
yield return (split[^2], split[^1], stream);
221+
}
222+
223+
if (zipEntries == null) yield break;
224+
225+
foreach (var entry in zipEntries.Where(entry => extensions.Any(entry.Name.EndsWith)))
226+
{
227+
using var stream = entry.Open();
228+
var fileName = GetZipEntryFileName(entry);
229+
yield return (
230+
Path.GetFileNameWithoutExtension(fileName),
231+
Path.GetExtension(fileName).TrimStart('.'),
232+
stream);
163233
}
164234
}
165235

236+
private static string GetZipEntryFileName(ZipArchiveEntry entry)
237+
{
238+
var normalized = entry.FullName.Replace('\\', '/');
239+
var index = normalized.LastIndexOf('/');
240+
return index >= 0 ? normalized[(index + 1)..] : normalized;
241+
}
166242

167243
/// <summary>
168244
/// Turns a stream into a WaveStream based on file extension

BloonsTD6 Mod Helper/Api/ModByteLoader.cs

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System.Collections.Generic;
22
using System.IO;
3+
using System.IO.Compression;
34
using System.Text.RegularExpressions;
45
using BTD_Mod_Helper.Api.Helpers;
56
using Il2CppInterop.Runtime;
@@ -125,19 +126,17 @@ internal void LoadAllBytes()
125126
/// <returns></returns>
126127
public sealed override IEnumerable<ModContent> Load()
127128
{
128-
var streamName = Array.Find(mod.GetAssembly().GetManifestResourceNames(), s => s.EndsWith(BytesFileName));
129-
if (streamName == null)
129+
if (!mod.GetAssembly().TryGetEmbeddedResource(BytesFileName, out var bytesStream))
130130
{
131131
ModHelper.Warning($"Couldn't find bytes file {BytesFileName} in Assembly {mod.GetModName()}. " +
132132
"Did you forget to include it as an embedded resource?");
133+
yield break;
133134
}
134-
else
135+
136+
Bytes = bytesStream.GetByteArray();
137+
if (Bytes != null)
135138
{
136-
Bytes = mod.GetAssembly().GetManifestResourceStream(streamName)?.GetByteArray();
137-
if (Bytes != null)
138-
{
139-
yield return this;
140-
}
139+
yield return this;
141140
}
142141
}
143142

BloonsTD6 Mod Helper/Api/Towers/ModTower.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -350,6 +350,13 @@ public virtual void OnUpgraded(Tower tower, ModUpgrade upgrade)
350350
{
351351
}
352352

353+
/// <summary>
354+
/// Allows you to perform one-time actions in game when the tower is first placed
355+
/// </summary>
356+
public virtual void OnPlaced(Tower tower)
357+
{
358+
}
359+
353360
/// <summary>
354361
/// Gets the portrait reference this tower should use for the given tiers
355362
/// <br />

BloonsTD6 Mod Helper/BloonsTD6Mod.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -504,6 +504,13 @@ public virtual void OnTowerUpgraded(Tower tower, string upgradeName, TowerModel
504504
{
505505
}
506506

507+
/// <summary>
508+
/// Called right after a Tower is placed
509+
/// </summary>
510+
public virtual void OnTowerPlaced(Tower tower)
511+
{
512+
}
513+
507514
/// <summary>
508515
/// Called right after a Tower's TowerModel is changed for any reason (creation, upgrading, etc.)
509516
/// <br />

BloonsTD6 Mod Helper/Extensions/ModelExtensions/TowerModelExt.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,10 @@ public static void SetDisplay(this TowerModel towerModel, PrefabReference displa
186186
{
187187
displayModel.display = display;
188188
}
189+
else
190+
{
191+
towerModel.AddBehavior(new DisplayModel("TowerDisplay", display, 0, DisplayCategory.Tower));
192+
}
189193
}
190194

191195
/// <summary>

0 commit comments

Comments
 (0)