Skip to content

Commit c8a911c

Browse files
authored
Merge pull request #80 from Neinndall/dev
HotFix Update v4.0.1.2
2 parents 961d75b + 0b994dd commit c8a911c

23 files changed

Lines changed: 705 additions & 254 deletions

AssetsManager/AssetsManager.csproj

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
<OutputType>WinExe</OutputType>
44
<TargetFramework>net10.0-windows</TargetFramework>
55
<ApplicationIcon>Resources\Img\logo.ico</ApplicationIcon>
6-
<Version>4.0.1.1</Version>
6+
<Version>4.0.1.2</Version>
77
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
88
<UseWPF>true</UseWPF>
99
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
@@ -17,7 +17,6 @@
1717
<PackageReference Include="Blake3" Version="2.2.0" />
1818
<PackageReference Include="Hardcodet.NotifyIcon.Wpf" Version="1.1.0" />
1919
<PackageReference Include="DiffPlex" Version="1.9.0" />
20-
<PackageReference Include="HelixToolkit.Core.Wpf" Version="2.27.3" />
2120
<ProjectReference Include="..\..\LeagueToolkit-4.1.0-beta.53\src\LeagueToolkit\LeagueToolkit.csproj" />
2221
<PackageReference Include="Material.Icons.WPF" Version="2.4.1" />
2322
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.2" />
@@ -92,4 +91,4 @@
9291
<Delete Files="@(DocFilesToDelete)" />
9392
<Delete Files="@(DocFilesToDeletePublish)" />
9493
</Target>
95-
</Project>
94+
</Project>

AssetsManager/Services/Audio/AudioBankService.cs

Lines changed: 72 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,10 @@ private void AddUnknownSoundsNode(List<AudioEventNode> eventNodes, Dictionary<ui
195195
{
196196
if (allSounds == null || !allSounds.Any()) return;
197197

198-
var linkedWemIds = new HashSet<uint>(eventNodes.SelectMany(e => e.Sounds).Select(s => s.Id));
198+
var linkedWemIds = new HashSet<uint>(
199+
eventNodes.SelectMany(e => e.Sounds.Select(s => s.Id))
200+
.Concat(eventNodes.SelectMany(e => e.Containers.SelectMany(c => c.Sounds.Select(s => s.Id))))
201+
);
199202
var unlinkedWemIds = allSounds.Keys.Where(id => !linkedWemIds.Contains(id)).ToList();
200203

201204
if (unlinkedWemIds.Any())
@@ -254,16 +257,36 @@ private List<AudioEventNode> ParseEventsBank(byte[][] bnkDatas, Dictionary<uint,
254257

255258
if (!hircObjects.Any()) return eventNodes;
256259

257-
void Traverse(uint objectId, AudioEventNode audioEventNode)
260+
void Traverse(uint objectId, AudioEventNode audioEventNode, AudioContainerNode currentContainer = null)
258261
{
259262
if (!hircObjects.TryGetValue(objectId, out var currentObject)) return;
260263

261264
if (currentObject.Data is IHircContainer container)
262265
{
263266
_logService.LogDebug($"[AUDIO TRAVERSE] Entering {currentObject.Type} (Type {(byte)currentObject.Type}), ID: {objectId}, Children: {container.Children.Count}");
267+
268+
string typeLabel = currentObject.Type.GetAbbreviatedName();
269+
string containerName = $"[{typeLabel}] {objectId}";
270+
271+
// Check if this container was already created for this event (due to multiple ActionIds referencing it)
272+
var nextContainer = audioEventNode.Containers.FirstOrDefault(c => c.Name == containerName);
273+
bool isNewContainer = false;
274+
275+
if (nextContainer == null)
276+
{
277+
nextContainer = new AudioContainerNode { Name = containerName };
278+
isNewContainer = true;
279+
}
280+
264281
foreach (var childId in container.Children)
265282
{
266-
Traverse(childId, audioEventNode);
283+
Traverse(childId, audioEventNode, nextContainer);
284+
}
285+
286+
// If the container accumulated any sounds and is new, add it to the event node
287+
if (isNewContainer && nextContainer.Sounds.Any())
288+
{
289+
audioEventNode.Containers.Add(nextContainer);
267290
}
268291
return;
269292
}
@@ -277,14 +300,29 @@ void Traverse(uint objectId, AudioEventNode audioEventNode)
277300
{
278301
var wemInfo = wemMetadata[soundData.WemId];
279302
_logService.LogDebug($"[AUDIO] Linking sound: {soundData.WemId}");
280-
audioEventNode.Sounds.Add(new WemFileNode
303+
var wemNode = new WemFileNode
281304
{
282305
Id = soundData.WemId,
283306
Name = $"{soundData.WemId}.wem",
284307
Offset = wemInfo.Offset,
285308
Size = wemInfo.Size,
286309
Source = wemInfo.Source
287-
});
310+
};
311+
312+
if (currentContainer != null)
313+
{
314+
if (!currentContainer.Sounds.Any(s => s.Id == wemNode.Id))
315+
{
316+
currentContainer.Sounds.Add(wemNode);
317+
}
318+
}
319+
else
320+
{
321+
if (!audioEventNode.Sounds.Any(s => s.Id == wemNode.Id))
322+
{
323+
audioEventNode.Sounds.Add(wemNode);
324+
}
325+
}
288326
}
289327
else
290328
{
@@ -296,7 +334,7 @@ void Traverse(uint objectId, AudioEventNode audioEventNode)
296334
case BnkObjectType.Action:
297335
if (currentObject.Data is ActionBnkObjectData actionData)
298336
{
299-
Traverse(actionData.ObjectId, audioEventNode);
337+
Traverse(actionData.ObjectId, audioEventNode, currentContainer);
300338
}
301339
break;
302340

@@ -311,14 +349,29 @@ void Traverse(uint objectId, AudioEventNode audioEventNode)
311349
{
312350
var wemInfo = wemMetadata[wemId];
313351
_logService.LogDebug($"[AUDIO] Linking WEM from MusicTrack: {wemId}");
314-
audioEventNode.Sounds.Add(new WemFileNode
352+
var wemNode = new WemFileNode
315353
{
316354
Id = wemId,
317355
Name = $"{wemId}.wem",
318356
Offset = wemInfo.Offset,
319357
Size = wemInfo.Size,
320358
Source = wemInfo.Source
321-
});
359+
};
360+
361+
if (currentContainer != null)
362+
{
363+
if (!currentContainer.Sounds.Any(s => s.Id == wemNode.Id))
364+
{
365+
currentContainer.Sounds.Add(wemNode);
366+
}
367+
}
368+
else
369+
{
370+
if (!audioEventNode.Sounds.Any(s => s.Id == wemNode.Id))
371+
{
372+
audioEventNode.Sounds.Add(wemNode);
373+
}
374+
}
322375
}
323376
else
324377
{
@@ -352,6 +405,17 @@ void Traverse(uint objectId, AudioEventNode audioEventNode)
352405

353406
if (audioEventNode.Sounds.Any() || audioEventNode.Containers.Any())
354407
{
408+
// POST-PROCESS OPTIMIZATION:
409+
// If the event has only one container, and no sounds at the root event level,
410+
// we dissolve the container and move all its sounds to the event root level.
411+
// This avoids creating folders when all sounds belong to the exact same "family".
412+
if (audioEventNode.Containers.Count == 1 && !audioEventNode.Sounds.Any())
413+
{
414+
var singleContainer = audioEventNode.Containers[0];
415+
audioEventNode.Sounds.AddRange(singleContainer.Sounds);
416+
audioEventNode.Containers.Clear();
417+
}
418+
355419
eventNodes.Add(audioEventNode);
356420
}
357421
}

AssetsManager/Services/Downloads/ExtractionService.cs

Lines changed: 56 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,10 @@ private async Task<int> CalculateTotalSmartAsync(IEnumerable<FileSystemNodeModel
291291
{
292292
if (eventNode.IsTechnicalNode) continue;
293293
soundsCount += eventNode.Sounds.Count;
294+
foreach (var containerNode in eventNode.Containers)
295+
{
296+
soundsCount += containerNode.Sounds.Count;
297+
}
294298
}
295299

296300
count += (soundsCount > 0) ? soundsCount : 1;
@@ -318,7 +322,7 @@ private int CountSoundsInAudioTree(IEnumerable<FileSystemNodeModel> nodes)
318322
foreach (var node in nodes)
319323
{
320324
if (node.Type == NodeType.WemFile) count++;
321-
else count += CountSoundsInAudioTree(node.Children);
325+
else if (node.Children != null) count += CountSoundsInAudioTree(node.Children);
322326
}
323327
return count;
324328
}
@@ -358,14 +362,25 @@ private async Task ExportSmartAsync(FileSystemNodeModel node, string destination
358362
string eventPath = Path.Combine(destinationPath, PathUtils.SanitizeName(node.Name));
359363
_directoriesCreator.CreateDirectory(eventPath);
360364

361-
foreach (var soundNode in node.Children)
365+
async Task ExportSoundsRecursiveAsync(FileSystemNodeModel parentNode, string currentPath)
362366
{
363-
cancellationToken.ThrowIfCancellationRequested();
364-
if (soundNode.Type == NodeType.WemFile)
367+
foreach (var childNode in parentNode.Children)
365368
{
366-
await HandleWemFileAsync(soundNode, eventPath, cancellationToken, onFileSavedCallback);
369+
cancellationToken.ThrowIfCancellationRequested();
370+
if (childNode.Type == NodeType.WemFile)
371+
{
372+
await HandleWemFileAsync(childNode, currentPath, cancellationToken, onFileSavedCallback);
373+
}
374+
else if (childNode.Type == NodeType.VirtualDirectory)
375+
{
376+
string subFolderPath = Path.Combine(currentPath, PathUtils.SanitizeName(childNode.Name));
377+
_directoriesCreator.CreateDirectory(subFolderPath);
378+
await ExportSoundsRecursiveAsync(childNode, subFolderPath);
379+
}
367380
}
368381
}
382+
383+
await ExportSoundsRecursiveAsync(node, eventPath);
369384
return;
370385
}
371386

@@ -560,36 +575,53 @@ private async Task HandleAudioBankFile(FileSystemNodeModel node, string destinat
560575
else
561576
audioTree = _audioBankService.ParseGenericAudioBank(wpkData, audioBnkFileData, eventsData);
562577

578+
async Task ExportSoundNodeAsync(WemFileNode soundNode, string eventPath)
579+
{
580+
byte[] wemData = null;
581+
if (soundNode.Source == AudioSourceType.Wpk && wpkData != null)
582+
{
583+
wemData = wpkData.AsSpan((int)soundNode.Offset, (int)soundNode.Size).ToArray();
584+
}
585+
else if (audioBnkFileData != null)
586+
{
587+
wemData = audioBnkFileData.AsSpan((int)soundNode.Offset, (int)soundNode.Size).ToArray();
588+
}
589+
590+
if (wemData != null)
591+
{
592+
var format = _appSettings.AudioExportFormat;
593+
byte[] convertedData = await _audioConversionService.ConvertAudioToFormatAsync(wemData, ".wem", format, cancellationToken);
594+
if (convertedData != null)
595+
{
596+
string extension = format switch { AudioExportFormat.Wav => ".wav", AudioExportFormat.Mp3 => ".mp3", _ => ".ogg" };
597+
string filePath = PathUtils.GetUniqueFilePath(eventPath, Path.ChangeExtension(soundNode.Name, extension));
598+
await File.WriteAllBytesAsync(filePath, convertedData, cancellationToken);
599+
onFileSavedCallback?.Invoke(filePath);
600+
}
601+
}
602+
}
603+
563604
foreach (var eventNode in audioTree)
564605
{
565606
if (eventNode.IsTechnicalNode) continue;
566607

567608
string eventPath = Path.Combine(audioBankPath, PathUtils.SanitizeName(eventNode.Name));
568609
_directoriesCreator.CreateDirectory(eventPath);
569610

611+
// Export root-level sounds
570612
foreach (var soundNode in eventNode.Sounds)
571613
{
572-
byte[] wemData = null;
573-
if (soundNode.Source == AudioSourceType.Wpk && wpkData != null)
574-
{
575-
wemData = wpkData.AsSpan((int)soundNode.Offset, (int)soundNode.Size).ToArray();
576-
}
577-
else if (audioBnkFileData != null)
578-
{
579-
wemData = audioBnkFileData.AsSpan((int)soundNode.Offset, (int)soundNode.Size).ToArray();
580-
}
614+
await ExportSoundNodeAsync(soundNode, eventPath);
615+
}
581616

582-
if (wemData != null)
617+
// Export sounds in sub-containers (families)
618+
foreach (var containerNode in eventNode.Containers)
619+
{
620+
string containerPath = Path.Combine(eventPath, PathUtils.SanitizeName(containerNode.Name));
621+
_directoriesCreator.CreateDirectory(containerPath);
622+
foreach (var soundNode in containerNode.Sounds)
583623
{
584-
var format = _appSettings.AudioExportFormat;
585-
byte[] convertedData = await _audioConversionService.ConvertAudioToFormatAsync(wemData, ".wem", format, cancellationToken);
586-
if (convertedData != null)
587-
{
588-
string extension = format switch { AudioExportFormat.Wav => ".wav", AudioExportFormat.Mp3 => ".mp3", _ => ".ogg" };
589-
string filePath = PathUtils.GetUniqueFilePath(eventPath, Path.ChangeExtension(soundNode.Name, extension));
590-
await File.WriteAllBytesAsync(filePath, convertedData, cancellationToken);
591-
onFileSavedCallback?.Invoke(filePath);
592-
}
624+
await ExportSoundNodeAsync(soundNode, containerPath);
593625
}
594626
}
595627
}

AssetsManager/Services/Explorer/ExplorerPreviewService.cs

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System.Linq;
55
using System.Text;
66
using System.Windows;
7+
using System.Threading;
78
using System.Threading.Tasks;
89
using System.Windows.Controls;
910
using System.Windows.Media.Imaging;
@@ -37,6 +38,7 @@ private enum Previewer { None, Image, WebView, AvalonEdit, StatusPanel }
3738
private TextEditor _textEditorPreview;
3839
private FilePreviewerModel _viewModel;
3940
private IHighlightingDefinition _jsonHighlightingDefinition;
41+
private CancellationTokenSource _previewCancellationTokenSource;
4042

4143
private readonly LogService _logService;
4244
private readonly DirectoriesCreator _directoriesCreator;
@@ -144,6 +146,11 @@ public async Task ShowPreviewAsync(FileSystemNodeModel node)
144146
}
145147
}
146148

149+
// Cancel previous active preview task to avoid WebView2 concurrency collisions
150+
_previewCancellationTokenSource?.Cancel();
151+
_previewCancellationTokenSource = new CancellationTokenSource();
152+
var cancellationToken = _previewCancellationTokenSource.Token;
153+
147154
// Step 2: Discovery of technical metadata (e.g., Summoner Icons, Emotes)
148155
// We only update/clear metadata if the current node is an image.
149156
// If it's a text file, we keep the metadata of the image shown in the other slot (Dual View).
@@ -170,13 +177,19 @@ public async Task ShowPreviewAsync(FileSystemNodeModel node)
170177
try
171178
{
172179
byte[] data = null;
173-
if (node.Type == NodeType.VirtualFile) { data = await _wadContentProvider.GetVirtualFileBytesAsync(node); }
174-
else if (node.Type == NodeType.RealFile) { if (File.Exists(node.VirtualPath)) data = await File.ReadAllBytesAsync(node.VirtualPath); }
175-
else if (node.Type == NodeType.WemFile) { data = await _wadContentProvider.GetWemFileBytesAsync(node); }
180+
if (node.Type == NodeType.VirtualFile) { data = await _wadContentProvider.GetVirtualFileBytesAsync(node, cancellationToken); }
181+
else if (node.Type == NodeType.RealFile) { if (File.Exists(node.VirtualPath)) data = await File.ReadAllBytesAsync(node.VirtualPath, cancellationToken); }
182+
else if (node.Type == NodeType.WemFile) { data = await _wadContentProvider.GetWemFileBytesAsync(node, cancellationToken); }
183+
184+
if (cancellationToken.IsCancellationRequested) return;
176185

177186
if (data != null) { await DispatchPreview(data, node.Extension, node); }
178187
else { await ShowUnsupportedPreviewAsync(node.Extension); }
179188
}
189+
catch (OperationCanceledException)
190+
{
191+
// Handled gracefully: Task was cancelled due to quick navigation
192+
}
180193
catch (Exception ex)
181194
{
182195
_logService.LogError(ex, $"Failed to preview file '{node.VirtualPath}'.");

0 commit comments

Comments
 (0)