Please disclose if any significant portion of your mod was created using AI tools by adding the 'AI Generated' category. Failing to do so may result in the mod being removed from Thunderstore.
Decompiled source of PPR v1.2.11
PPR.dll
Decompiled 3 days ago
The result has been truncated due to the large size, download it to view full contents!
using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Text; using System.Text.RegularExpressions; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using HarmonyLib; using JetBrains.Annotations; using Microsoft.CodeAnalysis; using TMPro; using UnityEngine; using UnityEngine.EventSystems; using UnityEngine.UI; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.Default | DebuggableAttribute.DebuggingModes.DisableOptimizations | DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints | DebuggableAttribute.DebuggingModes.EnableEditAndContinue)] [assembly: TargetFramework(".NETFramework,Version=v4.7.2", FrameworkDisplayName = ".NET Framework 4.7.2")] [assembly: AssemblyCompany("PPR")] [assembly: AssemblyConfiguration("Debug")] [assembly: AssemblyFileVersion("1.0.0.0")] [assembly: AssemblyInformationalVersion("1.0.0")] [assembly: AssemblyProduct("PPR")] [assembly: AssemblyTitle("PPR")] [assembly: AssemblyVersion("1.0.0.0")] namespace PPR { public class ConfigurationManagerAttributes { public int? Order; public string DispName; public string Category; public bool HideSettingName; public bool HideDefaultButton; public Action<ConfigEntryBase> CustomDrawer; } internal static class PPRConfigDisplayLocalization { private static readonly Regex KeyPattern = new Regex("^\\(([^)]+)\\)\\s*(.+)$"); private static readonly Dictionary<string, string> SectionNameKo = new Dictionary<string, string> { ["Perfect Parry"] = "퍼펙트 패링", ["Perfect Parry - Damage Reduction"] = "퍼펙트 패링 - 피해 감소", ["Perfect Parry - Effect"] = "퍼펙트 패링 - 이펙트", ["Perfect Parry - Ranged"] = "퍼펙트 패링 - 원거리", ["Shield Assignment"] = "방패 지정", ["Auto Shield Equip"] = "방패 자동 장착", ["Back Shield Guard"] = "후방 방패 가드", ["Guard Durability"] = "가드 내구도", ["Equip Motion Cancel"] = "장비 착용 모션 스킵", ["Guard Sprint"] = "가드 질주", ["Stagger Reduction"] = "스태거 감소", ["Debug"] = "디버그" }; private static readonly Dictionary<string, string> PrefixNameKo = new Dictionary<string, string> { ["Enable"] = "사용", ["Parry"] = "패링", ["Counter"] = "반격", ["Damage Reduction"] = "피해 감소", ["Effect"] = "이펙트", ["Reflect"] = "반사", ["Input"] = "입력", ["Display"] = "표시", ["Guard"] = "가드", ["Visual"] = "외형", ["Armor"] = "방어구", ["Blocker"] = "블로커", ["Cancel"] = "취소", ["Requirement"] = "요구조건", ["Speed"] = "속도", ["Classify"] = "분류", ["Animation"] = "애니메이션", ["Stamina"] = "스태미나", ["Reduction"] = "감소", ["Scope"] = "적용 범위" }; private static readonly Dictionary<string, string> ItemNameKo = new Dictionary<string, string> { ["Perfect Parry"] = "퍼펙트 패링", ["Detection Window (sec)"] = "판정 시간(초)", ["Input Window (sec)"] = "입력 시간(초)", ["Animation Speed Multiplier"] = "애니메이션 속도 배율", ["Damage Reduction"] = "피해 감소", ["Reduction Rate (%)"] = "감소율(%)", ["Head Effect"] = "머리 이펙트", ["VFX Name"] = "이펙트 이름", ["Offset X"] = "X 오프셋", ["Offset Y"] = "Y 오프셋", ["Offset Z"] = "Z 오프셋", ["Custom Color Enabled"] = "커스텀 색상 사용", ["Custom Color (Hex)"] = "커스텀 색상(Hex)", ["Ranged No Stagger"] = "원거리 스태거 방지", ["Projectile Reflect"] = "투사체 반사", ["Perfect Parry Damage Multiplier"] = "퍼펙트 패링 데미지 배율", ["Normal Parry Reflect"] = "일반 패링 반사", ["Normal Parry Damage Multiplier"] = "일반 패링 데미지 배율", ["Assign Key"] = "지정 키", ["Show Role Icons"] = "역할 아이콘 표시", ["Auto-Equip Assigned Shield"] = "지정 방패 자동 장착", ["Back Shield Guard"] = "후방 방패 가드", ["Rear Cone Half-Angle (deg)"] = "후방 판정각(도)", ["Position Offset X"] = "위치 오프셋 X", ["Position Offset Y"] = "위치 오프셋 Y", ["Position Offset Z"] = "위치 오프셋 Z", ["Rotation Offset X"] = "회전 오프셋 X", ["Rotation Offset Y"] = "회전 오프셋 Y", ["Rotation Offset Z"] = "회전 오프셋 Z", ["No Armor Wear On Guard"] = "가드 시 방어구 내구도 보호", ["No Durability Loss - Weapon"] = "무기 내구도 보호", ["No Durability Loss - Tool"] = "도구 내구도 보호", ["No Durability Loss - Shield"] = "방패 내구도 보호", ["Equip Motion Cancel"] = "장비 착용 모션 스킵", ["Shield"] = "방패", ["Helmet"] = "투구", ["Chest"] = "상의", ["Legs"] = "하의", ["Cape"] = "망토", ["Utility"] = "유틸리티", ["Trinket"] = "장신구", ["Weapon"] = "무기", ["Tool"] = "도구", ["Hide/Show Hand Items (R)"] = "무기 숨기기/꺼내기(R)", ["Guard Sprint"] = "가드 질주", ["Min Blocking Skill"] = "최소 막기 스킬", ["Buckler Penalty %"] = "버클러 감속 %", ["Normal Shield Penalty %"] = "일반 방패 감속 %", ["Tower Shield Penalty %"] = "대형 방패 감속 %", ["Weapon Penalty %"] = "무기 감속 %", ["Buckler Prefabs"] = "버클러 지정 목록", ["Normal Prefabs"] = "일반 방패 지정 목록", ["Tower Prefabs"] = "대형 방패 지정 목록", ["Sync Leg Animation"] = "다리 모션 동기화", ["Max Leg Speed-Up"] = "다리 모션 최대 배속", ["Guard Sprint Stamina Multiplier"] = "가드 질주 스태미나 배율", ["Stagger Reduction"] = "스태거 감소", ["Max Reduction %"] = "최대 감소율(%)", ["Apply To Unblocked Hits"] = "그냥 맞을 때도 적용", ["Verbose Log"] = "상세 로그" }; private static readonly HashSet<string> s_missingSections = new HashSet<string>(); private static readonly HashSet<string> s_missingPrefixes = new HashSet<string>(); private static readonly HashSet<string> s_missingItems = new HashSet<string>(); private static readonly FieldInfo DescriptionBackingField = AccessTools.Field(typeof(ConfigEntryBase), "<Description>k__BackingField"); private static bool s_descRefreshFailed; private static bool s_hooked; private static bool s_configManagerMissingLogged; public static void ApplyDisplayNames(ConfigFile cfg) { bool isKorean = PPRLocalizationManager.IsKorean; int num = 0; List<string> list = new List<string>(); foreach (ConfigDefinition key in cfg.Keys) { ConfigEntryBase val = cfg[key]; ConfigDescription description = val.Description; object[] array = ((description != null) ? description.Tags : null); if (array == null) { continue; } ConfigurationManagerAttributes configurationManagerAttributes = null; object[] array2 = array; foreach (object obj in array2) { if (obj is ConfigurationManagerAttributes configurationManagerAttributes2) { configurationManagerAttributes = configurationManagerAttributes2; break; } } if (configurationManagerAttributes != null) { configurationManagerAttributes.DispName = BuildDispName(key.Key, isKorean); configurationManagerAttributes.Category = BuildCategory(key.Section, isKorean); num++; if (list.Count < 3) { list.Add(configurationManagerAttributes.Category + " > " + configurationManagerAttributes.DispName); } TryRefreshDescriptionText(val, key.Key); } } Plugin.Log.LogInfo((object)($"[PPR:설정화면] 칸 제목/항목 이름 적용 — {num}개" + ((list.Count > 0) ? (", 예시: " + string.Join(" | ", list)) : ""))); if (s_missingSections.Count > 0) { Plugin.Log.LogWarning((object)($"[PPR:설정화면] 한국어 칸 제목 번역 누락 {s_missingSections.Count}건 — " + string.Join(", ", s_missingSections) + " (영어 원문으로 표시됨)")); } if (s_missingPrefixes.Count > 0) { Plugin.Log.LogWarning((object)($"[PPR:설정화면] 한국어 접두어 번역 누락 {s_missingPrefixes.Count}건 — " + string.Join(", ", s_missingPrefixes) + " (영어 원문으로 표시됨)")); } if (s_missingItems.Count > 0) { Plugin.Log.LogWarning((object)($"[PPR:설정화면] 한국어 항목명 번역 누락 {s_missingItems.Count}건 — " + string.Join(", ", s_missingItems) + " (영어 원문으로 표시됨)")); } } private static string BuildCategory(string section, bool korean) { if (!korean) { return section; } if (SectionNameKo.TryGetValue(section, out var value)) { return value; } s_missingSections.Add(section); return section; } private static string BuildDispName(string key, bool korean) { if (!korean) { return key; } Match match = KeyPattern.Match(key); if (!match.Success) { return key; } string value = match.Groups[1].Value; string value2 = match.Groups[2].Value; if (!PrefixNameKo.TryGetValue(value, out var value3)) { value3 = value; s_missingPrefixes.Add(value); } if (!ItemNameKo.TryGetValue(value2, out var value4)) { value4 = value2; s_missingItems.Add(value2); } return "(" + value3 + ") " + value4; } private static void TryRefreshDescriptionText(ConfigEntryBase entry, string key) { //IL_006e: Unknown result type (might be due to invalid IL or missing references) //IL_0075: Expected O, but got Unknown if (s_descRefreshFailed) { return; } string text = PPRLocalizationManager.Desc(key); ConfigDescription description = entry.Description; if (description == null || description.Description == text) { return; } if (DescriptionBackingField == null) { s_descRefreshFailed = true; Plugin.Log.LogWarning((object)"[PPR:설정화면] 설명(Desc) 실시간 갱신 불가 — BepInEx 내부 필드를 못 찾음, 설명은 재시작 후에만 반영됩니다(항목 이름·칸 제목은 정상 작동)."); return; } try { ConfigDescription value = new ConfigDescription(text, description.AcceptableValues, description.Tags); DescriptionBackingField.SetValue(entry, value); } catch (Exception ex) { s_descRefreshFailed = true; Plugin.Log.LogWarning((object)("[PPR:설정화면] 설명(Desc) 실시간 갱신 실패 — 설명은 재시작 후에만 반영됩니다(항목 이름·칸 제목은 정상 작동): " + ex.Message)); } } public static void HookLanguageChangeEvent() { if (!s_hooked) { s_hooked = true; Localization.OnLanguageChange = (Action)Delegate.Combine(Localization.OnLanguageChange, new Action(RefreshForLanguageChange)); } } public static void RefreshForLanguageChange() { PPRLocalizationManager.Init(); ApplyDisplayNames(((BaseUnityPlugin)Plugin.Instance).Config); ForceConfigManagerRebuild(); } private static void ForceConfigManagerRebuild() { try { Type type = AccessTools.TypeByName("ConfigurationManager.ConfigurationManager"); if (type == null) { if (!s_configManagerMissingLogged) { s_configManagerMissingLogged = true; Plugin.Log.LogInfo((object)"[PPR:설정화면] ConfigurationManager 미설치 — 설정 창 즉시 갱신 생략(기능 자체엔 영향 없음, 설치돼 있다면 재시작 없이 바로 반영됩니다)."); } return; } object obj = AccessTools.Field(type, "instance")?.GetValue(null); if (obj != null) { AccessTools.Method(type, "BuildSettingList", (Type[])null, (Type[])null)?.Invoke(obj, null); Plugin.Log.LogInfo((object)"[PPR:설정화면] 언어 변경 감지 — 칸 제목/항목 이름/설명 재계산 + 설정 목록 재구성 완료."); } } catch (Exception ex) { Plugin.Log.LogWarning((object)("[PPR:설정화면] 설정 목록 강제 재구성 실패 — " + ex.Message)); } } } internal static class PPRLocalizationManager { private static Dictionary<string, string> _strings = new Dictionary<string, string>(); private static readonly HashSet<string> _missingDescKeys = new HashSet<string>(); public static bool IsKorean { get; private set; } public static void Init() { string text = PlatformPrefs.GetString("language", "English"); IsKorean = text == "Korean"; string text2 = (IsKorean ? "ko" : "en"); string text3 = "PPR.Localization." + text2 + ".json"; Assembly executingAssembly = Assembly.GetExecutingAssembly(); using (Stream stream = executingAssembly.GetManifestResourceStream(text3)) { if (stream == null) { ManualLogSource log = Plugin.Log; if (log != null) { log.LogWarning((object)("[PPR] Localization resource not found: " + text3)); } return; } using StreamReader streamReader = new StreamReader(stream); _strings = ParseJson(streamReader.ReadToEnd()); } ManualLogSource log2 = Plugin.Log; if (log2 != null) { log2.LogInfo((object)$"[PPR] Localization loaded: {text2} ({_strings.Count} keys)"); } } public static string Desc(string key) { if (_strings.TryGetValue(key, out var value)) { return value; } _missingDescKeys.Add(key); return key; } public static void WarnMissingDescriptions() { if (_missingDescKeys.Count != 0) { Plugin.Log.LogWarning((object)($"[PPR:설정화면] 설명/문구 번역 누락 {_missingDescKeys.Count}건 — " + string.Join(", ", _missingDescKeys) + " (원문 키가 그대로 표시됨)")); } } private static Dictionary<string, string> ParseJson(string json) { Dictionary<string, string> dictionary = new Dictionary<string, string>(); MatchCollection matchCollection = Regex.Matches(json, "\"((?:[^\"\\\\]|\\\\.)*?)\"[ \\t]*:[ \\t]*\"((?:[^\"\\\\]|\\\\.)*?)\""); foreach (Match item in matchCollection) { dictionary[item.Groups[1].Value] = Unescape(item.Groups[2].Value); } return dictionary; } private static string Unescape(string s) { return s.Replace("\\n", "\n").Replace("\\t", "\t").Replace("\\\"", "\"") .Replace("\\\\", "\\"); } } [BepInPlugin("LJS.PPR", "PPR", "1.2.11")] public class Plugin : BaseUnityPlugin { public const string PluginGUID = "LJS.PPR"; public const string PluginName = "PPR"; public const string PluginVersion = "1.2.11"; internal static ManualLogSource Log; internal static Plugin Instance; private readonly Harmony _harmony = new Harmony("LJS.PPR"); private void Awake() { Instance = this; Log = ((BaseUnityPlugin)this).Logger; LogBuildInfo(); PPRAccess.WarmUp(); PPRConfig.Init(((BaseUnityPlugin)this).Config); PPRBackGuardPatch.HookConfig(); ApplyHarmonyPatchesIsolated(); Log.LogInfo((object)"PPR v1.2.11 loaded."); } private void ApplyHarmonyPatchesIsolated() { int num = 0; int num2 = 0; List<Type> list = (from t in AccessTools.GetTypesFromAssembly(Assembly.GetExecutingAssembly()) where t.GetCustomAttributes(typeof(HarmonyPatch), inherit: false).Length != 0 select t).ToList(); foreach (Type item in list) { try { _harmony.CreateClassProcessor(item).Patch(); num++; } catch (Exception arg) { num2++; Log.LogError((object)$"[PPR:패치실패] '{item.FullName}' 패치 적용 실패 — 이 기능만 비활성화된 채로 나머지는 계속 진행됨. {arg}"); } } Log.LogInfo((object)$"[PPR:패치요약] 대상 클래스 {list.Count}개 — 성공 {num}개 / 실패 {num2}개"); LogPatchedMethods(); } private void LogPatchedMethods() { try { foreach (MethodBase allPatchedMethod in Harmony.GetAllPatchedMethods()) { Patches patchInfo = Harmony.GetPatchInfo(allPatchedMethod); if (patchInfo == null) { continue; } bool flag = patchInfo.Prefixes.Any((Patch p) => p.owner == "LJS.PPR"); bool flag2 = patchInfo.Postfixes.Any((Patch p) => p.owner == "LJS.PPR"); bool flag3 = patchInfo.Finalizers.Any((Patch p) => p.owner == "LJS.PPR"); bool flag4 = patchInfo.Transpilers.Any((Patch p) => p.owner == "LJS.PPR"); if (flag || flag2 || flag3 || flag4) { string text = string.Join("+", new string[4] { flag ? "Prefix" : null, flag2 ? "Postfix" : null, flag3 ? "Finalizer" : null, flag4 ? "Transpiler" : null }.Where((string s) => s != null)); Log.LogInfo((object)("[PPR:패치목록] " + allPatchedMethod.DeclaringType?.Name + "." + allPatchedMethod.Name + " (" + text + ")")); } } } catch (Exception ex) { Log.LogError((object)("[PPR] 패치 목록 조회 오류: " + ex.Message)); } } private void LogBuildInfo() { try { string location = Assembly.GetExecutingAssembly().Location; DateTime lastWriteTime = File.GetLastWriteTime(location); Log.LogInfo((object)string.Format("[PPR:빌드정보] {0} — DLL 빌드 시각 {1:yyyy-MM-dd HH:mm:ss}", "1.2.11", lastWriteTime)); } catch (Exception ex) { Log.LogError((object)("[PPR] 빌드 정보 로그 오류: " + ex.Message)); } } } internal static class PPRAccess { private static int _successCount; private static int _failCount; private static readonly List<string> _failedNames; private static readonly Func<Humanoid, ItemData> _getCurrentBlocker; private static readonly Func<Character, float, Vector3, HitData, bool> _addStaggerDamage; private static readonly FieldRef<Character, SEMan> _semanRef; private static readonly FieldRef<VisEquipment, ZNetView> _visNetViewRef; private static readonly FieldRef<Humanoid, ItemData> _hiddenLeftItemRef; private static readonly FieldRef<Humanoid, ItemData> _hiddenRightItemRef; private static readonly FieldRef<ZNetScene, Dictionary<ZDO, ZNetView>> _znetInstancesRef; private static readonly Func<VisEquipment, int, int, Transform, bool, bool, int, GameObject> _attachItem; public static ItemData GetCurrentBlocker(Humanoid h) { return (_getCurrentBlocker != null) ? _getCurrentBlocker(h) : null; } public static bool AddStaggerDamage(Character c, float damage, Vector3 forceDirection, HitData hit) { //IL_000e: Unknown result type (might be due to invalid IL or missing references) return _addStaggerDamage != null && _addStaggerDamage(c, damage, forceDirection, hit); } public static SEMan GetSeman(Character c) { return (_semanRef != null) ? _semanRef.Invoke(c) : null; } public static ZNetView GetNetView(VisEquipment ve) { return (_visNetViewRef != null) ? _visNetViewRef.Invoke(ve) : null; } public static ItemData GetHiddenLeftItem(Humanoid h) { return (_hiddenLeftItemRef != null) ? _hiddenLeftItemRef.Invoke(h) : null; } public static ItemData GetHiddenRightItem(Humanoid h) { return (_hiddenRightItemRef != null) ? _hiddenRightItemRef.Invoke(h) : null; } public static Dictionary<ZDO, ZNetView> GetInstances(ZNetScene scene) { return (_znetInstancesRef != null) ? _znetInstancesRef.Invoke(scene) : null; } public static GameObject AttachItem(VisEquipment ve, int itemHash, int variant, Transform joint, bool enableEquipEffects, bool backAttach, int quality = 0) { return (_attachItem != null) ? _attachItem(ve, itemHash, variant, joint, enableEquipEffects, backAttach, quality) : null; } static PPRAccess() { _failedNames = new List<string>(); _getCurrentBlocker = BuildMethodDelegate<Func<Humanoid, ItemData>>(typeof(Humanoid), "GetCurrentBlocker", null); _addStaggerDamage = BuildMethodDelegate<Func<Character, float, Vector3, HitData, bool>>(typeof(Character), "AddStaggerDamage", new Type[3] { typeof(float), typeof(Vector3), typeof(HitData) }); _semanRef = BuildFieldRef<Character, SEMan>("m_seman"); _visNetViewRef = BuildFieldRef<VisEquipment, ZNetView>("m_nview"); _hiddenLeftItemRef = BuildFieldRef<Humanoid, ItemData>("m_hiddenLeftItem"); _hiddenRightItemRef = BuildFieldRef<Humanoid, ItemData>("m_hiddenRightItem"); _znetInstancesRef = BuildFieldRef<ZNetScene, Dictionary<ZDO, ZNetView>>("m_instances"); _attachItem = BuildMethodDelegate<Func<VisEquipment, int, int, Transform, bool, bool, int, GameObject>>(typeof(VisEquipment), "AttachItem", null); if (_failCount == 0) { Plugin.Log.LogInfo((object)$"[PPR:접근검사] 성공 {_successCount} / 실패 0 — 비공개 멤버 접근자 전부 정상."); } else { Plugin.Log.LogError((object)string.Format("[PPR:접근검사] 성공 {0} / 실패 {1} — 실패 목록: {2} (해당 기능만 비활성화됨, 개별 실패 로그는 위 참고)", _successCount, _failCount, string.Join(", ", _failedNames))); } } public static void WarmUp() { RuntimeHelpers.RunClassConstructor(typeof(PPRAccess).TypeHandle); } private static TDelegate BuildMethodDelegate<TDelegate>(Type declaringType, string methodName, Type[] paramTypes) where TDelegate : Delegate { try { MethodInfo methodInfo = ((paramTypes != null) ? AccessTools.Method(declaringType, methodName, paramTypes, (Type[])null) : AccessTools.Method(declaringType, methodName, (Type[])null, (Type[])null)); if (methodInfo == null) { throw new MissingMethodException(declaringType.FullName, methodName); } TDelegate result = AccessTools.MethodDelegate<TDelegate>(methodInfo, (object)null, true); _successCount++; return result; } catch (Exception ex) { string text = declaringType.FullName + "." + methodName; _failCount++; _failedNames.Add(text); Plugin.Log.LogError((object)("[PPR:접근검사] " + text + " 접근자 생성 실패 — 관련 기능만 비활성화됨: " + ex.Message)); return null; } } private static FieldRef<T, F> BuildFieldRef<T, F>(string fieldName) { try { FieldRef<T, F> result = AccessTools.FieldRefAccess<T, F>(fieldName); _successCount++; return result; } catch (Exception ex) { string text = typeof(T).FullName + "." + fieldName; _failCount++; _failedNames.Add(text); Plugin.Log.LogError((object)("[PPR:접근검사] " + text + " 접근자 생성 실패 — 관련 기능만 비활성화됨: " + ex.Message)); return null; } } } [HarmonyPatch] public static class PPRAutoEquipPatch { [HarmonyPatch(typeof(Humanoid), "EquipItem")] [HarmonyPostfix] public static void EquipItem_Postfix(Humanoid __instance, ItemData item, bool __result) { if (!__result || item == null) { return; } Player player = default(Player); ref Player reference = ref player; Humanoid obj = __instance; reference = (Player)(object)((obj is Player) ? obj : null); if (!((Object)(object)player == (Object)null) && !((Object)(object)player != (Object)(object)Player.m_localPlayer)) { PPRExceptionGuard.Run("AutoEquip.EquipItem", delegate { EquipItemLogic(__instance, player, item); }); } } private static void EquipItemLogic(Humanoid __instance, Player player, ItemData item) { //IL_0044: Unknown result type (might be due to invalid IL or missing references) //IL_004a: Invalid comparison between Unknown and I4 if (PPREquipGuard.Suppressed) { PPRDebugLog.Log("자동 장착 건너뜀 — 억제 중(로그인 복원/R키 복귀/재귀 호출)"); } else { if (!PPRConfig.AutoEquipShieldEnabled.Value || (int)item.m_shared.m_itemType != 3 || __instance.LeftItem != null) { return; } ItemData trackedShield = PPRShieldRole.TryGetAutoEquipShield(player); if (trackedShield == null) { PPRDebugLog.Log("자동 장착 건너뜀 — 자동 장착으로 지정된 방패 없음"); return; } if (trackedShield.m_equipped) { PPRDebugLog.Log("자동 장착 건너뜀 — 추적 방패가 이미 장착 상태"); return; } if (trackedShield.m_durability <= 0f) { PPRDebugLog.Log("자동 장착 건너뜀 — 추적 방패 내구도 0"); return; } PPREquipGuard.RunSuppressed(delegate { __instance.EquipItem(trackedShield, true); }); string[] obj = new string[5] { "자동 장착 실행 — ", null, null, null, null }; GameObject dropPrefab = trackedShield.m_dropPrefab; obj[1] = ((dropPrefab != null) ? ((Object)dropPrefab).name : null); obj[2] = " (한손무기 "; obj[3] = item.m_shared.m_name; obj[4] = " 장착에 대응)"; PPRDebugLog.Log(string.Concat(obj)); } } [HarmonyPatch(typeof(Player), "Load")] [HarmonyPostfix] public static void Load_Postfix(Player __instance) { if (!((Object)(object)__instance != (Object)(object)Player.m_localPlayer)) { PPRExceptionGuard.Run("AutoEquip.Load", delegate { LoadLogic(__instance); }); } } private static void LoadLogic(Player __instance) { ItemData val = PPRShieldRole.ResolveCurrentHolder(__instance, PPRShieldRoleComponent.Back); ItemData val2 = PPRShieldRole.ResolveCurrentHolder(__instance, PPRShieldRoleComponent.AutoEquip); PPRDebugLog.Log("접속 — 등 방패 지정: " + (val?.m_shared?.m_name ?? "없음") + ", 자동 장착 지정: " + (val2?.m_shared?.m_name ?? "없음")); } [HarmonyPatch(typeof(Player), "EquipInventoryItems")] [HarmonyPrefix] public static void EquipInventoryItems_Prefix() { PPREquipGuard.PushSuppressed(); } [HarmonyPatch(typeof(Player), "EquipInventoryItems")] [HarmonyFinalizer] public static Exception EquipInventoryItems_Finalizer(Exception __exception) { PPREquipGuard.PopSuppressed(); return __exception; } [HarmonyPatch(typeof(Humanoid), "ShowHandItems")] [HarmonyPrefix] public static void ShowHandItems_Prefix() { PPREquipGuard.PushSuppressed(); } [HarmonyPatch(typeof(Humanoid), "ShowHandItems")] [HarmonyFinalizer] public static Exception ShowHandItems_Finalizer(Exception __exception) { PPREquipGuard.PopSuppressed(); return __exception; } } [HarmonyPatch] public static class PPRBackGuardPatch { private class PPRBackShieldVisual : MonoBehaviour { public int CurrentHash; public int CurrentVariant; public GameObject Instance; private void Awake() { _activeVisuals.Add(this); } private void OnDestroy() { _activeVisuals.Remove(this); if ((Object)(object)Instance != (Object)null) { Object.Destroy((Object)(object)Instance); } } } private static readonly int ZdoHashShield = StringExtensionMethods.GetStableHashCode("PPR_BackShieldHash"); private static readonly int ZdoHashVariant = StringExtensionMethods.GetStableHashCode("PPR_BackShieldVariant"); private static readonly HashSet<PPRBackShieldVisual> _activeVisuals = new HashSet<PPRBackShieldVisual>(); public static void HookConfig() { EventHandler eventHandler = delegate { InvalidateAllVisuals(); }; PPRConfig.BackGuardOffsetX.SettingChanged += eventHandler; PPRConfig.BackGuardOffsetY.SettingChanged += eventHandler; PPRConfig.BackGuardOffsetZ.SettingChanged += eventHandler; PPRConfig.BackGuardRotationX.SettingChanged += eventHandler; PPRConfig.BackGuardRotationY.SettingChanged += eventHandler; PPRConfig.BackGuardRotationZ.SettingChanged += eventHandler; } private static void InvalidateAllVisuals() { foreach (PPRBackShieldVisual activeVisual in _activeVisuals) { if ((Object)(object)activeVisual != (Object)null) { activeVisual.CurrentHash = int.MinValue; } } PPRDebugLog.Log("등 방패 오프셋 설정 변경 — 기존 인스턴스 재적용 예약됨"); } [HarmonyPatch(typeof(Character), "RPC_Damage")] [HarmonyPrefix] public static void RPC_Damage_Prefix(Character __instance, HitData hit) { PPRBackGuardState.Clear(); if (!PPRConfig.BackGuardEnabled.Value) { return; } Player player = default(Player); ref Player reference = ref player; Character obj = __instance; reference = (Player)(object)((obj is Player) ? obj : null); if (!((Object)(object)player == (Object)null) && !((Object)(object)player != (Object)(object)Player.m_localPlayer) && hit != null) { PPRExceptionGuard.Run("BackGuard.RPC_Damage", delegate { ArmBackGuardIfEligible(__instance, player, hit); }); } } private static void ArmBackGuardIfEligible(Character __instance, Player player, HitData hit) { //IL_00b5: Unknown result type (might be due to invalid IL or missing references) //IL_00ba: Unknown result type (might be due to invalid IL or missing references) //IL_00f5: Unknown result type (might be due to invalid IL or missing references) //IL_00fa: Unknown result type (might be due to invalid IL or missing references) //IL_0134: Unknown result type (might be due to invalid IL or missing references) //IL_0135: Unknown result type (might be due to invalid IL or missing references) if (!hit.m_blockable) { PPRDebugLog.Log("후방 가드 미무장 — hit.m_blockable=false"); return; } if (hit.m_dodgeable && __instance.IsDodgeInvincible()) { PPRDebugLog.Log("후방 가드 미무장 — 구르기 무적 중(이 히트는 곧 무시됨)"); return; } if (__instance.IsStaggering()) { PPRDebugLog.Log("후방 가드 미무장 — 스태거 중"); return; } if (!PPRShieldRole.IsOnBack(player, out var shield)) { PPRDebugLog.Log("후방 가드 미무장 — 등 대기 상태 아님"); return; } if (shield.m_durability <= 0f) { PPRDebugLog.Log("후방 가드 미무장 — 추적 방패 내구도 0"); return; } Vector3 forward = ((Component)__instance).transform.forward; forward.y = 0f; if (((Vector3)(ref forward)).sqrMagnitude < 0.0001f) { PPRDebugLog.Log("후방 가드 미무장 — forward 벡터가 0"); return; } ((Vector3)(ref forward)).Normalize(); Vector3 dir = hit.m_dir; dir.y = 0f; if (((Vector3)(ref dir)).sqrMagnitude < 0.0001f) { PPRDebugLog.Log("후방 가드 미무장 — hit.m_dir 벡터가 0"); return; } ((Vector3)(ref dir)).Normalize(); float num = Vector3.Dot(dir, forward); float num2 = Mathf.Cos(PPRConfig.BackGuardHalfAngle.Value * ((float)Math.PI / 180f)); if (num < num2) { PPRDebugLog.Log($"후방 가드 미무장 — 후방 원뿔 밖(dot={num:F2}, 기준={num2:F2})"); return; } PPRBackGuardState.Arm(__instance, shield); object arg = num; object arg2 = num2; GameObject dropPrefab = shield.m_dropPrefab; PPRDebugLog.Log($"후방 가드 무장 — dot={arg:F2} >= 기준={arg2:F2}, 방패={((dropPrefab != null) ? ((Object)dropPrefab).name : null)}"); } [HarmonyPatch(typeof(Character), "RPC_Damage")] [HarmonyFinalizer] public static Exception RPC_Damage_Finalizer(Exception __exception) { PPRBackGuardState.Clear(); return __exception; } [HarmonyPatch(typeof(Humanoid), "IsBlocking")] [HarmonyPostfix] public static void IsBlocking_Postfix(Humanoid __instance, ref bool __result) { if (!__result && PPRBackGuardState.IsArmedFor((Character)(object)__instance)) { __result = true; } } [HarmonyPatch(typeof(Humanoid), "BlockAttack")] [HarmonyPrefix] [HarmonyPriority(800)] public static bool BlockAttack_Prefix(Humanoid __instance, HitData hit, Character attacker, ref bool __result) { Character character = (Character)(object)__instance; if (!PPRBackGuardState.IsArmedFor(character)) { return true; } ItemData shield = PPRBackGuardState.ArmedShield; PPRBackGuardState.Clear(); PPRBackGuardState.MarkHandledByBackGuard(); PPRExceptionGuard.Run("BackGuard.BlockAttack", delegate { ApplyBackGuardBlock(__instance, shield, hit, attacker); }); __result = true; return false; } private static void ApplyBackGuardBlock(Humanoid humanoid, ItemData shield, HitData hit, Character attacker) { //IL_0019: Unknown result type (might be due to invalid IL or missing references) //IL_001e: Unknown result type (might be due to invalid IL or missing references) //IL_00a0: Unknown result type (might be due to invalid IL or missing references) //IL_00ff: Unknown result type (might be due to invalid IL or missing references) //IL_0104: Unknown result type (might be due to invalid IL or missing references) //IL_010e: Unknown result type (might be due to invalid IL or missing references) //IL_0113: Unknown result type (might be due to invalid IL or missing references) //IL_0186: Unknown result type (might be due to invalid IL or missing references) //IL_018b: Unknown result type (might be due to invalid IL or missing references) //IL_0199: Unknown result type (might be due to invalid IL or missing references) //IL_019f: Unknown result type (might be due to invalid IL or missing references) float skillFactor = ((Character)humanoid).GetSkillFactor((SkillType)6); float blockPower = shield.GetBlockPower(skillFactor); DamageTypes val = ((DamageTypes)(ref hit.m_damage)).Clone(); ((DamageTypes)(ref val)).ApplyArmor(blockPower); float totalBlockableDamage = hit.GetTotalBlockableDamage(); float totalBlockableDamage2 = ((DamageTypes)(ref val)).GetTotalBlockableDamage(); float num = totalBlockableDamage - totalBlockableDamage2; float num2 = Mathf.Clamp01((blockPower <= 0f) ? 0f : (num / blockPower)); float num3 = humanoid.m_blockStaminaDrain * num2; num3 += num3 * ((Character)humanoid).GetEquipmentBlockStaminaModifier(); SEMan seman = PPRAccess.GetSeman((Character)(object)humanoid); if (seman != null) { seman.ModifyBlockStaminaUsage(num3, ref num3, true); } ((Character)humanoid).UseStamina(num3); float totalStaggerDamage = ((DamageTypes)(ref val)).GetTotalStaggerDamage(); bool flag = PPRAccess.AddStaggerDamage((Character)(object)humanoid, totalStaggerDamage, hit.m_dir, hit); bool flag2 = ((Character)humanoid).HaveStamina(0f) && !flag; float totalDamage = hit.GetTotalDamage(); if (flag2) { hit.m_statusEffectHash = 0; hit.BlockDamage(num); hit.m_pushForce *= num2; DamageText.instance.ShowText((TextType)6, hit.m_point + Vector3.up * 0.5f, num, false); } float durability = shield.m_durability; if (shield.m_shared.m_useDurability) { float num4 = shield.m_shared.m_useDurabilityDrain * ((blockPower <= 0f) ? 0f : (totalBlockableDamage / blockPower)); shield.m_durability -= num4; } ((Character)humanoid).RaiseSkill((SkillType)6, 1f); shield.m_shared.m_blockEffect.Create(hit.m_point, Quaternion.identity, (Transform)null, 1f, -1, default(ZDOID)); if (flag2) { PPRGuardDurabilityState.MarkGuardSucceeded(); if (PPRGuardDurabilityPatch.ShouldProtectBlocker(shield) && shield.m_durability < durability) { shield.m_durability = durability; PPRDebugLog.Log("PPR-GUARD", "블로커 내구도 복원(후방) — " + shield.m_shared.m_name + "(Shield)"); } } float totalDamage2 = hit.GetTotalDamage(); PPRDebugLog.Log($"후방 가드 발동 — blockPower={blockPower:F1}, dmg {totalDamage:F1}->{totalDamage2:F1}, stamina={num3:F1}, negated={flag2}"); } [HarmonyPatch(typeof(VisEquipment), "UpdateVisuals")] [HarmonyPostfix] public static void UpdateVisuals_Postfix(VisEquipment __instance) { if (!__instance.m_isPlayer) { return; } Player player = ((Component)__instance).GetComponent<Player>(); if (!((Object)(object)player == (Object)null)) { PPRExceptionGuard.Run("BackGuard.UpdateVisuals", delegate { UpdateVisualsLogic(__instance, player); }); } } private static void UpdateVisualsLogic(VisEquipment __instance, Player player) { ZNetView netView = PPRAccess.GetNetView(__instance); ZDO val = (((Object)(object)netView != (Object)null) ? netView.GetZDO() : null); if (val == null) { return; } if (!PPRConfig.BackGuardEnabled.Value) { if (netView.IsOwner() && val.GetInt(ZdoHashShield, 0) != 0) { val.Set(ZdoHashShield, 0, false); } ReconcileVisual(__instance, 0, 0); return; } if (netView.IsOwner()) { ItemData shield; bool flag = PPRShieldRole.IsOnBack(player, out shield); if (flag && PPRAccess.GetHiddenLeftItem((Humanoid)(object)player) == shield) { flag = false; } int num = ((flag && (Object)(object)shield.m_dropPrefab != (Object)null) ? StringExtensionMethods.GetStableHashCode(((Object)shield.m_dropPrefab).name) : 0); int num2 = (flag ? shield.m_variant : 0); if (val.GetInt(ZdoHashShield, 0) != num) { val.Set(ZdoHashShield, num, false); } if (val.GetInt(ZdoHashVariant, 0) != num2) { val.Set(ZdoHashVariant, num2, false); } } int hash = val.GetInt(ZdoHashShield, 0); int variant = val.GetInt(ZdoHashVariant, 0); ReconcileVisual(__instance, hash, variant); } private static void ReconcileVisual(VisEquipment visEq, int hash, int variant) { //IL_0104: Unknown result type (might be due to invalid IL or missing references) //IL_0127: Unknown result type (might be due to invalid IL or missing references) //IL_012c: Unknown result type (might be due to invalid IL or missing references) //IL_013e: Unknown result type (might be due to invalid IL or missing references) //IL_0161: Unknown result type (might be due to invalid IL or missing references) //IL_0166: Unknown result type (might be due to invalid IL or missing references) PPRBackShieldVisual pPRBackShieldVisual = ((Component)visEq).GetComponent<PPRBackShieldVisual>(); if ((Object)(object)pPRBackShieldVisual == (Object)null) { if (hash == 0) { return; } pPRBackShieldVisual = ((Component)visEq).gameObject.AddComponent<PPRBackShieldVisual>(); } if (pPRBackShieldVisual.CurrentHash == hash && pPRBackShieldVisual.CurrentVariant == variant) { return; } pPRBackShieldVisual.CurrentHash = hash; pPRBackShieldVisual.CurrentVariant = variant; if ((Object)(object)pPRBackShieldVisual.Instance != (Object)null) { Object.Destroy((Object)(object)pPRBackShieldVisual.Instance); pPRBackShieldVisual.Instance = null; PPRDebugLog.Log("등 방패 인스턴스 제거"); } if (hash != 0 && !((Object)(object)visEq.m_backShield == (Object)null)) { GameObject val = PPRAccess.AttachItem(visEq, hash, variant, visEq.m_backShield, enableEquipEffects: false, backAttach: true); if ((Object)(object)val == (Object)null) { PPRDebugLog.Warn($"등 방패 인스턴스 생성 실패 — AttachItem이 null 반환(hash={hash}, variant={variant}, m_backShield={(Object)(object)visEq.m_backShield != (Object)null})"); return; } Transform transform = val.transform; transform.localPosition += new Vector3(PPRConfig.BackGuardOffsetX.Value, PPRConfig.BackGuardOffsetY.Value, PPRConfig.BackGuardOffsetZ.Value); Transform transform2 = val.transform; transform2.localRotation *= Quaternion.Euler(PPRConfig.BackGuardRotationX.Value, PPRConfig.BackGuardRotationY.Value, PPRConfig.BackGuardRotationZ.Value); pPRBackShieldVisual.Instance = val; PPRDebugLog.Log($"등 방패 인스턴스 생성 완료 (hash={hash}, variant={variant})"); } } } internal static class PPRBackGuardState { private static Character _armedFor; private static ItemData _armedShield; private static bool _handledByBackGuard; public static ItemData ArmedShield => _armedShield; public static void Arm(Character character, ItemData shield) { _armedFor = character; _armedShield = shield; } public static bool IsArmedFor(Character character) { return character != null && _armedFor == character; } public static void MarkHandledByBackGuard() { _handledByBackGuard = true; } public static bool ConsumeHandledByBackGuard() { bool handledByBackGuard = _handledByBackGuard; _handledByBackGuard = false; return handledByBackGuard; } public static void Clear() { _armedFor = null; _armedShield = null; } } [HarmonyPatch(typeof(Humanoid), "BlockAttack")] public static class PPRBlockPatch { private const float VanillaPerfectBlockInterval = 0.25f; [HarmonyPostfix] public static void Postfix(Humanoid __instance, HitData hit, Character attacker, bool __result, float ___m_blockTimer) { if (!__result || PPRBackGuardState.ConsumeHandledByBackGuard()) { return; } Player val = default(Player); ref Player reference = ref val; Humanoid obj = __instance; reference = (Player)(object)((obj is Player) ? obj : null); if (!((Object)(object)val == (Object)null) && !((Object)(object)val != (Object)(object)Player.m_localPlayer)) { PPRExceptionGuard.Run("PerfectParry.BlockAttack", delegate { BlockAttackLogic(__instance, hit, attacker, val, ___m_blockTimer); }); } } private static void BlockAttackLogic(Humanoid __instance, HitData hit, Character attacker, Player val, float blockTimer) { //IL_0124: Unknown result type (might be due to invalid IL or missing references) //IL_0129: Unknown result type (might be due to invalid IL or missing references) //IL_012b: Unknown result type (might be due to invalid IL or missing references) //IL_012e: Invalid comparison between Unknown and I4 //IL_0130: Unknown result type (might be due to invalid IL or missing references) //IL_0134: Invalid comparison between Unknown and I4 //IL_0136: Unknown result type (might be due to invalid IL or missing references) //IL_013a: Invalid comparison between Unknown and I4 //IL_0170: Unknown result type (might be due to invalid IL or missing references) bool flag = blockTimer >= 0f; ItemData currentBlocker = PPRAccess.GetCurrentBlocker(__instance); bool flag2 = currentBlocker != null && currentBlocker.m_shared.m_timedBlockBonus > 1f; bool flag3 = PPRConfig.Enable.Value && flag && blockTimer < PPRConfig.ParryWindow.Value && flag2 && ((Character)__instance).HaveStamina(0f) && !((Character)__instance).IsStaggering(); bool flag4 = blockTimer < 0.25f; string text = (flag3 ? "퍼펙트" : ((flag && flag4) ? "일반" : "가드")); Plugin.Log.LogInfo((object)$"[PPR-DIAG] raw blockTimer={blockTimer:F3}s, perfectThreshold={PPRConfig.ParryWindow.Value:F2}s, normalThreshold={0.25f:F2}s, 판정={text} (hasPerfectBlockCapability={flag2}, ranged={hit.m_ranged})"); if (flag3) { ItemData currentWeapon = __instance.GetCurrentWeapon(); bool flag5 = false; string text2 = "무기 없음"; if (currentWeapon != null) { ItemType itemType = currentWeapon.m_shared.m_itemType; bool flag6 = (int)itemType == 3 || (int)itemType == 14 || (int)itemType == 22; bool flag7 = currentWeapon.HaveSecondaryAttack(); if (flag6 && flag7) { PPRState.ActivateParry(val); PPRState.SpawnParryVFX(val); flag5 = true; } else { text2 = $"itemType={itemType}(eligible={flag6}), hasSecondaryAttack={flag7}"; } } Plugin.Log.LogInfo((object)string.Format("[PPR] 퍼펙트 패링 (blockTimer={0:F3}s, 카운터윈도우={1})", blockTimer, flag5 ? "활성(데미지감소 가능)" : ("비활성(" + text2 + ")"))); if (hit.m_ranged && PPRConfig.ProjectileReflect.Value) { PPRProjectileReflectPatch.TryReflect(__instance, hit, attacker, isPerfect: true); } } else if (flag && hit.m_ranged) { if (blockTimer < 0.25f && PPRConfig.NormalParryReflect.Value) { Plugin.Log.LogInfo((object)$"[PPR] 패링 성공(반사) (blockTimer={blockTimer:F3}s)"); PPRProjectileReflectPatch.TryReflect(__instance, hit, attacker, isPerfect: false); } else { Plugin.Log.LogInfo((object)$"[PPR] 가드만(반사 없음) (blockTimer={blockTimer:F3}s)"); } } } } public static class PPRConfig { private const string SEC = "Perfect Parry"; private const string SEC_DMG = "Perfect Parry - Damage Reduction"; private const string SEC_VFX = "Perfect Parry - Effect"; private const string SEC_RANGED = "Perfect Parry - Ranged"; private const string SEC_SHIELDASSIGN = "Shield Assignment"; private const string SEC_AUTOSHIELD = "Auto Shield Equip"; private const string SEC_BACKGUARD = "Back Shield Guard"; private const string SEC_GUARDDUR = "Guard Durability"; private const string SEC_EQUIPCANCEL = "Equip Motion Cancel"; private const string SEC_GUARDSPRINT = "Guard Sprint"; private const string SEC_STAGGERREDUCTION = "Stagger Reduction"; private const string SEC_DEBUG = "Debug"; public static ConfigEntry<bool> Enable; public static ConfigEntry<float> ParryWindow; public static ConfigEntry<float> CounterWindow; public static ConfigEntry<float> CounterSpeedMultiplier; public static ConfigEntry<bool> DamageReductionEnabled; public static ConfigEntry<float> DamageReductionPercent; public static ConfigEntry<bool> VFXEnabled; public static ConfigEntry<string> VFXPrefab; public static ConfigEntry<float> VFXOffsetX; public static ConfigEntry<float> VFXOffsetY; public static ConfigEntry<float> VFXOffsetZ; public static ConfigEntry<bool> VFXColorEnabled; public static ConfigEntry<string> VFXColorHex; public static ConfigEntry<bool> RangedNoStagger; public static ConfigEntry<bool> ProjectileReflect; public static ConfigEntry<float> ProjectileReflectDamageMult; public static ConfigEntry<bool> NormalParryReflect; public static ConfigEntry<float> NormalParryReflectDamageMult; public static ConfigEntry<KeyCode> ShieldAssignKey; public static ConfigEntry<bool> ShieldAssignIconsEnabled; public static ConfigEntry<bool> AutoEquipShieldEnabled; public static ConfigEntry<bool> BackGuardEnabled; public static ConfigEntry<float> BackGuardHalfAngle; public static ConfigEntry<float> BackGuardOffsetX; public static ConfigEntry<float> BackGuardOffsetY; public static ConfigEntry<float> BackGuardOffsetZ; public static ConfigEntry<float> BackGuardRotationX; public static ConfigEntry<float> BackGuardRotationY; public static ConfigEntry<float> BackGuardRotationZ; public static ConfigEntry<bool> NoArmorWearOnGuard; public static ConfigEntry<bool> NoDurabilityLossWeapon; public static ConfigEntry<bool> NoDurabilityLossTool; public static ConfigEntry<bool> NoDurabilityLossShield; public static ConfigEntry<bool> EquipMotionCancelEnabled; public static ConfigEntry<bool> EquipCancelShield; public static ConfigEntry<bool> EquipCancelHelmet; public static ConfigEntry<bool> EquipCancelChest; public static ConfigEntry<bool> EquipCancelLegs; public static ConfigEntry<bool> EquipCancelCape; public static ConfigEntry<bool> EquipCancelUtility; public static ConfigEntry<bool> EquipCancelTrinket; public static ConfigEntry<bool> EquipCancelWeapon; public static ConfigEntry<bool> EquipCancelTool; public static ConfigEntry<bool> EquipCancelHideShow; public static ConfigEntry<bool> GuardSprintEnabled; public static ConfigEntry<float> GuardSprintMinBlockingSkill; public static ConfigEntry<float> GuardSprintBucklerPenaltyPercent; public static ConfigEntry<float> GuardSprintNormalShieldPenaltyPercent; public static ConfigEntry<float> GuardSprintTowerPenaltyPercent; public static ConfigEntry<float> GuardSprintWeaponPenaltyPercent; public static ConfigEntry<string> GuardSprintBucklerPrefabs; public static ConfigEntry<string> GuardSprintNormalPrefabs; public static ConfigEntry<string> GuardSprintTowerPrefabs; public static ConfigEntry<bool> GuardSprintSyncLegAnimation; public static ConfigEntry<float> GuardSprintMaxLegSpeedUp; public static ConfigEntry<float> GuardSprintStaminaMultiplier; public static ConfigEntry<bool> StaggerReductionEnabled; public static ConfigEntry<float> StaggerReductionMaxPercent; public static ConfigEntry<bool> StaggerReductionApplyToUnblockedHits; public static ConfigEntry<bool> VerboseLog; private static readonly PropertyInfo OrphanedEntriesProp = AccessTools.Property(typeof(ConfigFile), "OrphanedEntries"); public static void Init(ConfigFile cfg) { PPRLocalizationManager.Init(); Enable = BindSetting(cfg, "Perfect Parry", "(Enable) Perfect Parry", defaultValue: true); ParryWindow = BindSetting(cfg, "Perfect Parry", "(Parry) Detection Window (sec)", 0.1f, (AcceptableValueBase)(object)new AcceptableValueRange<float>(0.01f, 0.24f)); CounterWindow = BindSetting(cfg, "Perfect Parry", "(Counter) Input Window (sec)", 1f, (AcceptableValueBase)(object)new AcceptableValueRange<float>(0.1f, 2f)); CounterSpeedMultiplier = BindSetting(cfg, "Perfect Parry", "(Counter) Animation Speed Multiplier", 2f, (AcceptableValueBase)(object)new AcceptableValueRange<float>(1f, 5f)); DamageReductionEnabled = BindSetting(cfg, "Perfect Parry - Damage Reduction", "(Enable) Damage Reduction", defaultValue: true); DamageReductionPercent = BindSetting(cfg, "Perfect Parry - Damage Reduction", "(Damage Reduction) Reduction Rate (%)", 50f, (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 100f)); VFXEnabled = BindSetting(cfg, "Perfect Parry - Effect", "(Enable) Head Effect", defaultValue: true); VFXPrefab = BindSetting(cfg, "Perfect Parry - Effect", "(Effect) VFX Name", "vfx_blocked"); VFXOffsetX = BindSetting(cfg, "Perfect Parry - Effect", "(Effect) Offset X", 0f, (AcceptableValueBase)(object)new AcceptableValueRange<float>(-5f, 5f)); VFXOffsetY = BindSetting(cfg, "Perfect Parry - Effect", "(Effect) Offset Y", 1.25f, (AcceptableValueBase)(object)new AcceptableValueRange<float>(-2f, 8f)); VFXOffsetZ = BindSetting(cfg, "Perfect Parry - Effect", "(Effect) Offset Z", 0f, (AcceptableValueBase)(object)new AcceptableValueRange<float>(-5f, 5f)); VFXColorEnabled = BindSetting(cfg, "Perfect Parry - Effect", "(Effect) Custom Color Enabled", defaultValue: true); VFXColorHex = BindSetting(cfg, "Perfect Parry - Effect", "(Effect) Custom Color (Hex)", "#4DB8FF"); RangedNoStagger = BindSetting(cfg, "Perfect Parry - Ranged", "(Enable) Ranged No Stagger", defaultValue: true); ProjectileReflect = BindSetting(cfg, "Perfect Parry - Ranged", "(Enable) Projectile Reflect", defaultValue: true); ProjectileReflectDamageMult = BindSetting(cfg, "Perfect Parry - Ranged", "(Reflect) Perfect Parry Damage Multiplier", 3f, (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 10f)); NormalParryReflect = BindSetting(cfg, "Perfect Parry - Ranged", "(Enable) Normal Parry Reflect", defaultValue: true); NormalParryReflectDamageMult = BindSetting(cfg, "Perfect Parry - Ranged", "(Reflect) Normal Parry Damage Multiplier", 2f, (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 10f)); ShieldAssignKey = BindSetting<KeyCode>(cfg, "Shield Assignment", "(Input) Assign Key", (KeyCode)325); ShieldAssignIconsEnabled = BindSetting(cfg, "Shield Assignment", "(Display) Show Role Icons", defaultValue: true); AutoEquipShieldEnabled = BindSetting(cfg, "Auto Shield Equip", "(Enable) Auto-Equip Assigned Shield", defaultValue: true); BackGuardEnabled = BindSetting(cfg, "Back Shield Guard", "(Enable) Back Shield Guard", defaultValue: true); BackGuardHalfAngle = BindSetting(cfg, "Back Shield Guard", "(Guard) Rear Cone Half-Angle (deg)", 60f, (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 90f)); BackGuardOffsetX = BindSetting(cfg, "Back Shield Guard", "(Visual) Position Offset X", 0f, (AcceptableValueBase)(object)new AcceptableValueRange<float>(-2f, 2f)); BackGuardOffsetY = BindSetting(cfg, "Back Shield Guard", "(Visual) Position Offset Y", 0f, (AcceptableValueBase)(object)new AcceptableValueRange<float>(-2f, 2f)); BackGuardOffsetZ = BindSetting(cfg, "Back Shield Guard", "(Visual) Position Offset Z", 0f, (AcceptableValueBase)(object)new AcceptableValueRange<float>(-2f, 2f)); BackGuardRotationX = BindSetting(cfg, "Back Shield Guard", "(Visual) Rotation Offset X", 0f, (AcceptableValueBase)(object)new AcceptableValueRange<float>(-180f, 180f)); BackGuardRotationY = BindSetting(cfg, "Back Shield Guard", "(Visual) Rotation Offset Y", 0f, (AcceptableValueBase)(object)new AcceptableValueRange<float>(-180f, 180f)); BackGuardRotationZ = BindSetting(cfg, "Back Shield Guard", "(Visual) Rotation Offset Z", 0f, (AcceptableValueBase)(object)new AcceptableValueRange<float>(-180f, 180f)); NoArmorWearOnGuard = BindSetting(cfg, "Guard Durability", "(Armor) No Armor Wear On Guard", defaultValue: true); NoDurabilityLossWeapon = BindSetting(cfg, "Guard Durability", "(Blocker) No Durability Loss - Weapon", defaultValue: true); NoDurabilityLossTool = BindSetting(cfg, "Guard Durability", "(Blocker) No Durability Loss - Tool", defaultValue: true); NoDurabilityLossShield = BindSetting(cfg, "Guard Durability", "(Blocker) No Durability Loss - Shield", defaultValue: false); EquipMotionCancelEnabled = BindSetting(cfg, "Equip Motion Cancel", "(Enable) Equip Motion Cancel", defaultValue: true); EquipCancelShield = BindSetting(cfg, "Equip Motion Cancel", "(Cancel) Shield", defaultValue: true); EquipCancelHelmet = BindSetting(cfg, "Equip Motion Cancel", "(Cancel) Helmet", defaultValue: true); EquipCancelChest = BindSetting(cfg, "Equip Motion Cancel", "(Cancel) Chest", defaultValue: true); EquipCancelLegs = BindSetting(cfg, "Equip Motion Cancel", "(Cancel) Legs", defaultValue: true); EquipCancelCape = BindSetting(cfg, "Equip Motion Cancel", "(Cancel) Cape", defaultValue: true); EquipCancelUtility = BindSetting(cfg, "Equip Motion Cancel", "(Cancel) Utility", defaultValue: true); EquipCancelTrinket = BindSetting(cfg, "Equip Motion Cancel", "(Cancel) Trinket", defaultValue: true); EquipCancelWeapon = BindSetting(cfg, "Equip Motion Cancel", "(Cancel) Weapon", defaultValue: true); EquipCancelTool = BindSetting(cfg, "Equip Motion Cancel", "(Cancel) Tool", defaultValue: true); EquipCancelHideShow = BindSetting(cfg, "Equip Motion Cancel", "(Cancel) Hide/Show Hand Items (R)", defaultValue: true); GuardSprintEnabled = BindSetting(cfg, "Guard Sprint", "(Enable) Guard Sprint", defaultValue: true); GuardSprintMinBlockingSkill = BindSetting(cfg, "Guard Sprint", "(Requirement) Min Blocking Skill", 20f, (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 100f)); GuardSprintBucklerPenaltyPercent = BindSetting(cfg, "Guard Sprint", "(Speed) Buckler Penalty %", 0f, (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 100f)); GuardSprintNormalShieldPenaltyPercent = BindSetting(cfg, "Guard Sprint", "(Speed) Normal Shield Penalty %", 10f, (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 100f)); GuardSprintTowerPenaltyPercent = BindSetting(cfg, "Guard Sprint", "(Speed) Tower Shield Penalty %", 20f, (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 100f)); GuardSprintWeaponPenaltyPercent = BindSetting(cfg, "Guard Sprint", "(Speed) Weapon Penalty %", 0f, (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 100f)); GuardSprintBucklerPrefabs = BindSetting(cfg, "Guard Sprint", "(Classify) Buckler Prefabs", ""); GuardSprintNormalPrefabs = BindSetting(cfg, "Guard Sprint", "(Classify) Normal Prefabs", ""); GuardSprintTowerPrefabs = BindSetting(cfg, "Guard Sprint", "(Classify) Tower Prefabs", ""); GuardSprintSyncLegAnimation = BindSetting(cfg, "Guard Sprint", "(Animation) Sync Leg Animation", defaultValue: true); GuardSprintMaxLegSpeedUp = BindSetting(cfg, "Guard Sprint", "(Animation) Max Leg Speed-Up", 2f, (AcceptableValueBase)(object)new AcceptableValueRange<float>(1f, 5f)); GuardSprintStaminaMultiplier = BindSetting(cfg, "Guard Sprint", "(Stamina) Guard Sprint Stamina Multiplier", 1.1f, (AcceptableValueBase)(object)new AcceptableValueRange<float>(0.1f, 5f)); StaggerReductionEnabled = BindSetting(cfg, "Stagger Reduction", "(Enable) Stagger Reduction", defaultValue: true); StaggerReductionMaxPercent = BindSetting(cfg, "Stagger Reduction", "(Reduction) Max Reduction %", 50f, (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 100f)); StaggerReductionApplyToUnblockedHits = BindSetting(cfg, "Stagger Reduction", "(Scope) Apply To Unblocked Hits", defaultValue: false); VerboseLog = BindSetting(cfg, "Debug", "(Enable) Verbose Log", defaultValue: false); CleanupOrphanedEntries(cfg); PPRConfigDisplayLocalization.ApplyDisplayNames(cfg); PPRConfigDisplayLocalization.HookLanguageChangeEvent(); PPRLocalizationManager.WarnMissingDescriptions(); } private static ConfigEntry<T> BindSetting<T>(ConfigFile cfg, string section, string key, T defaultValue, AcceptableValueBase acceptableValues = null) { //IL_001f: Unknown result type (might be due to invalid IL or missing references) //IL_0029: Expected O, but got Unknown string text = PPRLocalizationManager.Desc(key); ConfigurationManagerAttributes configurationManagerAttributes = new ConfigurationManagerAttributes(); return cfg.Bind<T>(section, key, defaultValue, new ConfigDescription(text, acceptableValues, new object[1] { configurationManagerAttributes })); } private static void CleanupOrphanedEntries(ConfigFile cfg) { try { if (OrphanedEntriesProp?.GetValue(cfg) is IDictionary { Count: not 0, Count: var count } dictionary) { dictionary.Clear(); cfg.Save(); Plugin.Log.LogInfo((object)$"[PPR:설정정리] 연결되지 않은 옛 설정 항목 {count}개 삭제 후 저장."); } } catch (Exception ex) { Plugin.Log.LogWarning((object)("[PPR] 옛 설정 정리 실패(무해, 옛 항목이 파일에 남을 수 있음): " + ex.Message)); } } } [HarmonyPatch(typeof(Character), "ApplyDamage")] public static class PPRDamagePatch { [HarmonyPrefix] public static void Prefix(Character __instance, HitData hit) { if (!PPRConfig.Enable.Value || !PPRConfig.DamageReductionEnabled.Value) { return; } Player val = (Player)(object)((__instance is Player) ? __instance : null); if (!((Object)(object)val == (Object)null) && !((Object)(object)val != (Object)(object)Player.m_localPlayer) && hit != null) { PPRExceptionGuard.Run("DamageReduction.ApplyDamage", delegate { ApplyDamageReduction(val, hit); }); } } private static void ApplyDamageReduction(Player val, HitData hit) { if (PPRState.IsParryWindowActive(val)) { float totalDamage = ((DamageTypes)(ref hit.m_damage)).GetTotalDamage(); float num = 1f - PPRConfig.DamageReductionPercent.Value / 100f; ((DamageTypes)(ref hit.m_damage)).Modify(num); float totalDamage2 = ((DamageTypes)(ref hit.m_damage)).GetTotalDamage(); Plugin.Log.LogInfo((object)("[PPR] 데미지 감소 적용 (카운터윈도우, 퍼펙트패링) - " + $"배율={num:F2}x({PPRConfig.DamageReductionPercent.Value}%), 적용전={totalDamage:F1} → 적용후={totalDamage2:F1}")); } } } internal static class PPRDebugLog { public static void Log(string message) { Log("PPR-SHIELD", message); } public static void Warn(string message) { Warn("PPR-SHIELD", message); } public static void Log(string tag, string message) { if (PPRConfig.VerboseLog != null && PPRConfig.VerboseLog.Value) { Plugin.Log.LogInfo((object)("[" + tag + "] " + message)); } } public static void Warn(string tag, string message) { if (PPRConfig.VerboseLog != null && PPRConfig.VerboseLog.Value) { Plugin.Log.LogWarning((object)("[" + tag + "] " + message)); } } } internal static class PPREquipGuard { private static int _depth; public static bool Suppressed => _depth > 0; public static void PushSuppressed() { _depth++; } public static void PopSuppressed() { if (_depth <= 0) { Plugin.Log.LogWarning((object)"[PPR-SHIELD] PPREquipGuard.PopSuppressed 호출 시 깊이가 이미 0 — Push/Pop 짝이 맞지 않음(예외 경로에서 Pop이 빠졌을 가능성)."); } else { _depth--; } } public static void RunSuppressed(Action action) { PushSuppressed(); try { action(); } finally { PopSuppressed(); } } } [HarmonyPatch] public static class PPREquipMotionCancelPatch { private enum EquipClass { Shield, Helmet, Chest, Legs, Cape, Utility, Trinket, Weapon, Tool, Other } [HarmonyPatch(typeof(Player), "ToggleEquipped")] [HarmonyPrefix] public static bool ToggleEquipped_Prefix(Player __instance, ItemData item, ref bool __result) { if (!PPRConfig.EquipMotionCancelEnabled.Value) { return true; } if (item == null) { return true; } if ((Object)(object)__instance != (Object)(object)Player.m_localPlayer) { return true; } var (flag, flag2) = PPRExceptionGuard.Run("EquipMotionCancel.ToggleEquipped", () => ((bool, bool))TryInstantToggle(__instance, item), (false, false)); if (!flag) { return true; } __result = flag2; return false; } private static (bool handled, bool result) TryInstantToggle(Player __instance, ItemData item) { if (!item.IsEquipable()) { return (handled: false, result: false); } if (((Character)__instance).InAttack()) { return (handled: true, result: true); } if (!IsCancelEnabledFor(item)) { return (handled: false, result: false); } if (__instance.IsEquipActionQueued(item)) { ((Humanoid)__instance).RemoveEquipAction(item); } bool flag = ((Humanoid)__instance).IsItemEquiped(item); if (flag) { ((Humanoid)__instance).UnequipItem(item, true); } else { ((Humanoid)__instance).EquipItem(item, true); } PPRDebugLog.Log("PPR-EQUIP", string.Format("즉시 {0} — {1}({2})", flag ? "해제" : "장착", item.m_shared.m_name, ClassifyEquip(item))); return (handled: true, result: true); } private static EquipClass ClassifyEquip(ItemData item) { //IL_0007: Unknown result type (might be due to invalid IL or missing references) //IL_000c: Unknown result type (might be due to invalid IL or missing references) //IL_000d: Unknown result type (might be due to invalid IL or missing references) //IL_000e: Unknown result type (might be due to invalid IL or missing references) //IL_000f: Unknown result type (might be due to invalid IL or missing references) //IL_0011: Unknown result type (might be due to invalid IL or missing references) //IL_006f: Expected I4, but got Unknown ItemType itemType = item.m_shared.m_itemType; ItemType val = itemType; switch (val - 3) { case 2: return EquipClass.Shield; case 3: return EquipClass.Helmet; case 4: return EquipClass.Chest; case 8: return EquipClass.Legs; case 14: return EquipClass.Cape; case 15: return EquipClass.Utility; case 21: return EquipClass.Trinket; case 16: return EquipClass.Tool; case 0: case 1: case 11: case 12: case 19: return EquipClass.Weapon; default: return EquipClass.Other; } } private static bool IsCancelEnabledFor(ItemData item) { EquipClass equipClass = ClassifyEquip(item); if (1 == 0) { } bool result = equipClass switch { EquipClass.Shield => PPRConfig.EquipCancelShield.Value, EquipClass.Helmet => PPRConfig.EquipCancelHelmet.Value, EquipClass.Chest => PPRConfig.EquipCancelChest.Value, EquipClass.Legs => PPRConfig.EquipCancelLegs.Value, EquipClass.Cape => PPRConfig.EquipCancelCape.Value, EquipClass.Utility => PPRConfig.EquipCancelUtility.Value, EquipClass.Trinket => PPRConfig.EquipCancelTrinket.Value, EquipClass.Weapon => PPRConfig.EquipCancelWeapon.Value, EquipClass.Tool => PPRConfig.EquipCancelTool.Value, _ => false, }; if (1 == 0) { } return result; } [HarmonyPatch(typeof(Humanoid), "HideHandItems")] [HarmonyPrefix] public static void HideHandItems_Prefix(Humanoid __instance, ref bool animation) { if (PPRConfig.EquipMotionCancelEnabled.Value && PPRConfig.EquipCancelHideShow.Value && !((Object)(object)__instance != (Object)(object)Player.m_localPlayer)) { if (__instance.LeftItem == null && __instance.RightItem == null) { animation = false; } else if (PPRExceptionGuard.Run("EquipMotionCancel.HideHandItems", LogHideCancelled, fallback: false)) { animation = false; } } } [HarmonyPatch(typeof(Humanoid), "ShowHandItems")] [HarmonyPrefix] public static void ShowHandItems_Prefix(Humanoid __instance, ref bool animation) { if (PPRConfig.EquipMotionCancelEnabled.Value && PPRConfig.EquipCancelHideShow.Value && !((Object)(object)__instance != (Object)(object)Player.m_localPlayer)) { if (PPRAccess.GetHiddenLeftItem(__instance) == null && PPRAccess.GetHiddenRightItem(__instance) == null) { animation = false; } else if (PPRExceptionGuard.Run("EquipMotionCancel.ShowHandItems", LogShowCancelled, fallback: false)) { animation = false; } } } private static bool LogHideCancelled() { PPRDebugLog.Log("PPR-EQUIP", "손 아이템 숨김 — 모션 생략."); return true; } private static bool LogShowCancelled() { PPRDebugLog.Log("PPR-EQUIP", "손 아이템 꺼냄 — 모션 생략."); return true; } } internal static class PPRExceptionGuard { private const int SampleInterval = 200; private static readonly HashSet<string> _disabled = new HashSet<string>(); private static readonly Dictionary<string, int> _skipCounts = new Dictionary<string, int>(); public static void Run(string featureKey, Action logic) { if (IsDisabledAndCounted(featureKey)) { return; } try { logic(); } catch (Exception ex) { Disable(featureKey, ex); } } public static T Run<T>(string featureKey, Func<T> logic, T fallback) { if (IsDisabledAndCounted(featureKey)) { return fallback; } try { return logic(); } catch (Exception ex) { Disable(featureKey, ex); return fallback; } } private static bool IsDisabledAndCounted(string featureKey) { if (!_disabled.Contains(featureKey)) { return false; } int value; int num = ((!_skipCounts.TryGetValue(featureKey, out value)) ? 1 : (value + 1)); _skipCounts[featureKey] = num; if (num % 200 == 0) { Plugin.Log.LogWarning((object)$"[PPR:예외격리] '{featureKey}' 비활성화 상태 유지 중 — 누적 스킵 {num}회."); } return true; } private static void Disable(string featureKey, Exception ex) { _disabled.Add(featureKey); Plugin.Log.LogError((object)$"[PPR:예외격리] '{featureKey}' 예외 발생 — 이번 세션 동안 이 기능만 비활성화(나머지 PPR 기능/게임은 계속 동작):\n{ex}"); } } internal static class PPRFreezeReporter { private static readonly object Lock = new object(); private static string _reportPath; private static string ReportPath { get { if (_reportPath == null) { string path = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location) ?? "."; _reportPath = Path.Combine(path, "PPR-freeze-report.txt"); } return _reportPath; } } public static void Append(string section, string body) { lock (Lock) { try { string contents = string.Format("===== [{0}] {1:yyyy-MM-dd HH:mm:ss} (PPR v{2}) =====\n{3}\n\n", section, DateTime.Now, "1.2.11", body); File.AppendAllText(ReportPath, contents); } catch (Exception ex) { Plugin.Log.LogWarning((object)("[PPR-REPORT] 진단 파일 기록 실패(무시): " + ex.Message)); } } } } [HarmonyPatch] public static class PPRGuardDurabilityPatch { public enum BlockerClass { Shield, Tool, Weapon } private static bool _frontGuardPending; private static ItemData _pendingBlocker; private static float _pendingBlockerDurabilityBefore; private static float _pendingBlockableDamageBefore; [HarmonyPatch(typeof(Character), "RPC_Damage")] [HarmonyPrefix] public static void RPC_Damage_Prefix() { PPRGuardDurabilityState.Clear(); } [HarmonyPatch(typeof(Character), "RPC_Damage")] [HarmonyFinalizer] public static Exception RPC_Damage_Finalizer(Exception __exception) { PPRGuardDurabilityState.Clear(); return __exception; } [HarmonyPatch(typeof(Humanoid), "BlockAttack")] [HarmonyPrefix] public static void BlockAttack_Prefix(Humanoid __instance, HitData hit) { _frontGuardPending = false; if (hit != null && AnyProtectionEnabled()) { PPRExceptionGuard.Run("GuardDurability.BlockAttack.Prefix", delegate { CaptureBeforeState(__instance, hit); }); } } private static void CaptureBeforeState(Humanoid __instance, HitData hit) { ItemData currentBlocker = PPRAccess.GetCurrentBlocker(__instance); if (currentBlocker != null) { _pendingBlocker = currentBlocker; _pendingBlockerDurabilityBefore = currentBlocker.m_durability; _pendingBlockableDamageBefore = hit.GetTotalBlockableDamage(); _frontGuardPending = true; } } [HarmonyPatch(typeof(Humanoid), "BlockAttack")] [HarmonyPostfix] public static void BlockAttack_Postfix(HitData hit, bool __result) { bool frontGuardPending = _frontGuardPending; _frontGuardPending = false; if (frontGuardPending && __result && hit != null) { PPRExceptionGuard.Run("GuardDurability.BlockAttack.Postfix", delegate { EvaluateAfterState(hit); }); } } private static void EvaluateAfterState(HitData hit) { if (!(hit.GetTotalBlockableDamage() >= _pendingBlockableDamageBefore)) { PPRGuardDurabilityState.MarkGuardSucceeded(); ItemData pendingBlocker = _pendingBlocker; if (pendingBlocker != null && ShouldProtectBlocker(pendingBlocker) && pendingBlocker.m_durability < _pendingBlockerDurabilityBefore) { pendingBlocker.m_durability = _pendingBlockerDurabilityBefore; PPRDebugLog.Log("PPR-GUARD", $"블로커 내구도 복원(정면) — {pendingBlocker.m_shared.m_name}({ClassifyBlocker(pendingBlocker)})"); } } } [HarmonyPatch(typeof(Player), "DamageArmorDurability")] [HarmonyPrefix] public static bool DamageArmorDurability_Prefix(Player __instance) { if ((Object)(object)__instance != (Object)(object)Player.m_localPlayer) { return true; } if (!PPRConfig.NoArmorWearOnGuard.Value) { return true; } return PPRExceptionGuard.Run("GuardDurability.DamageArmorDurability", ShouldSkipArmorDurability, fallback: true); } private static bool ShouldSkipArmorDurability() { if (!PPRGuardDurabilityState.GuardSucceeded) { return true; } PPRDebugLog.Log("PPR-GUARD", "방어구 내구도 감소 생략 (가드 성공)"); return false; } public static BlockerClass ClassifyBlocker(ItemData item) { //IL_0007: Unknown result type (might be due to invalid IL or missing references) //IL_000d: Invalid comparison between Unknown and I4 //IL_001d: Unknown result type (might be due to invalid IL or missing references) //IL_0024: Invalid comparison between Unknown and I4 //IL_002c: Unknown result type (might be due to invalid IL or missing references) //IL_0033: Invalid comparison between Unknown and I4 if ((int)item.m_shared.m_itemType == 5) { return BlockerClass.Shield; } if ((int)item.m_shared.m_itemType == 19 || (int)item.m_shared.m_skillType == 12) { return BlockerClass.Tool; } return BlockerClass.Weapon; } public static bool ShouldProtectBlocker(ItemData item) { if (item == null) { return false; } BlockerClass blockerClass = ClassifyBlocker(item); if (1 == 0) { } bool result = blockerClass switch { BlockerClass.Shield => PPRConfig.NoDurabilityLossShield.Value, BlockerClass.Tool => PPRConfig.NoDurabilityLossTool.Value, _ => PPRConfig.NoDurabilityLossWeapon.Value, }; if (1 == 0) { } return result; } private static bool AnyProtectionEnabled() { return PPRConfig.NoArmorWearOnGuard.Value || PPRConfig.NoDurabilityLossWeapon.Value || PPRConfig.NoDurabilityLossTool.Value || PPRConfig.NoDurabilityLossShield.Value; } } internal static class PPRGuardDurabilityState { private static bool _guardSucceeded; public static bool GuardSucceeded => _guardSucceeded; public static void MarkGuardSucceeded() { _guardSucceeded = true; } public static void Clear() { _guardSucceeded = false; } } [HarmonyPatch] public static class PPRGuardSprintPatch { private const float LogDebounceSeconds = 0.2f; private const float MinSegmentLogSeconds = 0.5f; private static bool _dumped; private static bool _diagGuardWalkLogged; private static bool _diagGuardSprintLogged; private static bool _diagPlainSprintLogged; private static readonly int ParamForwardSpeed = Animator.StringToHash("forward_speed"); private static readonly int ParamSidewaySpeed = Animator.StringToHash("sideway_speed"); private static readonly int ParamTurnSpeed = Animator.StringToHash("turn_speed"); private static readonly int ParamBlocking = Animator.StringToHash("blocking"); private static readonly int ParamOnGround = Animator.StringToHash("onground"); [HarmonyPatch(typeof(Humanoid), "CheckRun")] [HarmonyPrefix] public static void CheckRun_Prefix(Humanoid __instance) { PPRGuardSprintState.SuppressBlockCheck = false; Player player = (Player)(object)((__instance is Player) ? __instance : null); if ((Object)(object)player == (Object)null || (Object)(object)player != (Object)(object)Player.m_localPlayer) { return; } PPRExceptionGuard.Run("GuardSprint.CheckRun", delegate { if (TryGetEligibleBlocker(player, out var _)) { PPRGuardSprintState.SuppressBlockCheck = true; } }); } [HarmonyPatch(typeof(Humanoid), "CheckRun")] [HarmonyFinalizer] public static Exception CheckRun_Finalizer(Exception __exception) { PPRGuardSprintState.SuppressBlockCheck = false; return __exception; } [HarmonyPatch(typeof(Humanoid), "IsBlocking")] [HarmonyPrefix] public static bool IsBlocking_SuppressPrefix(ref bool __result) { if (!PPRGuardSprintState.SuppressBlockCheck) { return true; } __result = false; return false; } [HarmonyPatch(typeof(Player), "GetRunSpeedFactor")] [HarmonyPostfix] public static void GetRunSpeedFactor_Postfix(Player __instance, ref float __result) { if (!((Object)(object)__instance != (Object)(object)Player.m_localPlayer)) { float current = __result; __result = PPRExceptionGuard.Run("GuardSprint.Speed", () => ApplySpeed(__instance, current), current); } } private static float ApplySpeed(Player player, float baseFactor) { ItemData blocker; bool flag = TryGetEligibleBlocker(player, out blocker); PPRShieldClass cls = (flag ? PPRShieldClassifier.ClassifyBlockerForSpeed(blocker) : PPRShieldClass.Buckler); float num = (flag ? PPRShieldClassifier.GetPenalty(cls) : 0f); bool rawActive = flag && ((Character)player).IsBlocking(); bool flag2 = UpdateActiveDebounce(player, rawActive); float num2 = (flag2 ? (1f - num) : 1f); float speedMps = ((Character)player).m_runSpeed * baseFactor * num2; UpdateSegmentTracking(player, flag2, blocker, cls, num, speedMps); return baseFactor * num2; } private static bool UpdateActiveDebounce(Player player, bool rawActive) { if (rawActive) { PPRGuardSprintState.SetInactiveGraceTimer(player, 0f); } else { PPRGuardSprintState.SetInactiveGraceTimer(player, PPRGuardSprintState.InactiveGraceTimer(player) + Time.deltaTime); } return rawActive || PPRGuardSprintState.InactiveGraceTimer(player) < 0.2f; } private static void UpdateSegmentTracking(Player player, bool effectiveActive, ItemData blocker, PPRShieldClass cls, float penalty, float speedMps) { PPRSprintSegment pPRSprintSegment = (effectiveActive ? PPRSprintSegment.Guard : PPRSprintSegment.Plain); PPRSprintSegment pPRSprintSegment2 = PPRGuardSprintState.CurrentSegment(player); if (pPRSprintSegment != pPRSprintSegment2) { LogSegmentEnd(player, pPRSprintSegment2); PPRGuardSprintState.ResetSegment(player, pPRSprintSegment); if (pPRSprintSegment == PPRSprintSegment.Guard) { string text = blocker?.m_shared?.m_name ?? "?"; PPRGuardSprintState.SetLastShield(player, text, cls); if (PPRConfig.VerboseLog.Value) { float value = PPRConfig.GuardSprintStaminaMultiplier.Value; PPRDebugLog.Log("PPR-SPRINT", $"가드 질주 시작 — {text}({cls}), 감속 {penalty * 100f:F0}%, {speedMps:F2}m/s, 스태미나 배율 {value:F2}"); } } } var (actual, vanilla) = PPRGuardSprintState.ConsumePendingStamina(player); PPRGuardSprintState.AddSegmentStamina(player, actual, vanilla); PPRGuardSprintState.AdvanceSegmentElapsed(player, Time.deltaTime); PPRGuardSprintState.SetSegmentLastSpeedMps(player, speedMps); } private static void CloseCurrentSegment(Player player) { PPRSprintSegment pPRSprintSegment = PPRGuardSprintState.CurrentSegment(player); if (pPRSprintSegment != PPRSprintSegment.None) { LogSegmentEnd(player, pPRSprintSegment); PPRGuardSprintState.ResetSegment(player, PPRSprintSegment.None); } } private static void LogSegmentEnd(Player player, PPRSprintSegment segment) { if (segment == PPRSprintSegment.None || !PPRConfig.VerboseLog.Value) { return; } float num = PPRGuardSprintState.SegmentElapsed(player); if (!(num < 0.5f)) { float num2 = PPRGuardSprintState.SegmentActualStamina(player); float num3 = PPRGuardSprintState.SegmentLastSpeedMps(player); float num4 = ((num > 0.0001f) ? (num2 / num) : 0f); if (segment == PPRSprintSegment.Guard) { float num5 = PPRGuardSprintState.SegmentVanillaStamina(player); float num6 = ((num5 > 0.0001f) ? (num2 / num5) : 1f); string text = PPRGuardSprintState.LastShieldName(player); PPRShieldClass pPRShieldClass = PPRGuardSprintState.LastShieldClass(player); PPRDebugLog.Log("PPR-SPRINT", $"가드 질주 종료 — {text}({pPRShieldClass}), 지속 {num:F1}s, 스태미나 {num2:F1}/{num5:F1}(비율 {num6:F2}), 초당 {num4:F2}"); } else { PPRDebugLog.Log("PPR-SPRINT", $"그냥 질주 종료 — 지속 {num:F1}s, {num3:F2}m/s, 스태미나 {num2:F1}, 초당 {num4:F2}"); } } } [HarmonyPatch(typeof(Player), "CheckRun")] [HarmonyPrefix] public static void PlayerCheckRun_Prefix(Player __instance) { if ((Object)(object)__instance == (Object)(object)Player.m_localPlayer) { PPRGuardSprintState.SetInsideCheckRunStaminaScope(__instance, value: true); } } [HarmonyPatch(typeof(Player), "CheckRun")] [HarmonyPostfix] public static void PlayerCheckRun_Postfix(Player __instance, ref bool __result) { if (!((Object)(object)__instance != (Object)(object)Player.m_localPlayer) && !__result) { PPRExceptionGuard.Run("GuardSprint.SegmentClose", delegate { CloseCurrentSegment(__instance); }); } } [HarmonyPatch(typeof(Player), "CheckRun")] [HarmonyFinalizer] public static Exception PlayerCheckRun_Finalizer(Player __instance, Exception __exception) { if ((Object)(object)__instance == (Object)(object)Player.m_localPlayer) { PPRGuardSprintState.SetInsideCheckRunStaminaScope(__instance, value: false); } return __exception; } [HarmonyPatch(typeof(Player), "UseStamina")] [HarmonyPrefix] public static void UseStamina_Prefix(Player __instance, ref float v) { if (!((Object)(object)__instance != (Object)(object)Player.m_localPlayer) && PPRGuardSprintState.InsideCheckRunStaminaScope(__instance)) { float before = v; v = PPRExceptionGuard.Run("GuardSprint.StaminaMultiply", () => TallyAndMultiplyRunStamina(__instance, before), before); } } private static float TallyAndMultiplyRunStamina(Player player, float before) { float num = before; if (TryGetEligibleBlocker(player, out var _) && ((Character)player).IsBlocking()) { num *= Mathf.Max(0f, PPRConfig.GuardSprintStaminaMultiplier.Value); } PPRGuardSprintState.AddPendingStamina(player, num, before); return num; } public static bool TryGetEligibleBlocker(Player player, out ItemData blocker) { blocker = null; if (!PPRConfig.GuardSprintEnabled.Value) { return false; } ItemData currentBlocker = PPRAccess.GetCurrentBlocker((Humanoid)(object)player); if (currentBlocker == null) { return false; } float value = PPRConfig.GuardSprintMinBlockingSkill.Value; if (value > 0f) { float skillLevel = ((Character)player).GetSkillLevel((SkillType)6); if (skillLevel < value) { if (((Character)player).IsBlocking() && !PPRGuardSprintState.SkillTooLowLatched(player)) { PPRDebugLog.Log("PPR-SPRINT", $"가드 질주 불가 — 막기 스킬 부족({skillLevel:F0} < {value:F0})"); PPRGuardSprintState.SetSkillTooLowLatched(player, value: true); } return false; } PPRGuardSprintState.SetSkillTooLowLatched(player, value: false); } blocker = currentBlocker; return true; } public static float GetJogSpeedMps(Player player) { return ((Character)player).m_speed * (1f + ((Character)player).GetEquipmentMovementModifier()); } public static float GetHorizontalSpeedMps(Player player) { //IL_0002: Unknown result type (might be due to invalid IL or missing references) //IL_0007: Unknown result type (might be due to invalid IL or missing references) Vector3 velocity = ((Character)player).GetVelocity(); velocity.y = 0f; return ((Vector3)(ref velocity)).magnitude; } public static bool TryGetLegSyncMultiplier(Player player, out float multiplier) { multiplier = 1f; if (!PPRConfig.GuardSprintSyncLegAnimation.Value) { return false; } if (!TryGetEligibleBlocker(player, out var _)) { return false; } if (!((Character)player).IsBlocking() || !((Character)player).IsRunning()) { return false; } if (((Character)player).InDodge() || ((Character)player).IsStaggering() || ((Character)player).InAttack()) { return false; } float jogSpeedMps = GetJogSpeedMps(player); if (jogSpeedMps <= 0.0001f) { return false; } float num = GetHorizontalSpeedMps(player) / jogSpeedMps; float num2 = Mathf.Max(1f, PPRConfig.GuardSprintMaxLegSpeedUp.Value); multiplier = Mathf.Clamp(num, 1f, num2); return true; } [HarmonyPatch(typeof(ObjectDB), "CopyOtherDB")] [HarmonyPostfix] public static void ObjectDB_CopyOtherDB_Postfix(ObjectDB __instance) { if (!_dumped && PPRConfig.VerboseLog.Value) { PPRExceptionGuard.Run("GuardSprint.ShieldDump", delegate { DumpShields(__instance); }); } } private static void DumpShields(ObjectDB db) { List<ItemDrop> allItems = db.GetAllItems((ItemType)5, ""); if (allItems == null || allItems.Count == 0) { return; } _dumped = true; Plugin.Log.LogInfo((object)$"[PPR-SPRINT] 방패 판별 덤프 — {allItems.Count}개(바닐라+모드 전체, ObjectDB.CopyOtherDB 이후)"); foreach (ItemDrop item in allItems) { SharedData shared = item.m_itemData.m_shared; PPRShieldClass pPRShieldClass = PPRShieldClassifier.Classify(((Object)((Component)item).gameObject).name, shared); Plugin.Log.LogInfo((object)$"[PPR-SPRINT] {((Object)((Component)item).gameObject).name} / {shared.m_name} -> {pPRShieldClass} (timedBlockBonus={shared.m_timedBlockBonus:F2}, movementModifier={shared.m_movementModifier:F2})"); } } public static void TickAnimatorDiagnostics(Player player) { if (PPRConfig.VerboseLog.Value) { PPRExceptionGuard.Run("GuardSprint.AnimDiag", delegate { DiagnoseAnimator(player); }); } } private static void DiagnoseAnimator(Player player) { ItemData blocker; bool flag = TryGetEligibleBlocker(player, out blocker); bool flag2 = ((Character)player).IsBlocking(); bool flag3 = ((Character)player).IsRunning(); bool flag4 = GetHorizontalSpeedMps(player) > 0.1f; if (!_diagGuardWalkLogged && flag && flag2 && !flag3 && flag4) { _diagGuardWalkLogged = true; DumpAnimatorState(player, "가드하고 걷기"); } else if (!_diagGuardSprintLogged && flag && flag2 && flag3) { _diagGuardSprintLogged = true; DumpAnimatorState(player, "가드 질주"); } else if (!_diagPlainSprintLogged && flag3 && !flag2) { _diagPlainSprintLogged = true; DumpAnimatorState(player, "그냥 질주"); } } private static void DumpAnimatorState(Player player, string situation) { Animator componentInChildren = ((Component)player).GetComponentInChildren<Animator>(); if ((Object)(object)componentInChildren == (Object)null) { Plugin.Log.LogInfo((object)("[PPR-SPRINT-ANIM] " + situation + " — Animator를 찾지 못함.")); return; } Plugin.Log.LogInfo((object)$"[PPR-SPRINT-ANIM] === {situation} — 레이어 {componentInChildren.layerCount}개 ==="); for (int i = 0; i < componentInChildren.layerCount; i++) { string layerName = componentInChildren.GetLayerName(i); float num = ((i == 0) ? 1f : componentInChildren.GetLayerWeight(i)); AnimatorClipInfo[] currentAnimatorClipInfo = componentInChildren.GetCurrentAnimatorClipInfo(i); string text = ((currentAnimatorClipInfo.Length == 0) ? "(클립 없음)" : string.Join(", ", Array.ConvertAll(currentAnimatorClipInfo, delegate(AnimatorClipInfo c) { AnimationClip clip = ((AnimatorClipInfo)(ref c)).clip; return $"{((clip != null) ? ((Object)clip).name : null)}({((AnimatorClipInfo)(ref c)).weight:F2})"; }))); Plugin.Log.LogInfo((object)$"[PPR-SPRINT-ANIM] 레이어[{i}] {layerName} weight={num:F2} 클립={text}"); } Plugin.Log.LogInfo((object)($"[PPR-SPRINT-ANIM] 파라미터 forward_speed={componentInChildren.GetFloat(ParamForwardSpeed):F2} " + $"sideway_speed={componentInChildren.GetFloat(ParamSidewaySpeed):F2} turn_speed={componentInChildren.GetFloat(ParamTurnSpeed):F2} " + $"blocking={componentInChildren.GetBool(ParamBlocking)} onground={componentInChildren.GetBool(ParamOnGround)}")); } } internal enum PPRSprintSegment { None, Guard, Plain } internal static class PPRGuardSprintState { private class Entry { public float InactiveGraceTimer = 999f; public bool SkillTooLowLatched; public string LastShieldName = "?"; public PPRShieldClass LastShieldClass; public PPRSprintSegment CurrentSegment = PPRSprintSegment.None; public float SegmentElapsed; public float SegmentActualStamina; public float SegmentVanillaStamina; public float SegmentLastSpeedMps; public float PendingActualStamina; public float PendingVanillaStamina; public bool InsideCheckRunStaminaScope; } private static readonly Dictionary<Player, Entry> _entries = new Dictionary<Player, Entry>(); public static bool SuppressBlockCheck; private static Entry Get(Player player) { if (!_entries.TryGetValue(player, out var value)) { value = new Entry(); _entries[player] = value; } return value; } public static float InactiveGraceTimer(Player player) { return Get(player).InactiveGraceTimer; } public static void SetInactiveGraceTimer(Player player, float value) { Get(player).InactiveGraceTimer = value; } public static bool SkillTooLowLatched(Player player) { return Get(player).SkillTooLowLatched; } public static void SetSkillTooLowLatched(Player player, bool value) { Get(player).SkillTooLowLatched = value; } public static string LastShieldName(Player player) { return Get(player).LastShieldName; } public static PPRShieldClass LastShieldClass(Player player) { return Get(player).LastShieldClass; } public static void SetLastShield(Player player, string name, PPRShieldClass cls) { Entry entry = Get(player); entry.LastShieldName = name; entry.LastShieldClass = cls; } public static PPRSprintSegment CurrentSegment(Player player) { return Get(player).CurrentSegment; } public static float SegmentElapsed(Player player) { return Get(player).SegmentElapsed; } public static float SegmentActualStamina(Player player) { return Get(player).SegmentActualStamina; } public static float SegmentVanillaStamina(Player player) { return Get(player).SegmentVanillaStamina; } public static float SegmentLastSpeedMps(Player player) { return Get(player).SegmentLastSpeedMps; } public static void ResetSegment(Player player, PPRSprintSegment segment) { Entry entry = Get(player); entry.CurrentSegment = segment; entry.SegmentElapsed = 0f; entry.SegmentActualStamina = 0f; entry.SegmentVanillaStamina = 0f; } public static void AdvanceSegmentElapsed(Player player, float dt) { Get(player).SegmentElapsed += dt; } public static void AddSegmentStamina(Player player, float actual, float vanilla) { Entry entry = Get(player); entry.SegmentActualStamina += actual; entry.SegmentVanillaStamina += vanilla; } public static void SetSegmentLastSpeedMps(Player player, float value) { Get(player).SegmentLastSpeedMps = value; } public static void AddPendingStamina(Player player, float actual, float vanilla) { Entry entry = Get(player); entry.PendingActualStamina += actual; entry.PendingVanillaStamina += vanilla; } public static (float actual, float vanilla) ConsumePendingStamina(Player player) { Entry entry = Get(player); float pendingActualStamina = entry.PendingActualStamina; float pendingVanillaStamina = entry.PendingVanillaStamina; entry.PendingActualStamina = 0f; entry.PendingVanillaStamina = 0f; return (actual: pendingActualStamina, vanilla: pendingVanillaStamina); } public static bool InsideCheckRunStaminaScope(Player player) { return Get(player).InsideCheckRunStaminaScope; } public static void SetInsideCheckRunStaminaScope(Player player, bool value) { Get(player).InsideCheckRunStaminaScope = value; } } [HarmonyPatch] public static class PPRProjectileReflectPatch { private static Projectile _currentProjectile = null; private static Aoe _currentAoe = null; private static DamageTypes _originalDamage; private static readonly FieldInfo _fVel = AccessTools.Field(typeof(Projectile), "m_vel"); private static readonly FieldInfo _fOwner = AccessTools.Field(typeof(Projectile), "m_owner"); private static readonly FieldInfo _fOrigHit = AccessTools.Field(typeof(Projectile), "m_originalHitData"); private static readonly FieldInfo _fDamage = AccessTools.Field(typeof(Projectile), "m_damage"); private static readonly FieldInfo _fBlock = AccessTools.Field(typeof(Projectile), "m_blockable"); private static readonly FieldInfo _fDodge = AccessTools.Field(typeof(Projectile), "m_dodgeable"); private static readonly FieldInfo _fForce = AccessTools.Field(typeof(Projectile), "m_attackForce"); private static readonly FieldInfo _fBackstab = AccessTools.Field(typeof(Projectile), "m_backstabBonus"); private static readonly FieldInfo _fHealth = AccessTools.Field(typeof(Projectile), "m_healthReturn"); private static readonly FieldInfo _fSEHash = AccessTools.Field(typeof(Projectile), "m_statusEffectHash"); [HarmonyPrefix] [HarmonyPatch(typeof(Projectile), "OnHit")] public static void OnHit_Prefix(Projectile __instance) { _currentProjectile = __instance; } [HarmonyPostfix] [HarmonyPatch(typeof(Projectile), "OnHit")] public static void OnHit_Postfix() { _currentProjectile = null; } [HarmonyPrefix] [HarmonyPatch(typeof(Aoe), "OnHit", new Type[] { typeof(Collider), typeof(Vector3) })] public static bool AoeOnHit_Prefix(Aoe __instance) { _currentAoe = __instance; return true; } [HarmonyPostfix] [HarmonyPatch(typeof(Aoe), "OnHit", new Type[] { typeof(Collider), typeof(Vector3) })] public static void AoeOnHit_Postfix() { _currentAoe = null; } [HarmonyPrefix] [HarmonyPatch(typeof(Humanoid), "BlockAttack")] public static void BlockAttack_Prefix(HitData hit) { if (!((Object)(object)_currentProjectile == (Object)null) || !((Object)(object)_currentAoe == (Object)null)) { PPRExceptionGuard.Run("ProjectileReflect.SaveOriginalDamage", delegate { //IL_000c: Unknown result type (might be due to invalid IL or missing references) //IL_0011: Unknown result type (might be due to invalid IL or missing references) _originalDamage = ((DamageTypes)(ref hit.m_damage)).Clone(); }); } } public static void TryReflect(Humanoid blocker, HitData hit, Character attacker, bool isPerfect) { //IL_00cd: Unknown result type (might be due to invalid IL or missing references) //IL_00d2: Unknown result type (might be due to invalid IL or missing references) //IL_0134: Unknown result type (might be due to invalid IL or missing references) //IL_0139: Unknown result type (might be due to invalid IL or missing references) //IL_0162: Unknown result type (might be due to invalid IL or missing references) //IL_0167: Unknown result type (might be due to invalid IL or missing references) //IL_0171: Unknown result type (might be due to invalid IL or missing references) //IL_0176: Unknown result type (might be due to invalid IL or missing references) //IL_017b: Unknown result type (might be due to invalid IL or missing references) //IL_017f: Unknown result type (might be due to invalid IL or missing references) //IL_0181: Unknown result type (might be due to invalid IL or missing references) //IL_0183: Unknown result type (might be due to invalid IL or missing references) //IL_0155: Unknown result type (might be due to invalid IL or missing references) //IL_015a: Unknown result type (might be due to invalid IL or missing references) //IL_0118: Unknown result type (might be due to invalid IL or missing references) //IL_011d: Unknown result type (might be due to invalid IL or missing references) //IL_01aa: Unknown result type (might be due to invalid IL or missing references) //IL_01b1: Expected O, but got Unknown //IL_01b8: Unknown result type (might be due to invalid IL or missing references) //IL_01bd: Unknown result type (might be due to invalid IL or missing references) //IL_0217: Unknown result type (might be due to invalid IL or missing references) //IL_021b: Unknown result type (might be due to invalid IL or missing references) //IL_0267: Unknown result type (might be due to invalid IL or missing references) //IL_026b: Unknown result type (might be due to invalid IL or missing references) Component val = (Component)(((object)_currentProjectile) ?? ((object)_currentAoe)); if ((Object)(object)val == (Object)null || (Object)(object)attacker == (Object)null) { return; } try { string text = ((Object)val.gameObject).name.Replace("(Clone)", "").Trim(); ZNetScene instance = ZNetScene.instance; GameObject val2 = ((instance != null) ? instance.GetPrefab(text) : null); if ((Object)(object)val2 == (Object)null) { Plugin.Log.LogWarning((object)("[PPR] 투사체 반사: prefab 없음 (" + text + ")")); return; } float num = 10f; if ((Object)(object)_currentProjectile != (Object)null && _fVel != null) { Vector3 val3 = (Vector3)_fVel.GetValue(_currentProjectile); num = Mathf.Max(((Vector3)(ref val3)).magnitude, 10f); } else if ((Object)(object)_currentAoe != (Object)null) { Rigidbody component = ((Component)_currentAoe).GetComponent<Rigidbody>(); if ((Object)(object)component != (Object)null) { Vector3 linearVelocity = component.linearVelocity; num = Mathf.Max(((Vector3)(ref linearVelocity)).magnitude, 10f); } } Vector3 val4 = ((Character)blocker).GetLookDir(); if (((Vector3)(ref val4)).sqrMagnitude < 0.001f) { val4 = ((Component)blocker).transform.forward; } Vector3 val5 = ((Component)blocker).transform.position + Vector3.up * 1.5f; GameObject val6 = Object.Instantiate<GameObject>(val2, val5, Quaternion.LookRotation(val4)); float num2 = (isPerfect ? PPRConfig.ProjectileReflectDamageMult.Value : PPRConfig.NormalParryReflectDamageMult.Value); HitData val7 = new HitData(); val7.m_damage = ((DamageTypes)(ref _originalDamage)).Clone(); ((DamageTypes)(ref val7.m_damage)).Modify(num2); val7.m_pushForce = hit.m_pushForce; val7.m_toolTier = hit.m_toolTier; val7.m_ranged = true; val7.SetAttacker((Character)(object)blocker); Projectile component2 = val6.GetComponent<Projectile>(); if ((Object)(object)component2 != (Object)null) { SetupProjectileReflection(component2, (Character)(object)blocker, val4 * num, val7); } else { IProjectile val8 = val6.GetComponent<IProjectile>() ?? val6.GetComponentInChildren<IProjectile>(); if (val8 == null) { ZNetScene instance2 = ZNetScene.instance; if (instance2 != null) { instance2.Destroy(val6); } return; } val8.Setup((Character)(object)blocker, val4 * num, -1f, val7, (ItemData)null, (ItemData)null); } string text2 = (isPerfect ? "퍼펙트" : "일반"); Plugin.Log.LogInfo((object)("[PPR] " + text2 + " 투사체 반사: " + text + " " + $"(speed={num:F1}, dmg={((DamageTypes)(ref val7.m_damage)).GetTotalDamage():F1}x{num2})")); } catch (Exception arg) { Plugin.Log.LogError((object)$"[PPR] 투사체 반사 오류: {arg}"); } } private static void SetupProjectileReflection(Projectile proj, Character owner, Vector3 vel, HitData hitData) { //IL_0020: Unknown result type (might be due to invalid IL or missing references) //IL_004c: Unknown result type (might be due to invalid IL or missing references) _fOwner?.SetValue(proj, owner); _fVel?.SetValue(proj, vel); _fOrigHit?.SetValue(proj, hitData); _fDamage?.SetValue(proj, hitData.m_damage); _fBlock?.SetValue(proj, hitData.m_blockable); _fDodge?.SetValue(proj, hitData.m_dodgeable); _fForce?.SetValue(proj, hitData.m_pushForce); _fBackstab?.SetValue(proj, hitData.m_backstabBonus); _fHealth?.SetValue(proj, hitData.m_healthReturn); _fSEHash?.SetValue(proj, hitData.m_statusEffectHash); } } [HarmonyPatch] public static class PPRRangedNoStaggerPatch { private static Character _patchedAttacker; private static bool _savedStaggerWhenBlocked; [HarmonyPrefix] [HarmonyPatch(typeof(Humanoid), "BlockAttack")] public static void BlockAttack_Prefix(HitData hit, Character attacker) { _patchedAttacker = null; if (PPRConfig.RangedNoStagger.Value && !((Object)(object)attacker == (Object)null)) { PPRExceptionGuard.Run("RangedNoStagger.BlockAttack.Prefix", delegate { PrefixLogic(hit, attacker); }); } } private static void PrefixLogic(HitData hit, Character attacker) { bool flag = hit?.m_ranged ?? false; bool flag2 = IsRangedWeaponEquipped(attacker); if (flag || flag2) { _patchedAttacker = attacker; _savedStaggerWhenBlocked = attacker.m_staggerWhenBlocked; attacker.m_staggerWhenBlocked = false; Plugin.Log.LogInfo((object)$"[PPR] 원거리 BlockAttack: m_staggerWhenBlocked=false (rangedHit={flag}, rangedWeapon={flag2}, attacker={((Object)attacker).name})"); } } [HarmonyPostfix] [HarmonyPatch(typeof(Humanoid), "BlockAttack")] public static void BlockAttack_Postfix() { if (!((Object)(object)_patchedAttacker == (Object)null)) { PPRExceptionGuard.Run("RangedNoStagger.BlockAttack.Postfix", delegate { _patchedAttacker.m_staggerWhenBlocked = _savedStaggerWhenBlocked; _patchedAttacker = null; }); } } private static bool IsRangedWeaponEquipped(Character c) { //IL_0047: Unknown result type (might be due to invalid IL or missing references) //IL_004d: Expected I4, but got Unknown try { if ((Object)(object)c == (Object)null) { return false; } Humanoid val = (Humanoid)(object)((c is Humanoid) ? c : null); if ((Object)(object)val == (Object)null) { return false; } ItemData currentWeapon = val.GetCurrentWeapon(); if (currentWeapon == null) { return false; } int num = (int)currentWeapon.m_shared.m_itemType; return num == 5 || num == 19; } catch { return false; } } } [HarmonyPatch(typeof(ZNetScene), "RemoveObjects")] public static class PPRRemoveObjectsDiagnosticsPatch { private const int DetailedLogLimit = 5; private const int SampledLogInterval = 100; private static int _exceptionCount = 0; private static readonly HashSet<string> BossaddKnownVfx = new HashSet<string>(StringComparer.Ordinal) { "lightningAOE", "vfx_GodExplosion", "fx_eikthyr_stomp", "vfx_spawn_small" }; [HarmonyFinalizer] public static Exception Finalizer(Exception __exception) { if (__exception == null) { return null; } int num = ++_exceptionCount; if (num <= 5) { string arg = LogGhostEntries(num); Plugin.Log.LogError((object)$"[RO-DIAG] RemoveObjects 예외 #{num}:\n{__exception}"); PPRFreezeReporter.Append("RemoveObjects 예외", $"발생 횟수: #{num}\n\n예외:\n{__exception}\n\n유령 ZNetView 스캔 결과:\n{arg}"); } else if (num % 100 == 0) { Plugin.Log.LogError((object)$"[RO-DIAG] RemoveObjects 예외 누적 {num}회 ({100}회마다 샘플 로그)"); } return __exception; } private static string LogGhostEntries(int occurrence) { try { ZNetScene instance = ZNetScene.instance; if ((Object)(object)instance == (Object)null) { return "(ZNetScene.instance == null)"; } S