mirror of
https://github.com/hykilpikonna/AquaDX.git
synced 2026-08-09 12:41:56 -05:00
374 lines
16 KiB
C#
374 lines
16 KiB
C#
using System;
|
|
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Reflection;
|
|
using AquaMai.Config.Attributes;
|
|
using AquaMai.Core.Helpers;
|
|
using Manager;
|
|
using MelonLoader;
|
|
using MelonLoader.TinyJSON;
|
|
using Net.VO.Mai2;
|
|
|
|
namespace AquaMai.Mods.Fix.Stability;
|
|
|
|
[ConfigSection(exampleHidden: true, defaultOn: true)]
|
|
public class SanitizeUserData
|
|
{
|
|
public static void OnBeforePatch()
|
|
{
|
|
NetPacketHook.OnNetPacketComplete += OnNetPacketComplete;
|
|
}
|
|
|
|
private static Variant OnNetPacketComplete(string api, Variant request, Variant response)
|
|
{
|
|
var handlerMap = new Dictionary<string, Action<ProxyObject, ProxyObject>>
|
|
{
|
|
// ["GetUserPreviewApi"] = /* no need */,
|
|
// ["GetUserFriendCheckApi"] = /* no need */,
|
|
["GetUserDataApi"] = OnUserDataResponse,
|
|
// ["GetUserCardApi"] = = /* no need */,
|
|
["GetUserCharacterApi"] = (_, response) => FilterListResponseById(api, response, "userCharacterList", "characterId", "GetCharas"),
|
|
["GetUserItemApi"] = OnUserItemResponse,
|
|
["GetUserCourseApi"] = (_, response) => FilterListResponseById(api, response, "userCourseList", "courseId", "GetCourses"),
|
|
["GetUserChargeApi"] = (_, response) => FilterListResponseById(api, response, "userChargeList", "chargeId", "GetTickets"),
|
|
["GetUserFavoriteApi"] = OnUserFavoriteResponse,
|
|
// ["GetUserGhostApi"] = /* no need */,
|
|
["GetUserMapApi"] = (_, response) => FilterListResponseById(api, response, "userMapList", "mapId", "GetMapDatas"),
|
|
// ["GetUserLoginBonusApi"] = /* no need */,
|
|
// ["GetUserRegionApi"] = /* no need */,
|
|
// ["GetUserRecommendRateMusicApi"] = /* no need */,
|
|
// ["GetUserRecommendSelectionMusicApi"] = /* no need */,
|
|
// ["GetUserOptionApi"] = /* no need */,
|
|
["GetUserExtendApi"] = OnUserExtendResponse,
|
|
// ["GetUserRatingApi"] = /* no need */,
|
|
// ["GetUserMusicApi"] = /* no need */,
|
|
// ["GetUserPortraitApi"] = /* no need */,
|
|
// ["GetUserActivityApi"] = /* no need */,
|
|
// ["GetUserFriendSeasonRankingApi"] = /* no need */,
|
|
["GetUserFavoriteItemApi"] = OnUserFavoriteItemResponse,
|
|
// ["GetUserRivalDataApi"] = /* no need */,
|
|
// ["GetUserRivalMusicApi"] = /* no need */,
|
|
// ["GetUserMissionDataApi"] = /* no need */,
|
|
// ["GetUserFriendBonusApi"] = /* no need */,
|
|
// ["GetUserIntimateApi"] = /* no need */,
|
|
// ["GetUserShopStockApi"] = /* no need */,
|
|
["GetUserKaleidxScopeApi"] = (_, response) => FilterListResponseById(api, response, "userKaleidxScopeList", "gateId", "GetKaleidxScopeKeys"),
|
|
// ["GetUserScoreRankingApi"] = /* no need */,
|
|
// ["GetUserNewItemApi"] = /* no need */,
|
|
// ["GetUserNewItemListApi"] = /* no need */,
|
|
};
|
|
if (handlerMap.TryGetValue(api, out var handler))
|
|
{
|
|
var requestObject = request is ProxyObject reqObj ? reqObj : [];
|
|
var responseObject = response is ProxyObject resObj ? resObj : [];
|
|
handler(requestObject, responseObject);
|
|
return responseObject;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
private static void OnUserDataResponse(ProxyObject _, ProxyObject response)
|
|
{
|
|
var userData = GetObjectOrSetDefault(response, "userData");
|
|
SanitizeItemIdField(userData, "iconId", "GetIcons", false);
|
|
SanitizeItemIdField(userData, "plateId", "GetPlates", false);
|
|
SanitizeItemIdField(userData, "titleId", "GetTitles", false);
|
|
SanitizeItemIdField(userData, "partnerId", "GetPartners", false);
|
|
SanitizeItemIdField(userData, "frameId", "GetFrames", false);
|
|
SanitizeItemIdField(userData, "selectMapId", "GetMapDatas", false);
|
|
var charaSlot = GetArrayOrSetDefault(userData, "charaSlot");
|
|
for (var i = 0; i < 5; i++)
|
|
{
|
|
if (charaSlot.Count <= i)
|
|
{
|
|
charaSlot.Add(new ProxyNumber(0));
|
|
}
|
|
else if (
|
|
!JsonHelper.TryToInt32(charaSlot[i], out var charaSlotEntryInt) ||
|
|
!SafelyCheckItemId("GetCharas", charaSlotEntryInt))
|
|
{
|
|
charaSlot.Add(new ProxyNumber(0));
|
|
MelonLogger.Warning($"[SanitizeUserData] Filtered out invalid chara {charaSlot[i].ToJSON()} at index {i}");
|
|
}
|
|
}
|
|
userData["charaSlot"] = ToProxyArray(charaSlot.Take(5));
|
|
}
|
|
|
|
private static void OnUserExtendResponse(ProxyObject _, ProxyObject response)
|
|
{
|
|
var userExtend = GetObjectOrSetDefault(response, "userExtend");
|
|
SanitizeItemIdField(userExtend, "selectMusicId", "GetMusics", true);
|
|
SanitizeItemIdField(userExtend, "selectDifficultyId", "GetMusicDifficultys", true);
|
|
// categoryIndex?
|
|
// musicIndex?
|
|
SanitizeEnumFieldIfDefined(userExtend, "selectScoreType", ResolveEnumType("MAI2System.ConstParameter+ScoreKind"));
|
|
SanitizeEnumFieldIfDefined(userExtend, "selectResultScoreViewType", ResolveEnumType("Process.ResultProcess+ResultScoreViewType"));
|
|
SanitizeEnumFieldIfDefined(userExtend, "sortCategorySetting", ResolveEnumType("DB.SortTabID"));
|
|
SanitizeEnumFieldIfDefined(userExtend, "sortMusicSetting", ResolveEnumType("DB.SortMusicID"));
|
|
SanitizeEnumFieldIfDefined(userExtend, "playStatusSetting", ResolveEnumType("DB.PlaystatusTabID"));
|
|
}
|
|
|
|
private static void OnUserItemResponse(ProxyObject request, ProxyObject response)
|
|
{
|
|
var requestKind = (ItemKind)(
|
|
request.TryGetValue("nextIndex", out var nextIndexVariant) &&
|
|
JsonHelper.TryToInt64(nextIndexVariant, out var nextIndex)
|
|
? nextIndex / 10000000000L
|
|
: 0);
|
|
|
|
var filteredOutCount = FilterListResponse(
|
|
null,
|
|
response,
|
|
"userItemList",
|
|
userItem =>
|
|
JsonHelper.TryToInt32(userItem["itemId"], out var itemId) &&
|
|
SafelyCheckItemIdByKind(requestKind, itemId));
|
|
|
|
if (filteredOutCount > 0)
|
|
{
|
|
MelonLogger.Warning($"[SanitizeUserData] Filtered out {filteredOutCount} invalid entries of kind {requestKind} in GetUserItemApi");
|
|
}
|
|
}
|
|
|
|
private static void OnUserFavoriteResponse(ProxyObject request, ProxyObject response)
|
|
{
|
|
var requestKind = (ItemKind)(
|
|
request.TryGetValue("itemKind", out var itemKindVariant) &&
|
|
JsonHelper.TryToInt64(itemKindVariant, out var itemKind)
|
|
? itemKind
|
|
: 0);
|
|
|
|
var userFavorite = GetObjectOrSetDefault(response, "userFavorite");
|
|
var itemIdList = GetArrayOrSetDefault(userFavorite, "itemIdList");
|
|
var validItemIdList = itemIdList
|
|
.Select(itemIdVariant =>
|
|
JsonHelper.TryToInt32(itemIdVariant, out var itemId) &&
|
|
SafelyCheckItemIdByKind(requestKind, itemId)
|
|
? itemIdVariant
|
|
: null)
|
|
.Where(itemIdVariant => itemIdVariant != null)
|
|
.ToList();
|
|
userFavorite["itemIdList"] = ToProxyArray(validItemIdList);
|
|
|
|
var filteredOutCount = itemIdList.Count - validItemIdList.Count;
|
|
if (filteredOutCount > 0)
|
|
{
|
|
MelonLogger.Warning($"[SanitizeUserData] Filtered out {filteredOutCount} invalid entries of kind {requestKind} in GetUserFavoriteApi");
|
|
}
|
|
}
|
|
|
|
private static void OnUserFavoriteItemResponse(ProxyObject request, ProxyObject response)
|
|
{
|
|
// Older versions of the game don't have the FavoriteItemKind enum
|
|
var enumType = AppDomain.CurrentDomain
|
|
.GetAssemblies()
|
|
.SelectMany(assembly => assembly.GetTypes())
|
|
.FirstOrDefault(type => type.FullName == "Net.VO.Mai2.FavoriteItemKind" && type.IsEnum);
|
|
|
|
var requestKind = Enum.ToObject(
|
|
enumType,
|
|
request.TryGetValue("kind", out var kindVariant) &&
|
|
JsonHelper.TryToInt64(kindVariant, out var kind)
|
|
? kind
|
|
: 0);
|
|
|
|
var userFavoriteItemList = GetArrayOrSetDefault(response, "userFavoriteItemList");
|
|
var validItemList = userFavoriteItemList
|
|
.Select(itemariant =>
|
|
itemariant is ProxyObject itemObject &&
|
|
itemObject.TryGetValue("id", out var idVariant) &&
|
|
JsonHelper.TryToInt32(idVariant, out var id)
|
|
? (id, itemObject)
|
|
: (0, null))
|
|
.Where(tuple => tuple.itemObject != null)
|
|
.Where(requestKind.ToString() switch
|
|
{
|
|
"FavoriteMusic" => tuple => SafelyCheckItemId("GetMusics", tuple.id),
|
|
"RivalScore" => _ => true,
|
|
_ => _ => true // Fail-safe for newly introduced favorite item kinds after mod release
|
|
})
|
|
.Select(tuple => tuple.itemObject)
|
|
.ToList();
|
|
response["userFavoriteItemList"] = ToProxyArray(validItemList);
|
|
|
|
var filteredOutCount = userFavoriteItemList.Count - validItemList.Count;
|
|
if (filteredOutCount > 0)
|
|
{
|
|
MelonLogger.Warning($"[SanitizeUserData] Filtered out {filteredOutCount} invalid entries of kind {requestKind} in GetUserFavoriteItemApi");
|
|
}
|
|
}
|
|
|
|
private static ProxyObject GetObjectOrSetDefault(ProxyObject response, string fieldName)
|
|
{
|
|
if (
|
|
!response.TryGetValue(fieldName, out var fieldVariant) ||
|
|
fieldVariant is not ProxyObject field)
|
|
{
|
|
field = [];
|
|
response[fieldName] = field;
|
|
}
|
|
return field;
|
|
}
|
|
|
|
private static ProxyArray GetArrayOrSetDefault(ProxyObject response, string fieldName)
|
|
{
|
|
if (
|
|
!response.TryGetValue(fieldName, out var fieldVariant) ||
|
|
fieldVariant is not ProxyArray field)
|
|
{
|
|
field = [];
|
|
response[fieldName] = field;
|
|
}
|
|
return field;
|
|
}
|
|
|
|
private static int FilterListResponse(string logApiName, ProxyObject response, string listFieldName, Func<ProxyObject, bool> isValidEntry)
|
|
{
|
|
var before = GetArrayOrSetDefault(response, listFieldName);
|
|
var after = GetArrayOrSetDefault(response, listFieldName)
|
|
.Select(entry => entry is ProxyObject entryObject ? entryObject : null)
|
|
.Select(entry => isValidEntry(entry) ? entry : null)
|
|
.Where(entry => entry != null)
|
|
.ToList();
|
|
response[listFieldName] = ToProxyArray(after);
|
|
var filteredOutCount = before.Count - after.Count;
|
|
if (logApiName != null && filteredOutCount > 0)
|
|
{
|
|
MelonLogger.Warning($"[SanitizeUserData] Filtered out {filteredOutCount} invalid entries in {logApiName}");
|
|
}
|
|
return filteredOutCount;
|
|
}
|
|
|
|
private static int FilterListResponseById(string logApiName, ProxyObject response, string listFieldName, string idFieldName, string dataManagerGetDictionaryMethod) =>
|
|
FilterListResponse(
|
|
logApiName,
|
|
response,
|
|
listFieldName,
|
|
listEntry =>
|
|
listEntry.TryGetValue(idFieldName, out var idVariant) &&
|
|
JsonHelper.TryToInt32(idVariant, out var idInt) &&
|
|
SafelyCheckItemId(dataManagerGetDictionaryMethod, idInt));
|
|
|
|
private static void SanitizeInt32Field(ProxyObject obj, string fieldName, Func<int, bool> isValid, int defaultValue)
|
|
{
|
|
if (
|
|
!obj.TryGetValue(fieldName, out var fieldVariant) ||
|
|
!JsonHelper.TryToInt32(fieldVariant, out var fieldValue) ||
|
|
!isValid(fieldValue))
|
|
{
|
|
MelonLogger.Warning($"[SanitizeUserData] Set value of invalid int32 field {fieldName} from {fieldVariant?.ToJSON() ?? "null"} to {defaultValue}");
|
|
obj[fieldName] = new ProxyNumber(defaultValue);
|
|
}
|
|
}
|
|
|
|
private static void SanitizeEnumFieldIfDefined(ProxyObject obj, string fieldName, System.Type enumType) =>
|
|
SanitizeInt32Field(
|
|
obj,
|
|
fieldName,
|
|
value => enumType == null || Enum.IsDefined(enumType, value),
|
|
enumType == null ? 0 : (int)enumType.GetEnumValues().GetValue(0));
|
|
|
|
private static void SanitizeItemIdField(ProxyObject obj, string fieldName, string dataManagerGetDictionaryMethod, bool defaultZero) =>
|
|
SanitizeInt32Field(
|
|
obj,
|
|
fieldName,
|
|
itemId => (itemId == 0 && defaultZero) || SafelyCheckItemId(dataManagerGetDictionaryMethod, itemId),
|
|
defaultZero ? 0 : SafelyGetDefaultItemId(dataManagerGetDictionaryMethod));
|
|
|
|
// The corresponding DataManager methods may not exist in all game versions
|
|
private static object SafelyGetDataMangerDictionary(string dataManagerGetDictionaryMethod)
|
|
{
|
|
return typeof(DataManager)
|
|
.GetMethod(dataManagerGetDictionaryMethod, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)
|
|
.Invoke(DataManager.Instance, []);
|
|
}
|
|
|
|
private static int SafelyGetDefaultItemId(string dataManagerGetDictionaryMethod)
|
|
{
|
|
var dictionary = SafelyGetDataMangerDictionary(dataManagerGetDictionaryMethod);
|
|
var enumerator = dictionary
|
|
.GetType()
|
|
.GetMethod("GetEnumerator", BindingFlags.Instance | BindingFlags.Public)
|
|
.Invoke(dictionary, []) as IEnumerator;
|
|
return !enumerator.MoveNext()
|
|
? 0
|
|
: enumerator.Current
|
|
.GetType()
|
|
.GetProperty("Key", BindingFlags.Instance | BindingFlags.Public)
|
|
.GetValue(enumerator.Current) as int? ?? 0;
|
|
}
|
|
|
|
private static bool SafelyCheckItemId(string dataManagerGetDictionaryMethod, int itemId)
|
|
{
|
|
var dictionary = SafelyGetDataMangerDictionary(dataManagerGetDictionaryMethod);
|
|
return dictionary
|
|
.GetType()
|
|
.GetMethod("ContainsKey", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)
|
|
.Invoke(dictionary, [itemId]) as bool? ?? false;
|
|
}
|
|
|
|
private static bool SafelyCheckItemIdByKind(ItemKind itemKind, int itemId) =>
|
|
itemKind.ToString() switch
|
|
{
|
|
"Plate" => SafelyCheckItemId("GetPlates", itemId),
|
|
"Title" => SafelyCheckItemId("GetTitles", itemId),
|
|
"Icon" => SafelyCheckItemId("GetIcons", itemId),
|
|
"Present" => DataManager.Instance.ConvertPresentID2Item(itemId, out _, out _),
|
|
// It's safe to have invalid music IDs in the user item list
|
|
"Music" => true,
|
|
"MusicMas" => true,
|
|
"MusicRem" => true,
|
|
"MusicSrg" => true,
|
|
"Character" => SafelyCheckItemId("GetCharas", itemId),
|
|
"Partner" => SafelyCheckItemId("GetPartners", itemId),
|
|
"Frame" => SafelyCheckItemId("GetFrames", itemId),
|
|
"Ticket" => SafelyCheckItemId("GetTickets", itemId),
|
|
"Mile" => true,
|
|
"IntimateItem" => true,
|
|
"KaleidxScopeKey" => SafelyCheckItemId("GetKaleidxScopeKeys", itemId),
|
|
_ => true, // Fail-safe for newly introduced item kinds after mod release
|
|
};
|
|
|
|
private static ProxyArray ToProxyArray(IEnumerable<Variant> values)
|
|
{
|
|
var array = new ProxyArray();
|
|
foreach (var value in values)
|
|
{
|
|
array.Add(value);
|
|
}
|
|
return array;
|
|
}
|
|
|
|
private static System.Type[] allTheTypes = null;
|
|
|
|
private static System.Type ResolveEnumType(string enumName)
|
|
{
|
|
#if DEBUG
|
|
MelonLogger.Msg($"[SanitizeUserData] Resolving enum {enumName}");
|
|
#endif
|
|
allTheTypes ??= AppDomain.CurrentDomain
|
|
.GetAssemblies()
|
|
.SelectMany(a =>
|
|
{
|
|
try
|
|
{
|
|
return a.GetTypes();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
MelonLogger.Warning($"[SanitizeUserData] Unable to parse assembly {a.FullName}: {ex.Message}");
|
|
return [];
|
|
}
|
|
}).ToArray();
|
|
var result = allTheTypes.FirstOrDefault(type => type.FullName == enumName && type.IsEnum);
|
|
#if DEBUG
|
|
MelonLogger.Msg($"[SanitizeUserData] Resolved: {result}");
|
|
#endif
|
|
if (result == null)
|
|
{
|
|
MelonLogger.Warning($"[SanitizeUserData] Unable to resolve enum {enumName}");
|
|
}
|
|
return result;
|
|
}
|
|
} |