Project_WL/Assets/WL/Combat/Auto/ActiveSkillLock.cs

171 lines
8.4 KiB
C#

// ─────────────────────────────────────────────────────────────────────────────
// ActiveSkillLock.cs — 액티브 스킬 사용 임시 OFF (WL-816u ③ · #816)
//
// PD 지시 원문(2026-09-15) 「우선 엑티브 스킬은 사용 Off 해줘.」
//
// ■ 무엇을 끄나 / 무엇을 남기나
// 끈다 : 자동전투 AI 의 스킬 시전 · 전투 패드 스킬 버튼 4칸
// 남긴다: 기본 공격 · 이동 · 물약 · 회피 · 자동전투 토글 자체 · 몹/보스 스킬
//
// ■ 왜 이 지점인가 (코드 실측 · 원본 0줄)
// `PCActor.Update()`(PCActor.cs:79~101) 의 자동 스킬 루프는 매 프레임
// `if (DSUtil.CheckNull(arr_magicData[i]) || arr_magicData[i].e_SkillType == eSkillType.Passive) continue;`
// 로 시작한다. 즉 **`arr_magicData[i]` 가 null 이면 그 슬롯은 쿨다운도 안 돌고 `Use_Skill` 도 안 불린다.**
// `arr_magicData` 는 `ReSet()`(PCActor.cs:189)에서
// `table_skilllist.Ins.Get_Data_orNull(m_ServerData.Get_EquipSkillID(i))`
// 로 다시 채워지므로, **매 프레임 비워 두면** 언제 ReSet 이 돌아도 다음 프레임에 다시 잠긴다.
// · `arr_magicData` 는 PCActor 의 private 필드다 → 원본을 고치지 않으려면 리플렉션뿐이다
// (FieldInfo 는 1회만 찾아 캐시 · 프레임당 비용 = 배열 6칸 쓰기).
// · `ServerData.Equip` 은 **건드리지 않는다** — 장착 데이터를 지우면 스킬 UI·미션·서버 프리셋까지
// 영향이 가고, 되돌릴 때 원래 ID 를 복원해야 한다. 여기서는 **이번 PC 인스턴스의 런타임 캐시만** 비운다.
//
// ■ 되돌리기(C8) — `WLCombatCoreSettings.activeSkillsEnabled = true` **값 하나**.
// 그러면 이 러너가 아예 뜨지 않고(오브젝트 0), 다음 `ReSet()` 이 원래 스킬을 그대로 다시 읽는다.
//
// 🔴 Debug.LogError / Debug.LogException 을 쓰지 않는다(ErrorLogHookManager 가 플레이어 팝업을 띄운다).
// 🔴 `Assets/Script/**` · `Assets/FarmingIsland/**` 는 이 파일 때문에 한 줄도 바뀌지 않는다.
// 🔴 어셈블리 주의: Assets/WL/Combat/ 에 .asmdef 를 만들지 말 것(PCActor 가 Assembly-CSharp 에 있다).
// ─────────────────────────────────────────────────────────────────────────────
using System.Reflection;
using UnityEngine;
using WL.Combat.Core;
namespace WL.Combat.Auto
{
/// <summary>액티브 스킬 사용 잠금. 정적 · 새 Manager/Singleton 0(런너는 숨김 GameObject 1개).</summary>
public static class ActiveSkillLock
{
static WLCombatCoreSettings Cfg { get { return WLCombatCoreSettings.Instance; } }
/// <summary>잠겨 있는가(= 액티브 스킬 OFF). 에셋이 없으면 false = 원본 동작 100 %.</summary>
public static bool Locked
{
get { var c = Cfg; return c != null && !c.activeSkillsEnabled && !RuntimeDisabled; }
}
/// <summary>진단·A/B 전용 런타임 스위치. Play 종료 시 도메인 리로드로 자동 false.</summary>
public static bool RuntimeDisabled;
// ── 진단 카운터 (프로브·보고가 읽는다 · 실측만)
public static int Ticks, Cleared, PcSwaps;
public static string LastLog = "";
/// <summary>마지막으로 비우기 직전에 본 스킬 ID 들(= 잠그지 않았다면 시전됐을 스킬).</summary>
public static int[] LastSeenSkillIds = new int[0];
static FieldInfo s_magicField;
static Object s_lastPc;
static float s_nextPoll;
static bool s_faulted;
/// <summary>PCActor 의 private `arr_magicData` (SkillListTableData[6]). 1회만 찾는다.</summary>
static FieldInfo MagicField
{
get
{
if (s_magicField == null)
s_magicField = typeof(PCActor).GetField("arr_magicData",
BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
return s_magicField;
}
}
// ─────────────────────────────────────────────────────────────────────
/// <summary>런너가 매 프레임 부른다(프로브도 직접 부를 수 있다). 비운 슬롯 수를 돌려준다.</summary>
public static int Tick()
{
if (s_faulted || !Locked) return 0;
var cfg = Cfg;
float poll = cfg != null ? cfg.activeSkillLockPollSeconds : 0f;
if (poll > 0f)
{
float now = Time.unscaledTime;
if (now < s_nextPoll) return 0;
s_nextPoll = now + poll;
}
Ticks++;
var pc = MyValue.MyPC;
if (DSUtil.CheckNull(pc)) { s_lastPc = null; return 0; }
if (!ReferenceEquals(pc, s_lastPc)) { s_lastPc = pc; PcSwaps++; }
// 🔴 Debug.LogException 을 쓰지 않는다 — ErrorLogHookManager 가 플레이어 팝업으로 띄운다.
try { return ClearOn(pc); }
catch (System.Exception ex) { s_faulted = true; Log("중단(예외) — " + ex.Message); return 0; }
}
/// <summary>한 PC 의 자동 스킬 캐시를 비운다(이미 비어 있으면 아무 것도 쓰지 않는다 = GC 0).</summary>
public static int ClearOn(PCActor pc)
{
var f = MagicField;
if (f == null || DSUtil.CheckNull(pc)) return 0;
var arr = f.GetValue(pc) as SkillListTableData[];
if (arr == null) return 0;
int n = 0;
for (int i = 0; i < arr.Length; i++)
{
if (arr[i] == null) continue;
if (LastSeenSkillIds.Length != arr.Length) LastSeenSkillIds = new int[arr.Length];
LastSeenSkillIds[i] = arr[i].n_SkillID;
arr[i] = null;
n++;
}
if (n > 0)
{
Cleared += n;
Log("액티브 스킬 잠금 — 슬롯 " + n + "칸 비움(누적 " + Cleared + ")");
}
return n;
}
static void Log(string msg)
{
LastLog = msg;
var c = Cfg;
if (c != null && c.verboseLog) Debug.Log("[ActiveSkillLock] " + msg);
}
/// <summary>프로브용 — 카운터 초기화.</summary>
public static void ResetDiagnostics()
{
Ticks = Cleared = PcSwaps = 0; LastLog = "";
s_lastPc = null; s_nextPoll = 0f; s_faulted = false;
}
public static string Dump()
{
var c = Cfg;
return "[ActiveSkillLock] 잠금=" + Locked +
" (설정 activeSkillsEnabled=" + (c != null ? c.activeSkillsEnabled.ToString() : "에셋없음") +
" · RuntimeDisabled=" + RuntimeDisabled + ")" +
" · 틱 " + Ticks + " · 비운 슬롯 " + Cleared + " · PC 교체 " + PcSwaps +
" · 필드 " + (MagicField != null) + " · " + LastLog;
}
// ── 런너 (숨김 GameObject 1개 · 플레이 모드 전용) ────────────────────
static ActiveSkillLockRunner s_runner;
internal static void EnsureRunner()
{
if (!Application.isPlaying || !DSUtil.CheckNull(s_runner)) return;
var go = new GameObject("[WL816u] ActiveSkillLockRunner");
go.hideFlags = HideFlags.HideAndDontSave;
s_runner = go.AddComponent<ActiveSkillLockRunner>();
}
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)]
static void Boot()
{
if (Locked) EnsureRunner(); // 꺼져 있으면(= 스킬 사용 ON) 오브젝트 0 (C8)
}
}
/// <summary>ActiveSkillLock 의 시간 축. 숨김 GameObject 1개 · 코루틴 0.</summary>
internal sealed class ActiveSkillLockRunner : MonoBehaviour
{
void Update() { ActiveSkillLock.Tick(); }
}
}