forked from LittleBigRefresh/Refresh
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathGameAssetExtensions.cs
More file actions
439 lines (380 loc) · 19.9 KB
/
Copy pathGameAssetExtensions.cs
File metadata and controls
439 lines (380 loc) · 19.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
using System.Security.Cryptography;
using Bunkum.Core.Storage;
using Refresh.Common.Helpers;
using Refresh.Core.Importing;
using Refresh.Core.Importing.Mip;
using Refresh.Core.Types.Data;
using Refresh.Database;
using Refresh.Database.Models.Assets;
using Refresh.Database.Models.Authentication;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
namespace Refresh.Core.Extensions;
public static class GameAssetExtensions
{
public static void TraverseDependenciesRecursively(this GameAsset asset, GameDatabaseContext database, Action<string, GameAsset?> callback)
{
callback(asset.AssetHash, asset);
foreach (string internalAssetHash in database.GetAssetDependencies(asset))
{
GameAsset? internalAsset = database.GetAssetFromHash(internalAssetHash);
// Only run this if this is null, since the next recursion will trigger its own callback
if(internalAsset == null)
callback(internalAssetHash, internalAsset);
internalAsset?.TraverseDependenciesRecursively(database, callback);
}
}
/// <summary>
/// Automatically crops and resizes an image into its corresponding icon form.
/// </summary>
/// <param name="image">The source image</param>
/// <returns>The cropped and resized image, or null if its already fine</returns>
private static Image<Rgba32>? CropToIcon(Image image)
{
const int maxWidth = 256;
//If the image is already square, and already small enough for our uses, then we can just return it as-is
if (image.Width == image.Height && image.Width <= maxWidth) return null;
Image<Rgba32> copy = image.CloneAs<Rgba32>();
//If the image is already square, just resize it.
if (image.Width == image.Height)
{
copy.Mutate(ctx => ctx.Resize(maxWidth, maxWidth));
return copy;
}
Rectangle cropRectangle;
//If the image is wider than it is tall
cropRectangle = image.Width > image.Height ? new Rectangle(image.Width / 2 - image.Height / 2, 0, image.Height, image.Height) :
//If the image is taller than it is wide
new Rectangle(0, image.Height / 2 - image.Width / 2, image.Width, image.Width);
int targetWidth = Math.Clamp(cropRectangle.Width, 16, maxWidth);
//Round to the nearest multiple of 16, this is to make PSP happy
targetWidth =
(int)Math.Round(
targetWidth / (double)16,
MidpointRounding.AwayFromZero
) * 16;
copy.Mutate(ctx => ctx.Crop(cropRectangle).Resize(targetWidth, targetWidth));
return copy;
}
/// <summary>
/// Transforms an image to be consumed by a particular game
/// </summary>
/// <param name="game">The game which will consume the resulting asset</param>
/// <param name="dataStore">The data store</param>
/// <param name="decodeImage">The function used to decode an image from the data store</param>
/// <param name="transformImage">The transformation function</param>
/// <returns>The hash of the transformed asset</returns>
/// <exception cref="NotImplementedException">That conversion step is unimplemented at the moment</exception>
/// <exception cref="ArgumentOutOfRangeException">Invalid TokenGame</exception>
private static string TransformImage(this GameAsset asset, TokenGame game, IDataStore dataStore, Func<string, Image<Rgba32>> decodeImage, Func<Image, Image<Rgba32>?> transformImage)
{
string dataStorePath = asset.IsPSP ? $"psp/{asset.AssetHash}" : asset.AssetHash;
bool mainlineDoesntNeedConversion = asset.AssetType is GameAssetType.Png or GameAssetType.Texture or GameAssetType.GameDataTexture;
switch (game)
{
case TokenGame.Website:
case TokenGame.LittleBigPlanet1:
case TokenGame.LittleBigPlanet2:
case TokenGame.LittleBigPlanet3:
case TokenGame.LittleBigPlanetVita: {
Image sourceImage = decodeImage(dataStorePath);
//Load the image from the data store and transform it
Image? image = transformImage(sourceImage);
//If its null, then no transformation was needed
if (image == null)
{
if (mainlineDoesntNeedConversion)
{
//Return the existing asset hash
return asset.AssetHash;
}
//Set the image to use to the source image
image = sourceImage;
}
//Save the image as a PNG file in a byte array in memory
MemoryStream ms = new();
image.SaveAsPng(ms);
byte[] data = ms.ToArray();
//Get the hash of the converted asset
string convertedHash = HexHelper.BytesToHexString(SHA1.HashData(data));
//Write the data to the store
dataStore.WriteToStore(convertedHash, data);
//Return the new icon hash
return convertedHash;
}
case TokenGame.LittleBigPlanetPSP: {
Image<Rgba32> sourceImage = decodeImage(dataStorePath);
//Transform the image, if no transformation is needed, use the source image
Image<Rgba32> image = transformImage(sourceImage) ?? sourceImage;
MemoryStream ms = new();
new MipEncoder().Encode(image, ms);
//Get the used chunk of the underlying buffer
Span<byte> data = ms.GetBuffer().AsSpan()[..((int)ms.Length)];
//Encrypt the data
byte[] encryptedData = ResourceHelper.PspEncrypt(data, Importer.PSPKey.Value);
//Get the hash
string convertedHash = HexHelper.BytesToHexString(SHA1.HashData(encryptedData));
dataStore.WriteToStore($"psp/{convertedHash}", encryptedData);
return convertedHash;
}
default:
throw new ArgumentOutOfRangeException(nameof(game), game, null);
}
}
/// <summary>
/// Converts the asset into a suitable format to be used as a photo in the target game.
/// </summary>
/// <param name="game">The game to convert for</param>
/// <param name="database">The database</param>
/// <param name="dataStore">The data store</param>
/// <returns>The new hash of the converted asset, or null if no conversion has taken place</returns>
public static string? GetAsPhoto(this GameAsset asset, TokenGame game, DataContext dataContext)
{
return asset.GetAsGeneric(
game,
dataContext.Database,
dataContext.DataStore,
_ => null,
() => asset.AsMainlinePhotoHash,
hash => dataContext.Database.SetMainlinePhotoHash(asset, hash),
() => throw new NotSupportedException(),
_ => throw new NotSupportedException()
);
}
/// <summary>
/// Converts the asset into a suitable format to be used as an icon in the target game.
/// </summary>
/// <param name="game">The game to convert for</param>
/// <param name="database">The database</param>
/// <param name="dataStore">The data store</param>
/// <returns>The new hash of the converted asset, or null if no conversion has taken place</returns>
public static string? GetAsIcon(this GameAsset asset, TokenGame game, DataContext dataContext)
{
return asset.GetAsGeneric(
game,
dataContext.Database,
dataContext.DataStore,
CropToIcon,
() => asset.AsMainlineIconHash,
hash => dataContext.Database.SetMainlineIconHash(asset, hash),
() => asset.AsMipIconHash,
hash => dataContext.Database.SetMipIconHash(asset, hash)
);
}
/// <summary>
/// Converts the asset to the correct type for the specified game, using the provided transformation methods.
///
/// Caches the converted mainline and MIP hashes using get/set functions passed in.
/// </summary>
/// <param name="game">The game to convert for</param>
/// <param name="database">The database</param>
/// <param name="dataStore">The data store</param>
/// <param name="transformImage">The transform function for the image, returning null if no transformation needs to take place</param>
/// <param name="getMainline">The method to get the cached mainline hash, or null if uncached</param>
/// <param name="setMainline">The method to set the cached mainline hash</param>
/// <param name="getMip">The method to get the cached MIP hash, or null if uncached</param>
/// <param name="setMip">The method to set the cached MIP hash</param>
/// <returns>The converted hash, or null if no conversion has taken place</returns>
private static string? GetAsGeneric(this GameAsset asset, TokenGame game, GameDatabaseContext database, IDataStore dataStore, Func<Image, Image<Rgba32>?> transformImage, Func<string?> getMainline, Action<string> setMainline,
Func<string?> getMip, Action<string> setMip)
{
try
{
switch (asset.AssetType)
{
case GameAssetType.Tga:
case GameAssetType.Jpeg:
case GameAssetType.Png:
switch (game)
{
case TokenGame.Website:
case TokenGame.LittleBigPlanet1:
case TokenGame.LittleBigPlanet2:
case TokenGame.LittleBigPlanet3:
case TokenGame.LittleBigPlanetVita: {
//If the cached icon hash is already set, early return it.
if (getMainline() != null) return getMainline()!;
string convertedHash = asset.TransformImage(game, dataStore, path => Image.Load<Rgba32>(dataStore.GetStreamFromStore(path)), transformImage);
setMainline(convertedHash);
//Return the new icon hash
return getMainline()!;
}
case TokenGame.LittleBigPlanetPSP: {
if (getMip() != null) return getMip()!;
string convertedHash = asset.TransformImage(game, dataStore, path => Image.Load<Rgba32>(dataStore.GetStreamFromStore(path)), transformImage);
setMip(convertedHash);
return getMip()!;
}
default:
throw new ArgumentOutOfRangeException(nameof(game), game, null);
}
case GameAssetType.Texture:
switch (game)
{
case TokenGame.Website:
case TokenGame.LittleBigPlanet1:
case TokenGame.LittleBigPlanet2:
case TokenGame.LittleBigPlanet3:
case TokenGame.LittleBigPlanetVita:
{
//If the cached icon hash is already set, early return it.
if (getMainline() != null) return getMainline()!;
string convertedHash = asset.TransformImage(game, dataStore, path => ImageImporter.LoadTex(dataStore.GetStreamFromStore(path)), transformImage);
setMainline(convertedHash);
//Return the new icon hash
return getMainline()!;
}
case TokenGame.LittleBigPlanetPSP:
{
if (getMip() != null) return getMip()!;
string convertedHash = asset.TransformImage(game, dataStore, path => ImageImporter.LoadTex(dataStore.GetStreamFromStore(path)), transformImage);
setMip(convertedHash);
return getMip()!;
}
default:
throw new ArgumentOutOfRangeException(nameof(game), game, null);
}
case GameAssetType.GameDataTexture:
switch (game)
{
case TokenGame.Website:
case TokenGame.LittleBigPlanet1:
case TokenGame.LittleBigPlanet2:
case TokenGame.LittleBigPlanet3:
case TokenGame.LittleBigPlanetVita: {
//If the cached icon hash is already set, early return it.
if (getMainline() != null) return getMainline()!;
string convertedHash = asset.TransformImage(game, dataStore, path => ImageImporter.LoadGtf(dataStore.GetStreamFromStore(path)), transformImage);
setMainline(convertedHash);
//Return the new icon hash
return getMainline()!;
}
case TokenGame.LittleBigPlanetPSP:
{
if (getMip() != null) return getMip()!;
string convertedHash = asset.TransformImage(game, dataStore, path => ImageImporter.LoadGtf(dataStore.GetStreamFromStore(path)), transformImage);
setMip(convertedHash);
return getMip()!;
}
default:
throw new ArgumentOutOfRangeException(nameof(game), game, null);
}
case GameAssetType.Mip:
switch (game)
{
//LBP1, LBP2, LBP3, and LBP Vita are unable to handle MIP files.
//The Website technically can utilize them after import,
//but using PNGs for the site will cause less load on the server, so lets do that!
case TokenGame.Website:
case TokenGame.LittleBigPlanet1:
case TokenGame.LittleBigPlanet2:
case TokenGame.LittleBigPlanet3:
case TokenGame.LittleBigPlanetVita: {
//If the cached icon hash is already set, early return it.
if (getMainline() != null) return getMainline()!;
string convertedHash = asset.TransformImage(game, dataStore, path =>
{
//Load the data from the data store
byte[] rawData = dataStore.GetDataFromStore(path);
//Decrypt it
byte[] sourceData = ResourceHelper.PspDecrypt(rawData, Importer.PSPKey.Value);
//Create a memory stream from the decrypted asset data
using MemoryStream sourceDataStream = new(sourceData);
//Load the mip file
return ImageImporter.LoadMip(sourceDataStream);
}, transformImage);
setMainline(convertedHash);
//Return the new icon hash
return getMainline()!;
}
case TokenGame.LittleBigPlanetPSP: {
//If the cached icon hash is already set, early return it.
if (getMip() != null) return getMip()!;
string convertedHash = asset.TransformImage(game, dataStore, path =>
{
//Load the data from the data store
byte[] rawData = dataStore.GetDataFromStore(path);
//Decrypt it
byte[] sourceData = ResourceHelper.PspDecrypt(rawData, Importer.PSPKey.Value);
//Create a memory stream from the decrypted asset data
using MemoryStream sourceDataStream = new(sourceData);
//Load the mip file
return ImageImporter.LoadMip(sourceDataStream);
}, transformImage);
setMip(convertedHash);
return getMip()!;
}
default:
throw new ArgumentOutOfRangeException(nameof(game), game, null);
}
case GameAssetType.Level:
case GameAssetType.Painting:
case GameAssetType.Plan:
case GameAssetType.GfxMaterial:
case GameAssetType.Material:
case GameAssetType.Mesh:
case GameAssetType.Palette:
case GameAssetType.Script:
case GameAssetType.ThingRecording:
case GameAssetType.VoiceRecording:
case GameAssetType.SyncedProfile:
case GameAssetType.GriefSongState:
case GameAssetType.SoftPhysicsSettings:
case GameAssetType.Bevel:
case GameAssetType.StreamingLevelChunk:
case GameAssetType.Animation:
case GameAssetType.Unknown:
case GameAssetType.GuidSubstitution:
case GameAssetType.SettingsCharacter:
case GameAssetType.Fontface:
case GameAssetType.DownloadableContent:
case GameAssetType.Joint:
case GameAssetType.GameConstants:
case GameAssetType.PoppetSettings:
case GameAssetType.CachedLevelData:
case GameAssetType.Game:
case GameAssetType.SettingsNetwork:
case GameAssetType.Packs:
case GameAssetType.BigProfile:
case GameAssetType.SlotList:
case GameAssetType.AdventureCreateProfile:
case GameAssetType.LocalProfile:
case GameAssetType.LimitsSettings:
case GameAssetType.Tutorials:
case GameAssetType.GuidList:
case GameAssetType.AudioMaterials:
case GameAssetType.SettingsFluid:
case GameAssetType.TextureList:
case GameAssetType.MusicSettings:
case GameAssetType.MixerSettings:
case GameAssetType.ReplayConfig:
case GameAssetType.StaticMesh:
case GameAssetType.AnimatedTexture:
case GameAssetType.Pins:
case GameAssetType.Instrument:
case GameAssetType.OutfitList:
case GameAssetType.PaintBrush:
case GameAssetType.Quest:
case GameAssetType.AnimationBank:
case GameAssetType.AnimationSet:
case GameAssetType.SkeletonMap:
case GameAssetType.SkeletonRegistry:
case GameAssetType.SkeletonAnimStyles:
case GameAssetType.AdventureSharedData:
case GameAssetType.AdventurePlayProfile:
case GameAssetType.AnimationMap:
case GameAssetType.CachedCostumeData:
case GameAssetType.DataLabels:
case GameAssetType.AdventureMaps:
default:
// If we don't know what asset type this is, just hope that whatever is asking for it knows what it is
return null;
}
}
catch
{
return null;
}
}
}