Try to fix missing generals

This commit is contained in:
Neo 2020-10-24 03:42:09 +01:00
parent 1ad0087a03
commit ceea825fa2
12 changed files with 285 additions and 41 deletions

View File

@ -8,6 +8,7 @@ using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using KamihamaWeb.Interfaces; using KamihamaWeb.Interfaces;
using KamihamaWeb.Models; using KamihamaWeb.Models;
using KamihamaWeb.Services;
using Marvin.Cache.Headers; using Marvin.Cache.Headers;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
@ -90,7 +91,7 @@ namespace KamihamaWeb.Controllers
{ {
var md5 = qs.Value.Substring(1); var md5 = qs.Value.Substring(1);
if (_masterService.EnglishMasterAssets.ContainsKey(url)) if (!url.StartsWith("scenario/json/general/") && _masterService.EnglishMasterAssets.ContainsKey(url))
{ {
if (_masterService.EnglishMasterAssets[url].Md5 == md5) if (_masterService.EnglishMasterAssets[url].Md5 == md5)
{ {
@ -120,17 +121,17 @@ namespace KamihamaWeb.Controllers
} }
var asset = await _diskCache.Get(url, md5); var asset = await _diskCache.Get(url, md5);
if (asset.Item1 == 404) if (asset.Result == DiskCacheResultType.NotFound)
{ {
return new APIResult(404, "asset not found"); return new APIResult(404, "asset not found");
} }
else if (asset.Item1 == 500) else if (asset.Result == DiskCacheResultType.Failed)
{ {
return new APIResult(503, "internal error fetching asset"); return new APIResult(503, "internal error fetching asset");
} }
else else
{ {
return new FileStreamResult(asset.Item2, "binary/octet-stream"); return new FileStreamResult(asset.Data, "binary/octet-stream");
} }
} }
else else

View File

@ -1,6 +1,7 @@
using System; using System;
using System.IO; using System.IO;
using System.Threading.Tasks; using System.Threading.Tasks;
using KamihamaWeb.Services;
namespace KamihamaWeb.Interfaces namespace KamihamaWeb.Interfaces
{ {
@ -11,6 +12,7 @@ namespace KamihamaWeb.Interfaces
public interface IDiskCacheSingleton: IDiskCacheService public interface IDiskCacheSingleton: IDiskCacheService
{ {
public Task<Tuple<int, Stream>> Get(string cacheItem, string versionMd5); public Task<DiskCacheItem> Get(string cacheItem, string versionMd5, bool forceOrigin = false);
public Task<string> Store(string filepath, byte[] storeContents, DiskCacheService.StoreType type);
} }
} }

View File

@ -0,0 +1,16 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using KamihamaWeb.Models;
namespace KamihamaWeb.Interfaces
{
public interface IMasterListBuilder
{
Task<Dictionary<string, GamedataAsset>> GenerateEnglishAssetList();
public GamedataAsset GetFileInformation(string file);
public Task<GamedataAsset> BuildScenarioGeneralJson(GamedataAsset generalAsset,
Dictionary<string, GamedataAsset> englishAssets);
}
}

View File

@ -2,6 +2,7 @@
using System.IO; using System.IO;
using System.Threading.Tasks; using System.Threading.Tasks;
using KamihamaWeb.Models; using KamihamaWeb.Models;
using KamihamaWeb.Services;
namespace KamihamaWeb.Interfaces namespace KamihamaWeb.Interfaces
{ {
@ -13,7 +14,7 @@ namespace KamihamaWeb.Interfaces
public interface IRestSharpTransient : IRestSharpClient public interface IRestSharpTransient : IRestSharpClient
{ {
Task<T> GetMasterJson<T>(string masterJsonEndpoint); Task<T> GetMasterJson<T>(string masterJsonEndpoint);
Task<Tuple<int, Stream>> FetchAsset(string item); Task<DiskCacheItem> FetchAsset(string item);
Task<string> GetAdditionalJson(string item); Task<string> GetAdditionalJson(string item);
} }
} }

View File

@ -51,7 +51,8 @@ namespace KamihamaWeb.Models
public enum AssetSourceType public enum AssetSourceType
{ {
Local, Local,
Remote Remote,
GeneralScript
} }
} }

View File

@ -0,0 +1,17 @@
using System.Collections;
using System.Collections.Generic;
using Newtonsoft.Json;
namespace KamihamaWeb.Models
{
public class ScenarioGeneral
{
[JsonProperty("story")]
public Dictionary<string, dynamic> story { get; set; }
[JsonProperty("version")]
public int version { get; set; }
}
}

View File

@ -26,18 +26,42 @@ namespace KamihamaWeb.Services
} }
CacheDirectory = config["MagiRecoServer:CacheDirectory"]; CacheDirectory = config["MagiRecoServer:CacheDirectory"];
ScenarioCacheDirectory = $"{config["MagiRecoServer:CacheDirectory"]}/scenario/json/general/";
Directory.CreateDirectory(CacheDirectory); Directory.CreateDirectory(CacheDirectory);
Directory.CreateDirectory(ScenarioCacheDirectory);
} }
public Guid Guid { get; } public Guid Guid { get; }
private IRestSharpTransient Rest { get; set; } private IRestSharpTransient Rest { get; set; }
private string CacheDirectory { get; set; } = ""; private string CacheDirectory { get; set; } = "";
private string ScenarioCacheDirectory { get; set; } = "";
public async Task<Tuple<int, Stream>> Get(string cacheItem, string versionMd5) public async Task<DiskCacheItem> Get(string cacheItem, string versionMd5, bool forceOrigin = false)
{ {
var filename = CryptUtil.CalculateSha256(cacheItem + "?" + versionMd5); var filename = CryptUtil.CalculateSha256(cacheItem + "?" + versionMd5);
var filePath = Path.Combine(CacheDirectory, filename); var filePath = Path.Combine(CacheDirectory, filename);
if (!forceOrigin && cacheItem.StartsWith("scenario/json/general"))
{
var generalJson = Path.Combine(CacheDirectory, cacheItem + versionMd5);
if (File.Exists(generalJson))
{
return new DiskCacheItem()
{
Data = File.Open(generalJson, FileMode.Open, FileAccess.Read, FileShare.Read),
Result = DiskCacheResultType.Success
};
}
else
{
Log.Warning($"Cache item {generalJson} not found!");
return new DiskCacheItem()
{
Result = DiskCacheResultType.Failed
};
}
}
if (File.Exists(filePath)) if (File.Exists(filePath))
{ {
Log.Debug($"Loading {cacheItem} from disk ({filePath})."); Log.Debug($"Loading {cacheItem} from disk ({filePath}).");
@ -56,7 +80,10 @@ namespace KamihamaWeb.Services
} }
else else
{ {
return new Tuple<int, Stream>(0, stream); return new DiskCacheItem()
{
Data = stream
};
} }
} }
catch (IOException) // File in use, wait catch (IOException) // File in use, wait
@ -66,14 +93,39 @@ namespace KamihamaWeb.Services
} }
} }
Log.Warning($"Max loops exceeded in DiskCacheService.Get() for {cacheItem}."); Log.Warning($"Max loops exceeded in DiskCacheService.Get() for {cacheItem}.");
return new Tuple<int, Stream>(500, null); return new DiskCacheItem()
{
Result = DiskCacheResultType.Failed
};
} }
Log.Information($"Fetching {cacheItem}."); Log.Information($"Fetching {cacheItem}.");
return await FastFetch(cacheItem, filePath, versionMd5); return await FastFetch(cacheItem, filePath, versionMd5);
} }
private async Task<Tuple<int, Stream>> FastFetch(string item, string path, string md5) public async Task<string> Store(string filepath, byte[] storeContents, StoreType type)
{
string storePath;
switch (type)
{
case StoreType.ScenarioGeneral:
var md5 = CryptUtil.CalculateMd5Bytes(storeContents);
storePath = Path.Combine(CacheDirectory, filepath + md5);
await File.WriteAllBytesAsync(storePath, storeContents);
break;
default:
throw new Exception("Invalid StoreType.");
}
return storePath;
}
public enum StoreType
{
ScenarioGeneral
}
private async Task<DiskCacheItem> FastFetch(string item, string path, string md5)
{ {
var maxLoops = 5; var maxLoops = 5;
var deleteFlag = false; var deleteFlag = false;
@ -89,19 +141,23 @@ namespace KamihamaWeb.Services
if (file.Length > 0) if (file.Length > 0)
{ {
return new Tuple<int, Stream>(0, file); return new DiskCacheItem()
{
Data = file,
Result = DiskCacheResultType.Success
};
} }
var stream = await Rest.FetchAsset(item + $"?{md5}"); var stream = await Rest.FetchAsset(item + $"?{md5}");
if (stream.Item2 == null) if (stream.Data == null)
{ {
deleteFlag = true; deleteFlag = true;
return stream; return stream;
} }
((MemoryStream) stream.Item2).WriteTo(file); ((MemoryStream) stream.Data).WriteTo(file);
stream.Item2.Seek(0, SeekOrigin.Begin); stream.Data.Seek(0, SeekOrigin.Begin);
return stream; return stream;
} }
catch (Exception) catch (Exception)
@ -126,9 +182,25 @@ namespace KamihamaWeb.Services
} }
Log.Warning($"Max loops exceeded in DiskCacheService.FastFetch() for {item}."); Log.Warning($"Max loops exceeded in DiskCacheService.FastFetch() for {item}.");
File.Delete(path); File.Delete(path);
return new Tuple<int, Stream>(500, null); return new DiskCacheItem()
{
Result = DiskCacheResultType.Failed
};
} }
} }
public class DiskCacheItem
{
public Stream Data { get; set; } = null;
public DiskCacheResultType Result { get; set; } = DiskCacheResultType.Success;
}
public enum DiskCacheResultType
{
Success = 0,
Failed = 500,
NotFound = 404
}
} }

View File

@ -2,27 +2,41 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Security.Cryptography;
using System.Text; using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading.Tasks; using System.Threading.Tasks;
using KamihamaWeb.Interfaces;
using KamihamaWeb.Models; using KamihamaWeb.Models;
using KamihamaWeb.Util;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Caching.StackExchangeRedis;
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Serilog; using Serilog;
using StackExchange.Redis;
namespace KamihamaWeb.Util namespace KamihamaWeb.Services
{ {
public class MasterListBuilder public class MasterListBuilder: IMasterListBuilder
{ {
private Regex multiPartRegex = new Regex(@"\.a[a-z]{2}"); // A bit crude private readonly Regex _multiPartRegex = new Regex(@"\.a[a-z]{2}"); // A bit crude
public MasterListBuilder()
private IDiskCacheSingleton _disk;
private IDatabase _cache;
public MasterListBuilder(IDiskCacheSingleton disk, IDistributedCache cache)
{ {
BasePathLength = BasePathLength =
@"MagiRecoStatic/magica/resource/download/asset/master/resource/".Length; @"MagiRecoStatic/magica/resource/download/asset/master/resource/".Length;
_cache = ((RedisCache)cache).GetConnection().GetDatabase();
_disk = disk;
} }
private int BasePathLength { get; } private int BasePathLength { get; }
private string StaticDirectory { get; set; } = "MagiRecoStatic/";
public async Task<Dictionary<string, GamedataAsset>> GenerateEnglishAssetList() public async Task<Dictionary<string, GamedataAsset>> GenerateEnglishAssetList()
{ {
if (File.Exists("en_cache.json")) if (File.Exists("en_cache.json"))
@ -48,9 +62,9 @@ namespace KamihamaWeb.Util
if (Directory.Exists("MagiRecoStatic/")) if (Directory.Exists(StaticDirectory))
{ {
List<string> files = Directory.GetFiles("MagiRecoStatic/magica/resource/download/asset/master", List<string> files = Directory.GetFiles(Path.Combine(StaticDirectory, "magica/resource/download/asset/master"),
"*.*", "*.*",
SearchOption.AllDirectories).ToList(); SearchOption.AllDirectories).ToList();
@ -93,7 +107,7 @@ namespace KamihamaWeb.Util
// Remove multi-part files // Remove multi-part files
foreach (var asset in englishAssets) foreach (var asset in englishAssets)
{ {
if (multiPartRegex.IsMatch(asset.Value.Path.Substring(asset.Value.Path.Length - 4))) if (_multiPartRegex.IsMatch(asset.Value.Path.Substring(asset.Value.Path.Length - 4)))
{ {
Log.Debug($"Removing duplicate asset {asset.Key}"); Log.Debug($"Removing duplicate asset {asset.Key}");
englishAssets.Remove(asset.Value.Path); englishAssets.Remove(asset.Value.Path);
@ -101,7 +115,7 @@ namespace KamihamaWeb.Util
else if (asset.Key.StartsWith("image_native/mini/") else if (asset.Key.StartsWith("image_native/mini/")
|| asset.Key.StartsWith("image_native/live2d/") || asset.Key.StartsWith("image_native/live2d/")
//|| asset.Key.StartsWith("image_native/scene/gacha") //|| asset.Key.StartsWith("image_native/scene/gacha")
|| asset.Key.StartsWith("scenario/json/general/") //|| asset.Key.StartsWith("scenario/json/general/")
|| asset.Key.StartsWith("scenario/json/oneShot/") || asset.Key.StartsWith("scenario/json/oneShot/")
|| asset.Key.StartsWith("image_native/scene/event/") || asset.Key.StartsWith("image_native/scene/event/")
|| asset.Key.StartsWith("image_native/scene/emotion/") || asset.Key.StartsWith("image_native/scene/emotion/")
@ -163,5 +177,76 @@ namespace KamihamaWeb.Util
return asset; return asset;
} }
public async Task<GamedataAsset> BuildScenarioGeneralJson(GamedataAsset generalAsset, Dictionary<string, GamedataAsset> englishAssets)
{
Log.Information($"Building scenario JSON for {generalAsset.Path}.");
if (!englishAssets.ContainsKey(generalAsset.Path)) // No english to replace with (yet)
{
Log.Warning($"No english asset for {generalAsset.Path}! This should be caught earlier!");
return generalAsset;
}
// Fetch JP asset
DiskCacheItem jpAsset = await _disk.Get(generalAsset.Path, generalAsset.Md5, true);
if (jpAsset.Result != DiskCacheResultType.Success)
{
Log.Warning("An error has occurred fetching a general scenario asset.");
return generalAsset;
}
// Merge assets
var enPath = Path.Combine(StaticDirectory, "magica/resource/download/asset/master/resource",
generalAsset.Path);
if (!File.Exists(enPath))
{
Log.Warning($"File {enPath} does not exist!");
return generalAsset;
}
var mergedAsset = MergeScenarioGeneral(
Encoding.UTF8.GetString(
CryptUtil.ReadFully(jpAsset.Data)
),
await File.ReadAllTextAsync(enPath)
);
var storeFilePath = await _disk.Store(generalAsset.Path, Encoding.UTF8.GetBytes(mergedAsset),
DiskCacheService.StoreType.ScenarioGeneral);
generalAsset.AssetSource = AssetSourceType.GeneralScript;
generalAsset.Md5 = CryptUtil.CalculateMd5File(storeFilePath);
generalAsset.FileList[0].Size = new FileInfo(storeFilePath).Length;
return generalAsset;
}
private string MergeScenarioGeneral(string jp, string en)
{
if (jp == en)
{
return jp;
}
var jp_json = JsonConvert.DeserializeObject<ScenarioGeneral>(jp);
var en_json = JsonConvert.DeserializeObject<ScenarioGeneral>(en);
foreach (var item in jp_json.story)
{
if (!en_json.story.ContainsKey(item.Key))
{
Log.Information($"Adding key {item.Key}.");
en_json.story[item.Key] = item.Value;
}
}
/*jp_json.Merge(en_json, new JsonMergeSettings()
{
MergeArrayHandling = MergeArrayHandling.Union,
MergeNullValueHandling = MergeNullValueHandling.Ignore,
});*/
return JsonConvert.SerializeObject(en_json);
}
} }
} }

View File

@ -21,12 +21,14 @@ namespace KamihamaWeb.Services
private IDatabase _cache { get; set; } private IDatabase _cache { get; set; }
private IConfiguration _config { get; set; } private IConfiguration _config { get; set; }
private IRestSharpTransient _rest { get; set; } private IRestSharpTransient _rest { get; set; }
private IMasterListBuilder _builder { get; set; }
public MasterService( public MasterService(
IDistributedCache cache, IDistributedCache cache,
IConfiguration config, IConfiguration config,
IRestSharpTransient rest IRestSharpTransient rest,
) : this(Guid.NewGuid(), cache, config, rest) IMasterListBuilder builder
) : this(Guid.NewGuid(), cache, config, rest, builder)
{ {
} }
@ -34,13 +36,15 @@ namespace KamihamaWeb.Services
Guid guid, Guid guid,
IDistributedCache cache, IDistributedCache cache,
IConfiguration config, IConfiguration config,
IRestSharpTransient rest IRestSharpTransient rest,
IMasterListBuilder builder
) )
{ {
Guid = guid; Guid = guid;
_config = config; _config = config;
_cache = ((RedisCache) cache).GetConnection().GetDatabase(); _cache = ((RedisCache) cache).GetConnection().GetDatabase();
_rest = rest; _rest = rest;
_builder = builder;
Task.Run(Initialize); Task.Run(Initialize);
} }
public Guid Guid { get; set; } public Guid Guid { get; set; }
@ -79,9 +83,13 @@ namespace KamihamaWeb.Services
Log.Information("Configuring master list..."); Log.Information("Configuring master list...");
var postProcessingGeneralScenario = new Dictionary<string, GamedataAsset>();
long counterReplace = 0; long counterReplace = 0;
long counterSkip = 0; long counterSkip = 0;
long counterNew = 0; long counterNew = 0;
long counterPost = 0;
foreach (var assetType in workGamedataAssets) foreach (var assetType in workGamedataAssets)
{ {
var readyAssets = new Dictionary<string, GamedataAsset>(); var readyAssets = new Dictionary<string, GamedataAsset>();
@ -90,7 +98,12 @@ namespace KamihamaWeb.Services
// Replace with english assets as needed // Replace with english assets as needed
if (EnglishMasterAssets.ContainsKey(asset.Path)) if (EnglishMasterAssets.ContainsKey(asset.Path))
{ {
if (EnglishMasterAssets[asset.Path].Md5 != asset.Md5) if (asset.Path.StartsWith("scenario/json/general/"))
{
postProcessingGeneralScenario.Add(asset.Path, asset);
counterPost++;
}
else if (EnglishMasterAssets[asset.Path].Md5 != asset.Md5)
{ {
//Log.Debug($"Replacing Japanese asset with English asset for {asset.Path}."); //Log.Debug($"Replacing Japanese asset with English asset for {asset.Path}.");
readyAssets.Add(asset.Path, EnglishMasterAssets[asset.Path]); readyAssets.Add(asset.Path, EnglishMasterAssets[asset.Path]);
@ -112,7 +125,7 @@ namespace KamihamaWeb.Services
} }
GamedataAssets.Add(assetType.Key, readyAssets); GamedataAssets.Add(assetType.Key, readyAssets);
} }
Log.Information($"Finished setting up. {counterReplace} replaced assets, {counterSkip} duplicate assets, {counterNew} new assets."); Log.Information($"Finished setting up. {counterReplace} replaced assets, {counterSkip} duplicate assets, {counterNew} new assets, {counterPost} assets for post processing.");
// Add scripts // Add scripts
foreach (var asset in EnglishMasterAssets) foreach (var asset in EnglishMasterAssets)
@ -128,6 +141,14 @@ namespace KamihamaWeb.Services
}); });
} }
} }
// Post processing
foreach (var asset in postProcessingGeneralScenario)
{
var builtJson = await _builder.BuildScenarioGeneralJson(asset.Value, EnglishMasterAssets);
GamedataAssets["asset_main"].Add(builtJson.Path, builtJson);
}
IsReady = true; IsReady = true;
return true; return true;
} }
@ -155,8 +176,8 @@ namespace KamihamaWeb.Services
await Task.Delay(delay); await Task.Delay(delay);
delay *= 2; delay *= 2;
} }
var builder= new MasterListBuilder();
var lists = await builder.GenerateEnglishAssetList(); var lists = await _builder.GenerateEnglishAssetList();
if (lists != null) if (lists != null)
{ {
@ -189,11 +210,6 @@ namespace KamihamaWeb.Services
} }
} }
//public async Task<FileStream> ProvideFile(string filePath)
//{
// }
public async Task<string> ProvideJson(string which) public async Task<string> ProvideJson(string which)
{ {
if (which == "asset_config") if (which == "asset_config")

View File

@ -72,7 +72,7 @@ namespace KamihamaWeb.Services
return result.Content; return result.Content;
} }
public async Task<Tuple<int, Stream>> FetchAsset(string item) public async Task<DiskCacheItem> FetchAsset(string item)
{ {
var request = new RestRequest("resource/" + item, Method.GET); var request = new RestRequest("resource/" + item, Method.GET);
@ -82,17 +82,27 @@ namespace KamihamaWeb.Services
if (response.StatusCode == HttpStatusCode.NotFound || response.ContentType == "text/html") if (response.StatusCode == HttpStatusCode.NotFound || response.ContentType == "text/html")
{ {
return new Tuple<int, Stream>(404, null); return new DiskCacheItem()
{
Result = DiskCacheResultType.NotFound
};
} }
var stream = new MemoryStream(response.RawBytes); var stream = new MemoryStream(response.RawBytes);
return new Tuple<int, Stream>(0, stream); return new DiskCacheItem()
{
Result = DiskCacheResultType.Success,
Data = stream
};
} }
catch (WebException ex) catch (WebException ex)
{ {
Log.Warning($"Web exception thrown: Status code {ex.Status}, {ex.ToString()}"); Log.Warning($"Web exception thrown: Status code {ex.Status}, {ex.ToString()}");
} }
return new Tuple<int, Stream>(500, null); return new DiskCacheItem()
{
Result = DiskCacheResultType.Failed
};
} }
} }
} }

View File

@ -4,6 +4,7 @@ using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using KamihamaWeb.Interfaces; using KamihamaWeb.Interfaces;
using KamihamaWeb.Services; using KamihamaWeb.Services;
using KamihamaWeb.Util;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.HttpsPolicy; using Microsoft.AspNetCore.HttpsPolicy;
@ -46,6 +47,7 @@ namespace KamihamaWeb
services.AddTransient<IRestSharpTransient, RestSharpClient>(); services.AddTransient<IRestSharpTransient, RestSharpClient>();
services.AddSingleton<IMasterSingleton, MasterService>(); services.AddSingleton<IMasterSingleton, MasterService>();
services.AddSingleton<IDiskCacheSingleton, DiskCacheService>(); services.AddSingleton<IDiskCacheSingleton, DiskCacheService>();
services.AddTransient<IMasterListBuilder, MasterListBuilder>();
} }
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline. // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.

View File

@ -32,5 +32,26 @@ namespace KamihamaWeb.Util
return sb.ToString(); return sb.ToString();
} }
public static byte[] ReadFully(Stream input)
{
using MemoryStream ms = new MemoryStream();
input.CopyTo(ms);
return ms.ToArray();
}
public static object CalculateMd5Bytes(byte[] storeContents)
{
using var md5 = MD5.Create();
using var stream = new MemoryStream(storeContents);
StringBuilder sb = new StringBuilder();
foreach (byte b in md5.ComputeHash(stream))
{
sb.Append(b.ToString("x2"));
}
return sb.ToString();
}
} }
} }