218 lines
12 KiB
C#
218 lines
12 KiB
C#
// ─────────────────────────────────────────────────────────────────────────────
|
||
// AutoCombatOnEnter.cs — 맵 진입 시 자동전투 기본 ON (Q3 D-2)
|
||
//
|
||
// PD 지시 #813 · 발주서 WL-813x §1-2 (2026-09-09) · 근거 = qa/W2-3_바퀴2.md D-2
|
||
//
|
||
// ■ 관측된 결함 (QA 실측)
|
||
// 신규 계정 기본값이 `eCommon.Auto = 0` 이라 인게임에 들어가도 자동전투가 꺼져 있다.
|
||
// 존1 60 s 방치 = **0킬 · skillCast 0**. `Actor.Set_Auto()` 1회 뒤 같은 조건 60 s = **23킬**.
|
||
// (W2-1 의 「스킬 0발」 미확인 항목이 여기서 해소됐다.) 이 값이 0인 채로 PD 가 Play 하면
|
||
// "스킬도 안 나가고 적도 안 죽는 게임"을 보게 된다.
|
||
//
|
||
// ■ 원인 (코드 실측)
|
||
// PCActor.cs:185 `m_Auto = m_ServerData.Get_Common(eCommon.Auto) == 1;` — ReSet() 마다 서버 값에서 다시 읽는다.
|
||
// PCActor.cs:93 의 자동 스킬 루프가 `m_Auto` 로 게이트된다 → false 면 스킬을 한 발도 쏘지 않는다.
|
||
// Actor.cs:2260 `Set_Auto()` 는 m_Auto 를 토글하고 같은 값을 eCommon.Auto 에 넣는다(둘은 항상 동기).
|
||
//
|
||
// ■ 이 파일이 하는 것 — 원본 훅 0 · 폴링만(Survival.PollRunBoundary 선례)
|
||
// 런(맵) 진입 경계를 관측해서 그 시점에 자동전투가 꺼져 있으면 `MyValue.MyPC.Set_Auto()` 를 **1회** 부른다.
|
||
// · 경계 = 메인 PC 교체(PCActor.ReSet 이 MyValue.MyPC 를 갈아 끼운다) 또는 맵 ID 변경.
|
||
// · 진입 뒤 플레이어가 직접 끈 것은 다시 켜지 않는다(경계당 1회이므로).
|
||
// · 로비는 기본 제외(적이 없어 자동 타겟팅이 의미가 없다) — autoCombatInLobby 로 켤 수 있다.
|
||
// · Set_Auto() 는 m_Auto 와 eCommon.Auto 를 함께 세우므로, 뒤늦게 도는 PCActor.ReSet() 이
|
||
// 다시 읽어도 ON 이 유지된다.
|
||
//
|
||
// ■ 계정 영구 변경 아님
|
||
// ServerClass.Set_Common(ServerClass.cs:1469) 은 로컬 캐시 배열만 쓴다(서버 저장 호출 없음).
|
||
// QA 도 "슬롯 2에서 켠 값이 슬롯 3 로그인에 안 남았다 = 세션 간 비영속"을 실측했다.
|
||
// → 이 핫픽스는 매 진입마다 다시 켜 주는 방식이지 계정 기본값을 바꾸는 것이 아니다.
|
||
// 계정 기본값(신규 계정 Auto=1)은 서버/테이블 소관 = S 트랙 · PD 결정.
|
||
//
|
||
// ■ 전투 패드 아이콘
|
||
// BattleUI.Set_Auto()(private)가 `Get_Common(eCommon.Auto)` 로 스프라이트를 고른다.
|
||
// UI 가 먼저 떴다면 아이콘이 OFF 로 굳으므로, 적용 뒤 잠시 동안 public 필드
|
||
// `BattleUI.images[0]` 의 스프라이트만 맞춘다(UI 파일 수정 0 · 813c 패드 배치와 무관).
|
||
//
|
||
// ■ 값(C45) 전부 WLCombatCoreSettings. autoCombatOnEnter = 0 이면 러너 자체가 뜨지 않는다(C8 롤백).
|
||
// 🔴 코어 스위치(WLCombatCoreSettings.Enabled)에는 의존하지 않는다 — 코어 A/B 가 킬 수를 0으로 만들면 안 된다.
|
||
//
|
||
// 🔴 어셈블리 주의: Assets/WL/Combat/ 에 .asmdef 를 만들지 말 것(Actor/PCActor 가 Assembly-CSharp 에 있다).
|
||
// ─────────────────────────────────────────────────────────────────────────────
|
||
|
||
using UnityEngine;
|
||
using UnityEngine.UI;
|
||
using WL.Combat.Core;
|
||
|
||
namespace WL.Combat.Auto
|
||
{
|
||
/// <summary>맵 진입 시 자동전투 1회 ON. 정적 · 새 Manager/Singleton 0(런너는 숨김 GameObject 1개).</summary>
|
||
public static class AutoCombatOnEnter
|
||
{
|
||
static WLCombatCoreSettings Cfg { get { return WLCombatCoreSettings.Instance; } }
|
||
|
||
/// <summary>기능이 살아 있는가. 에셋이 없거나 플래그가 꺼져 있으면 false = 원본 동작 100%.</summary>
|
||
public static bool Active
|
||
{
|
||
get { var c = Cfg; return c != null && c.autoCombatOnEnter && !RuntimeDisabled; }
|
||
}
|
||
|
||
/// <summary>진단·A/B 전용 런타임 스위치. Play 종료 시 도메인 리로드로 자동 false.</summary>
|
||
public static bool RuntimeDisabled;
|
||
|
||
// ───────────────────────────────────────── 진단 카운터 (프로브가 읽는다)
|
||
public static int Boundaries, Applied, AlreadyOn, SkippedLobby, IconFixed;
|
||
public static string LastReason = "", LastLog = "";
|
||
|
||
/// <summary>현재 자동전투 값(서버 데이터 캐시). 프로브가 「진입 후 true」를 확인하는 지점.</summary>
|
||
public static bool IsAutoOn
|
||
{
|
||
get
|
||
{
|
||
// 🔴 ServerInfo.Ins 는 없으면 GameObject 를 만든다(ServerInfo.cs:19) — 에디트 모드를 오염시키지 않는다.
|
||
if (!Application.isPlaying) return false;
|
||
var si = ServerInfo.Ins;
|
||
if (DSUtil.CheckNull(si) || si.m_ServerData == null) return false;
|
||
return si.m_ServerData.Get_Common(eCommon.Auto) == 1;
|
||
}
|
||
}
|
||
|
||
// ───────────────────────────────────────── 내부 상태
|
||
static PCActor s_lastPc;
|
||
static int s_lastMapId = int.MinValue;
|
||
static float s_nextPoll, s_iconDeadline;
|
||
static bool s_faulted;
|
||
|
||
// ─────────────────────────────────────────────────────────────────────
|
||
// 틱
|
||
// ─────────────────────────────────────────────────────────────────────
|
||
|
||
/// <summary>런너가 매 프레임 부른다(에디트 모드에서는 프로브가 직접 부를 수 있다).</summary>
|
||
internal static void Tick()
|
||
{
|
||
if (s_faulted || !Active) return;
|
||
var cfg = Cfg;
|
||
float now = Time.unscaledTime;
|
||
if (now < s_nextPoll) return;
|
||
s_nextPoll = now + Mathf.Max(0.02f, cfg.autoCombatPollSeconds);
|
||
|
||
// 🔴 Debug.LogException 을 쓰지 않는다 — ErrorLogHookManager 가 그것을 플레이어 팝업으로 띄운다(같은 커밋 §D-1).
|
||
try
|
||
{
|
||
PollBoundary(cfg);
|
||
if (now < s_iconDeadline) SyncPadIcon();
|
||
}
|
||
catch (System.Exception ex) { s_faulted = true; Log("중단(예외) — " + ex.Message); }
|
||
}
|
||
|
||
static void PollBoundary(WLCombatCoreSettings cfg)
|
||
{
|
||
var pc = MyValue.MyPC; // 파괴된 참조는 Unity != 로 걸러진다
|
||
if (DSUtil.CheckNull(pc)) { s_lastPc = null; return; } // PC 없음(타이틀·로딩) — 다음 등장을 경계로 본다
|
||
|
||
// InGameInfo 가 아직 없으면 경계를 「소비」하지 않는다 — 다음 폴링에서 같은 경계를 다시 본다.
|
||
var info = InGameInfo.Ins;
|
||
if (DSUtil.CheckNull(info)) return;
|
||
|
||
int map = MyValue.MyChoiceMapData != null ? MyValue.MyChoiceMapData.n_MapID : int.MinValue;
|
||
bool pcChanged = !ReferenceEquals(pc, s_lastPc);
|
||
bool mapChanged = map != s_lastMapId;
|
||
if (!pcChanged && !mapChanged) return;
|
||
|
||
s_lastPc = pc; s_lastMapId = map;
|
||
Boundaries++;
|
||
LastReason = pcChanged ? "pc" : ("map " + map);
|
||
|
||
// WL 인게임 맵(WL_Nature)은 BattleMapConfig n_MapID −1 · e_MapType = Stage 로 실측 — 로비가 아니다.
|
||
if (!cfg.autoCombatInLobby && info.Get_GameMode() == eGameMode.Lobby)
|
||
{
|
||
SkippedLobby++;
|
||
Log("로비 — 건너뜀(" + LastReason + ")");
|
||
return;
|
||
}
|
||
|
||
if (IsAutoOn) { AlreadyOn++; Log("이미 ON — 무동작(" + LastReason + ")"); ArmIcon(cfg); return; }
|
||
|
||
pc.Set_Auto(); // m_Auto = true + eCommon.Auto = 1 (Actor.cs:2260)
|
||
Applied++;
|
||
Log("자동전투 ON 적용 #" + Applied + " (" + LastReason + ") → auto=" + (IsAutoOn ? "1" : "0"));
|
||
ArmIcon(cfg);
|
||
}
|
||
|
||
static void ArmIcon(WLCombatCoreSettings cfg)
|
||
{
|
||
if (cfg.autoCombatIconRetrySeconds > 0f)
|
||
s_iconDeadline = Time.unscaledTime + cfg.autoCombatIconRetrySeconds;
|
||
}
|
||
|
||
/// <summary>전투 패드 자동 버튼 아이콘을 현재 값에 맞춘다. UI 파일 수정 0 — public 필드만 읽고 쓴다.</summary>
|
||
static void SyncPadIcon()
|
||
{
|
||
var atlas = UIAtlasMgr.Ins;
|
||
if (DSUtil.CheckNull(atlas)) return;
|
||
var want = atlas.Get_Sprite(IsAutoOn ? "autotarget_on" : "autotarget_off");
|
||
if (want == null) return;
|
||
|
||
var uis = Object.FindObjectsByType<BattleUI>(FindObjectsInactive.Include, FindObjectsSortMode.None);
|
||
for (int i = 0; i < uis.Length; i++)
|
||
{
|
||
var imgs = uis[i].images;
|
||
if (imgs == null || imgs.Length == 0) continue;
|
||
Image img = imgs[0];
|
||
if (img == null || img.sprite == want) continue;
|
||
img.sprite = want;
|
||
IconFixed++;
|
||
s_iconDeadline = 0f; // 한 번 맞췄으면 폴링을 멈춘다
|
||
}
|
||
}
|
||
|
||
static void Log(string msg)
|
||
{
|
||
LastLog = msg;
|
||
var c = Cfg;
|
||
if (c != null && c.verboseLog) Debug.Log("[AutoCombatOnEnter] " + msg);
|
||
}
|
||
|
||
// ─────────────────────────────────────────────────────────────────────
|
||
// 진단 · 초기화
|
||
// ─────────────────────────────────────────────────────────────────────
|
||
|
||
/// <summary>프로브용: 카운터·경계 상태 초기화.</summary>
|
||
public static void ResetDiagnostics()
|
||
{
|
||
Boundaries = Applied = AlreadyOn = SkippedLobby = IconFixed = 0;
|
||
LastReason = LastLog = "";
|
||
s_lastPc = null; s_lastMapId = int.MinValue;
|
||
s_nextPoll = s_iconDeadline = 0f; s_faulted = false;
|
||
}
|
||
|
||
/// <summary>프로브용: 러너 없이 1회 폴링(강제 틱).</summary>
|
||
public static void ForceTick() { s_nextPoll = 0f; Tick(); }
|
||
|
||
// ─────────────────────────────────────────────────────────────────────
|
||
// 런너 (숨김 GameObject 1개 · 플레이 모드 전용)
|
||
// ─────────────────────────────────────────────────────────────────────
|
||
|
||
static AutoCombatOnEnterRunner s_runner;
|
||
|
||
internal static void EnsureRunner()
|
||
{
|
||
if (!Application.isPlaying || !DSUtil.CheckNull(s_runner)) return;
|
||
var go = new GameObject("[WL813x] AutoCombatOnEnterRunner");
|
||
go.hideFlags = HideFlags.HideAndDontSave;
|
||
s_runner = go.AddComponent<AutoCombatOnEnterRunner>();
|
||
}
|
||
|
||
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)]
|
||
static void Boot()
|
||
{
|
||
if (Active) EnsureRunner(); // 꺼져 있으면 오브젝트 0(C8)
|
||
}
|
||
}
|
||
|
||
/// <summary>AutoCombatOnEnter 의 시간 축. 숨김 GameObject 1개 · 코루틴 0.</summary>
|
||
internal sealed class AutoCombatOnEnterRunner : MonoBehaviour
|
||
{
|
||
void Update() { AutoCombatOnEnter.Tick(); }
|
||
}
|
||
}
|