Skip to content

Commit 25d2ca4

Browse files
authored
Merge pull request WolvenKit#2775 from WolvenKit/add-radioext-station
Feature: add radioext station
2 parents a95ca6d + 1d556bd commit 25d2ca4

File tree

14 files changed

+973
-13
lines changed

14 files changed

+973
-13
lines changed

WolvenKit.App/Helpers/InkatlasImageGenerator.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.IO;
66
using System.Linq;
77
using WolvenKit.App.Models.ProjectManagement.Project;
8+
using WolvenKit.Core.Exceptions;
89
using WolvenKit.Interfaces.Extensions;
910
using WolvenKit.RED4.Archive.CR2W;
1011
using WolvenKit.RED4.Types;
@@ -148,13 +149,13 @@ private static List<AtlasPart> CreateAtlasImages(string pngFolder, string relati
148149
}
149150
catch (Exception ex)
150151
{
151-
Console.WriteLine($"Error loading image {file}: {ex.Message}");
152+
throw new WolvenKitException(0, $"Error loading image {file}: {ex.Message}");
152153
}
153154
}
154155

155156
if (images.Count == 0)
156157
{
157-
throw new InvalidOperationException("No valid PNG files could be loaded.");
158+
throw new WolvenKitException(0, $"Failed to load PNG data from a file in {pngFolder}");
158159
}
159160

160161
// Calculate atlas dimensions

WolvenKit.App/Helpers/TemplateFileTools.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,10 @@
2222

2323
namespace WolvenKit.App.Helpers;
2424

25-
public class TemplateFileTools
25+
public partial class TemplateFileTools
2626
{
2727
private readonly ILoggerService _loggerService;
28+
private readonly INotificationService _notificationService;
2829
private readonly IProjectManager _projectManager;
2930
private readonly IModTools _modTools;
3031
private readonly Cr2WTools _cr2WTools;
@@ -35,7 +36,7 @@ public class TemplateFileTools
3536

3637
public TemplateFileTools(ILoggerService loggerService, IProjectManager projectManager, IModTools modTools,
3738
Cr2WTools cr2WTools, DocumentTools documentTools, ProjectResourceTools projectResourceTools,
38-
ISettingsManager settingsManager, IAppArchiveManager archiveManager)
39+
ISettingsManager settingsManager, IAppArchiveManager archiveManager, INotificationService notificationService)
3940
{
4041
_loggerService = loggerService;
4142
_projectManager = projectManager;
@@ -46,6 +47,7 @@ public TemplateFileTools(ILoggerService loggerService, IProjectManager projectMa
4647
_settingsManager = settingsManager;
4748
_settingsManager = settingsManager;
4849
_archiveManager = archiveManager;
50+
_notificationService = notificationService;
4951
}
5052

5153
public void CopyInkatlasTemplateSingle(string inkatlasRelativePath, bool forceOverwrite)
Lines changed: 307 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,307 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.IO;
4+
using System.Linq;
5+
using System.Text.Json;
6+
using WolvenKit.App.Interaction;
7+
using WolvenKit.App.Models.ProjectManagement.Project;
8+
using WolvenKit.App.ViewModels.Dialogs;
9+
using WolvenKit.Core.Exceptions;
10+
using WolvenKit.Interfaces.Extensions;
11+
12+
namespace WolvenKit.App.Helpers;
13+
14+
public class RadioExtStreamInfo
15+
{
16+
public string StreamURL { get; set; } = "";
17+
public bool IsStream { get; set; } = false;
18+
}
19+
20+
public class RadioExtIcon
21+
{
22+
public string InkAtlasPath { get; set; } = "";
23+
public string InkAtlasPart { get; set; } = "";
24+
public bool UseCustom { get; set; } = false;
25+
}
26+
27+
public class RadioExtStation
28+
{
29+
public string DisplayName { get; init; } = "";
30+
public double Fm { get; init; } = 0.0;
31+
public double Volume { get; init; } = 1.0;
32+
public string Icon { get; set; } = "UIIcon.alcohol_absynth";
33+
public RadioExtIcon CustomIcon { get; set; } = new();
34+
public RadioExtStreamInfo StreamInfo { get; init; } = new();
35+
public string[] Order { get; set; } = [];
36+
}
37+
38+
public partial class TemplateFileTools
39+
{
40+
private static readonly string s_subfolderPath =
41+
Path.Combine("bin", "x64", "plugins", "cyber_engine_tweaks", "mods", "radioExt", "radios");
42+
43+
private const string s_metadataFileName = "metadata.json";
44+
45+
private static RadioExtStation ReadJson(string absolutePath)
46+
{
47+
if (!File.Exists(absolutePath))
48+
{
49+
throw new WolvenKitException(0, $"{s_metadataFileName} not found: {absolutePath}");
50+
}
51+
52+
var json = File.ReadAllText(absolutePath);
53+
var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
54+
55+
try
56+
{
57+
return JsonSerializer.Deserialize<RadioExtStation>(json, options)
58+
?? throw new WolvenKitException(0, $"Failed to deserialize JSON in {absolutePath}");
59+
}
60+
catch (JsonException ex)
61+
{
62+
throw new WolvenKitException(0, $"Invalid JSON in {absolutePath}: {ex.Message}");
63+
}
64+
}
65+
66+
private void CreateInkatlas(AddRadioExtFilesDialogViewModel model, string modderName)
67+
{
68+
if (string.IsNullOrEmpty(model.IconFilePath) || !File.Exists(model.IconFilePath) ||
69+
_projectManager.ActiveProject is not { } project)
70+
{
71+
return;
72+
}
73+
74+
var destDir = Directory.CreateTempSubdirectory();
75+
File.Copy(model.IconFilePath, Path.Combine(destDir.FullName, "icon.png"));
76+
77+
var relativePath = Path.Combine(modderName, "radio_stations", model.StationName!.ToFileName());
78+
var fileName = $"{model.StationName!.ToFileName()}_atlas";
79+
80+
if (!string.IsNullOrEmpty(model.InkatlasPath) && Path.GetDirectoryName(model.InkatlasPath) is string p &&
81+
Path.GetFileName(model.InkatlasPath) is string f && !string.IsNullOrEmpty(p) && !string.IsNullOrEmpty(f))
82+
{
83+
relativePath = p;
84+
fileName = f;
85+
}
86+
else
87+
{
88+
model.InkatlasPath = Path.Combine(relativePath, $"{fileName}.inkatlas");
89+
}
90+
91+
InkatlasImageGenerator.GenerateAtlas(
92+
pngFolder: destDir.FullName,
93+
relativeSourcePath: relativePath,
94+
atlasFileName: fileName,
95+
tileWidth: 200,
96+
tileHeight: 200,
97+
_cr2WTools,
98+
project
99+
);
100+
}
101+
102+
private void SerializeToJson(AddRadioExtFilesDialogViewModel model, string modderName)
103+
{
104+
var station = new RadioExtStation
105+
{
106+
DisplayName = model.StationName ?? "Unnamed Station",
107+
Fm = model.Frequency,
108+
Volume = 1.0,
109+
Icon = "",
110+
StreamInfo = new RadioExtStreamInfo
111+
{
112+
IsStream = model.UseStream, StreamURL = model.UseStream ? model.StreamPath ?? "" : ""
113+
},
114+
Order = model.UseStream
115+
? []
116+
: model.SongItems.OrderBy(s => s.Index).Select(s => s.DisplayName).ToArray()
117+
};
118+
119+
CreateInkatlas(model, modderName);
120+
121+
if (model.InkatlasPath is string inkatlasPath && !string.IsNullOrEmpty(inkatlasPath))
122+
{
123+
station.Icon = "";
124+
station.CustomIcon = new RadioExtIcon
125+
{
126+
UseCustom = true, InkAtlasPath = inkatlasPath ?? "", InkAtlasPart = "icon"
127+
};
128+
}
129+
130+
131+
var options = new JsonSerializerOptions { WriteIndented = true };
132+
var json = JsonSerializer.Serialize(station, options);
133+
134+
var absolutePath = Path.Combine(model.JsonFileFolder ?? "", s_metadataFileName);
135+
File.WriteAllText(absolutePath, json);
136+
}
137+
138+
139+
private static readonly List<string> s_audioFileExtensions =
140+
[".mp3", ".wav", ".ogg", ".flac", ".mp2", ".wax", ".wma"];
141+
142+
public AddRadioExtFilesDialogViewModel LoadRadioProperties(string absolutePath)
143+
{
144+
try
145+
{
146+
var station = ReadJson(absolutePath);
147+
148+
var stationFileFolder = Path.GetDirectoryName(absolutePath) ?? absolutePath.Replace(s_metadataFileName, "");
149+
150+
var model = new AddRadioExtFilesDialogViewModel
151+
{
152+
StationName = station.DisplayName,
153+
Frequency = Math.Round(station.Fm, 1),
154+
JsonFileFolder = stationFileFolder,
155+
};
156+
157+
if (station.CustomIcon.UseCustom)
158+
{
159+
model.InkatlasPath = station.CustomIcon.InkAtlasPath;
160+
model.InkatlasPart = station.CustomIcon.InkAtlasPart;
161+
}
162+
163+
if (station.StreamInfo.IsStream)
164+
{
165+
model.UseStream = true;
166+
model.StreamPath = station.StreamInfo.StreamURL;
167+
station.Order = [];
168+
return model;
169+
}
170+
171+
model.UseStream = false;
172+
173+
var songs = station.Order.ToList();
174+
List<RadioSongItem> songItems = [];
175+
176+
// Add files from mod folder
177+
178+
var filesInMod = Directory.GetFiles(stationFileFolder)
179+
.Where(f => s_audioFileExtensions.Contains(Path.GetExtension(f))).ToList();
180+
foreach (var file in filesInMod)
181+
{
182+
var fileName = Path.GetFileName(file);
183+
songs = songs.Where(f => !fileName.EndsWith(f)).ToList();
184+
songItems.Add(new RadioSongItem(file, 0));
185+
}
186+
187+
if (station.Order.Length > 0)
188+
{
189+
var songIdx = 0;
190+
foreach (var songName in station.Order)
191+
{
192+
if (songItems.FirstOrDefault(f => f.DisplayName == songName) is not RadioSongItem item)
193+
{
194+
continue;
195+
}
196+
197+
item.Index = songIdx;
198+
model.AddSong(item);
199+
songIdx += 1;
200+
}
201+
}
202+
203+
if (songs.Count > 0)
204+
{
205+
_loggerService.Error("No audio files found for the following songs:\n\t" +
206+
string.Join("\n\t", songs)
207+
+ "\nThey will be removed from your radio. To include them, please add them again via dialogue.");
208+
_notificationService.Error("Some audio files are missing. Please check the console for details.");
209+
}
210+
211+
model.AddSongs(songItems);
212+
return model;
213+
}
214+
catch
215+
{
216+
// ignore
217+
}
218+
219+
return new AddRadioExtFilesDialogViewModel();
220+
}
221+
222+
223+
private void CopyFilesToModFolder(AddRadioExtFilesDialogViewModel viewModel)
224+
{
225+
if (_projectManager.ActiveProject is not { } project)
226+
{
227+
throw new WolvenKitException(0, "No active project.");
228+
}
229+
230+
if (string.IsNullOrEmpty(viewModel.JsonFileFolder))
231+
{
232+
viewModel.JsonFileFolder = Path.Combine(project.ResourcesDirectory, s_subfolderPath,
233+
viewModel.StationName!.ToFileName());
234+
}
235+
236+
var songPaths = viewModel.SongItems.Select(f => f.FilePath).ToList();
237+
238+
var songsToDelete = Directory.EnumerateFiles(project.ResourcesDirectory, "*.*", SearchOption.AllDirectories)
239+
.Where(f => s_audioFileExtensions.Contains(Path.GetExtension(f)) && !songPaths.Contains(f)).ToList();
240+
241+
242+
if (songsToDelete.Count > 0)
243+
{
244+
var question = $"Delete the following song files from the project? You can't undo this!\n{
245+
(string.Join("\n", songsToDelete.Select(f => f.Replace(project.ResourcesDirectory, ""))))
246+
}";
247+
248+
if (Interactions.ShowQuestionYesNo((question, "Delete Unused Songs")))
249+
{
250+
foreach (var songFilePath in songsToDelete)
251+
{
252+
File.Delete(songFilePath);
253+
}
254+
}
255+
}
256+
257+
258+
var songsOutsideOfProject =
259+
viewModel.SongItems.Where(s => !s.FilePath.StartsWith(project.FileDirectory)).ToList();
260+
261+
if (songsOutsideOfProject.Count == 0)
262+
{
263+
return;
264+
}
265+
266+
267+
var sourceDestPath = songsOutsideOfProject.ToDictionary(x => x,
268+
s => Path.Combine(viewModel.JsonFileFolder, Path.GetFileName(s.FilePath)));
269+
270+
// Check for duplicates and ask if we want to overwrite them
271+
var existingFiles = sourceDestPath.Values.Where(File.Exists).ToList();
272+
var overwrite = false;
273+
if (existingFiles.Count > 0)
274+
{
275+
overwrite = Interactions.ShowQuestionYesNo(("Some files already exist in the destination folder:\n\t" +
276+
string.Join("\n\t", existingFiles) +
277+
"\n\nDo you want to overwrite them?",
278+
"Overwrite Existing Files"));
279+
}
280+
281+
foreach (var kvp in sourceDestPath)
282+
{
283+
File.Copy(kvp.Key.FilePath, kvp.Value, overwrite);
284+
kvp.Key.FilePath = kvp.Value;
285+
}
286+
}
287+
288+
public void CreateRadio(AddRadioExtFilesDialogViewModel model, string modderName, Cp77Project project)
289+
{
290+
CopyFilesToModFolder(model);
291+
SerializeToJson(model, modderName);
292+
293+
var msg = "You can now install your mod!";
294+
if (project.RawFiles.Any(f => f.HasFileExtension(".png")) &&
295+
!project.ModFiles.Any(f => f.HasFileExtension(".xbm")))
296+
{
297+
msg = "Use the import tool to import your icon as .xbm, then install your mod!";
298+
}
299+
300+
_notificationService.Success($"Radio station written to metadata.json. {msg}");
301+
_loggerService.Success($"Radio station written to metadata.json. {msg}");
302+
303+
304+
}
305+
306+
307+
}

WolvenKit.App/Interaction/Interactions.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,4 +308,10 @@ string inputFieldDefaultValue
308308
/// </summary>
309309
public static Func<PlayerHeadDialogViewModel> ShowNewPlayerHeadView { get; set; } =
310310
() => throw new NotImplementedException();
311+
312+
/// <summary>
313+
/// Shows dialogue to add item codes to .reds store and vendor yaml
314+
/// </summary>
315+
public static Func<Cp77Project, AddRadioExtFilesDialogViewModel?> CreateOrEditRadioDialog { get; set; } =
316+
project => throw new NotImplementedException();
311317
}

0 commit comments

Comments
 (0)