[WL-813o2] 선택스킬 풀 검증·제외 · 결과창 모달 차단 연장 (#813)
D-41: table_selectskill 58행 전수 실측 — 참조 프리팹/이펙트 키가 없는 3행
(3 Select_Shield · 11 Select_SwordSlash · 14 Select_Star)을 뽑기에서 제외.
원본 Get_DataList(3) 은 그대로 쓰고 결과만 후필터(원본 수정 0줄) · 값 전부 SO.
D-42: WLErrorGuardSettings.suppressUntilNextRunStart(기본 1) — RunPhase.Ended
(결과창~다음 런 시작)까지 훅을 내린 채 둔다. 813x2 파일 +7/-1줄.
This commit is contained in:
parent
fc9a265bd7
commit
5edb54b2f9
|
|
@ -0,0 +1,195 @@
|
||||||
|
# WL813o2 Probe 2026-09-09 19:01:19 (edit mode · Play 0 · 로그인 0)
|
||||||
|
playMode=False
|
||||||
|
|
||||||
|
── ⓪ 값(SO) — 코드 상수 0 (C45)
|
||||||
|
[PASS] WLZonePickupSettings.asset 로드
|
||||||
|
[PASS] WLErrorGuardSettings.asset 로드
|
||||||
|
validatePool=True runtimeKeyCheck=True maxRedraws=6 logExclusion=True cardCount=3
|
||||||
|
excludeSkillIds = [3, 11, 14]
|
||||||
|
noPrefabEffects(10) = Select_DoubleShot, Select_TripleShot, Select_Bounce, Select_Bounce2, Select_LeafFairy2, Select_FrostFairy2, Select_WindFairy2, Select_HikikomoriFairy2, Select_Heal, Select_Shield
|
||||||
|
extraKeys[0] Select_Star → Assets/Res_Addr/Projectile/ProjectileStar.prefab (Select_Star.cs:27 Shoot_Projectile("ProjectileStar"))
|
||||||
|
extraKeys[1] Select_SwordSlash → Assets/Res_Addr/Projectile/SwordSlash.prefab (Select_SwordSlash.cs:29 Shoot_Projectile("SwordSlash"))
|
||||||
|
extraKeys[2] Select_Shield → Assets/Res_Addr/Skill/Skill_Shield.prefab (InGameInfo2.cs:46 Set_Skill_ToPet(eEffect.Skill_Shield))
|
||||||
|
경로형식: select=Assets/Res_Addr/SelectSkill/{0}.prefab · skill=Assets/Res_Addr/Skill/{0}.prefab · effect=Assets/Res_Addr/Effect/{0}.prefab
|
||||||
|
[813x2] suppressErrorModalInRun=True suppressOnlyWhileRunning=True suppressUntilNextRunStart=True
|
||||||
|
[PASS] validatePool=1(풀 검증 켜짐)
|
||||||
|
[PASS] excludeSkillIds 비어 있지 않음
|
||||||
|
[PASS] extraKeys(코드 문자열 키) 채워짐
|
||||||
|
[PASS] suppressUntilNextRunStart=1(D-42 연장 켜짐)
|
||||||
|
[PASS] 카드 3장(기획안 F-3)
|
||||||
|
|
||||||
|
── ① Addressables 그룹 실측 — 키 존재 ⟺ 그 경로의 에셋 존재
|
||||||
|
Projectile 그룹 m_Address = Assets/Res_Addr/Projectile (엔트리 1개)
|
||||||
|
[PASS] Projectile = 폴더 엔트리 1개(자식 주소 = 에셋 경로)
|
||||||
|
Skill 그룹 m_Address = Assets/Res_Addr/Skill (엔트리 1개)
|
||||||
|
[PASS] Skill = 폴더 엔트리 1개(자식 주소 = 에셋 경로)
|
||||||
|
Effect 그룹 m_Address = Assets/Res_Addr/Effect (엔트리 1개)
|
||||||
|
[PASS] Effect = 폴더 엔트리 1개(자식 주소 = 에셋 경로)
|
||||||
|
SelectSkill 그룹 m_Address = Assets/Res_Addr/SelectSkill (엔트리 1개)
|
||||||
|
[PASS] SelectSkill = 폴더 엔트리 1개(자식 주소 = 에셋 경로)
|
||||||
|
|
||||||
|
── ② 원본 데이터 경로 — table_selectskill.Awake 를 그대로 태운다
|
||||||
|
[PASS] 테이블 TextAsset Assets/ResWork/Table/table_selectskill.txt
|
||||||
|
[PASS] table_selectskill.Ins 설정(원본 Awake)
|
||||||
|
[PASS] InGameInfo.Ins · ActorInfo.Ins 설정
|
||||||
|
테이블 행 수 실측 = 58 (발주서 「기획안 42행」과 다르다 → 실측값을 쓴다)
|
||||||
|
[PASS] 행 파싱 성공
|
||||||
|
|
||||||
|
── ③ 원본 효과 클래스 grep — `Shoot_Projectile("…")` 코드 문자열 (읽기만 · 수정 0)
|
||||||
|
[PASS] 원본 폴더 Assets/Script/Character/Projectile/SelectSkill
|
||||||
|
파일 16개
|
||||||
|
(주석) Select_LeafFairy.cs:44 키="LeafBullet"
|
||||||
|
Select_Star.cs:27 키="ProjectileStar"
|
||||||
|
Select_SwordSlash.cs:29 키="SwordSlash"
|
||||||
|
[PASS] 살아 있는 Shoot_Projectile 문자열 2건
|
||||||
|
[PASS] SO extraKeys 가 Select_Star → Assets/Res_Addr/Projectile/ProjectileStar.prefab 를 덮는다
|
||||||
|
[PASS] SO extraKeys 가 Select_SwordSlash → Assets/Res_Addr/Projectile/SwordSlash.prefab 를 덮는다
|
||||||
|
|
||||||
|
── ④ 🔴 풀 검증 표 — 전 행 × 참조 키 × 실존 (에디트 모드 · AssetDatabase)
|
||||||
|
|
||||||
|
| ID | SkillEffect | Stg | 뽑기가능 | 1차 키 | 실존 | 추가 키 | 실존 | 판정 |
|
||||||
|
|---|---|---|---|---|---|---|---|---|
|
||||||
|
| 1 | Select_DoubleShot | 2 | - | (없음) | - | - | - | 사용 |
|
||||||
|
| 2 | Select_Bounce | 4 | - | (없음) | - | - | - | 사용 |
|
||||||
|
| 3 | Select_Shield | 1 | O | (없음) | - | Skill/Skill_Shield.prefab | 🔴X | 🔴 제외 |
|
||||||
|
| 4 | Select_FireCircle | 1 | O | SelectSkill/Select_FireCircle.prefab | O | - | - | 사용 |
|
||||||
|
| 5 | Select_BarrelCircle | 1 | O | SelectSkill/Select_BarrelCircle.prefab | O | - | - | 사용 |
|
||||||
|
| 6 | Select_TripleShot | 5 | - | (없음) | - | - | - | 사용 |
|
||||||
|
| 7 | Select_Heal | 5 | - | (없음) | - | - | - | 사용 |
|
||||||
|
| 8 | Select_Bottle | 3 | - | SelectSkill/Select_Bottle.prefab | O | - | - | 사용 |
|
||||||
|
| 9 | Select_BodyonFire | 1 | O | SelectSkill/Select_BodyonFire.prefab | O | - | - | 사용 |
|
||||||
|
| 10 | Select_LeafFairy | 1 | O | SelectSkill/Select_LeafFairy.prefab | O | - | - | 사용 |
|
||||||
|
| 11 | Select_SwordSlash | 7 | - | SelectSkill/Select_SwordSlash.prefab | O | Projectile/SwordSlash.prefab | 🔴X | 🔴 제외 |
|
||||||
|
| 12 | Select_FrostFairy | 1 | O | SelectSkill/Select_FrostFairy.prefab | O | - | - | 사용 |
|
||||||
|
| 13 | Select_WindFairy | 1 | O | SelectSkill/Select_WindFairy.prefab | O | - | - | 사용 |
|
||||||
|
| 14 | Select_Star | 1 | O | SelectSkill/Select_Star.prefab | O | Projectile/ProjectileStar.prefab | 🔴X | 🔴 제외 |
|
||||||
|
| 15 | Select_MoveSpeedUp | 1 | O | SelectSkill/Select_MoveSpeedUp.prefab | O | - | - | 사용 |
|
||||||
|
| 16 | Select_MoveSpeedUp | 4 | - | SelectSkill/Select_MoveSpeedUp.prefab | O | - | - | 사용 |
|
||||||
|
| 17 | Select_HikikomoriFairy | 1 | O | SelectSkill/Select_HikikomoriFairy.prefab | O | - | - | 사용 |
|
||||||
|
| 18 | Select_LeafFairy2 | 4 | - | (없음) | - | - | - | 사용 |
|
||||||
|
| 19 | Select_FrostFairy2 | 3 | - | (없음) | - | - | - | 사용 |
|
||||||
|
| 20 | Select_WindFairy2 | 3 | - | (없음) | - | - | - | 사용 |
|
||||||
|
| 21 | Select_HikikomoriFairy2 | 2 | - | (없음) | - | - | - | 사용 |
|
||||||
|
| 22 | Select_Bounce2 | 5 | - | (없음) | - | - | - | 사용 |
|
||||||
|
| 101 | None | 1 | O | (없음) | - | - | - | 사용 |
|
||||||
|
| 102 | None | 1 | O | (없음) | - | - | - | 사용 |
|
||||||
|
| 103 | None | 1 | O | (없음) | - | - | - | 사용 |
|
||||||
|
| 104 | None | 1 | O | (없음) | - | - | - | 사용 |
|
||||||
|
| 105 | None | 1 | O | (없음) | - | - | - | 사용 |
|
||||||
|
| 106 | None | 1 | O | (없음) | - | - | - | 사용 |
|
||||||
|
| 107 | None | 1 | O | (없음) | - | - | - | 사용 |
|
||||||
|
| 108 | None | 1 | O | (없음) | - | - | - | 사용 |
|
||||||
|
| 109 | None | 1 | O | (없음) | - | - | - | 사용 |
|
||||||
|
| 110 | None | 1 | O | (없음) | - | - | - | 사용 |
|
||||||
|
| 111 | None | 1 | O | (없음) | - | - | - | 사용 |
|
||||||
|
| 112 | None | 1 | O | (없음) | - | - | - | 사용 |
|
||||||
|
| 113 | None | 3 | - | (없음) | - | - | - | 사용 |
|
||||||
|
| 114 | None | 3 | - | (없음) | - | - | - | 사용 |
|
||||||
|
| 115 | None | 3 | - | (없음) | - | - | - | 사용 |
|
||||||
|
| 116 | None | 3 | - | (없음) | - | - | - | 사용 |
|
||||||
|
| 117 | None | 3 | - | (없음) | - | - | - | 사용 |
|
||||||
|
| 118 | None | 3 | - | (없음) | - | - | - | 사용 |
|
||||||
|
| 119 | None | 3 | - | (없음) | - | - | - | 사용 |
|
||||||
|
| 120 | None | 3 | - | (없음) | - | - | - | 사용 |
|
||||||
|
| 121 | None | 3 | - | (없음) | - | - | - | 사용 |
|
||||||
|
| 122 | None | 3 | - | (없음) | - | - | - | 사용 |
|
||||||
|
| 123 | None | 3 | - | (없음) | - | - | - | 사용 |
|
||||||
|
| 124 | None | 3 | - | (없음) | - | - | - | 사용 |
|
||||||
|
| 125 | None | 6 | - | (없음) | - | - | - | 사용 |
|
||||||
|
| 126 | None | 6 | - | (없음) | - | - | - | 사용 |
|
||||||
|
| 127 | None | 6 | - | (없음) | - | - | - | 사용 |
|
||||||
|
| 128 | None | 6 | - | (없음) | - | - | - | 사용 |
|
||||||
|
| 129 | None | 6 | - | (없음) | - | - | - | 사용 |
|
||||||
|
| 130 | None | 6 | - | (없음) | - | - | - | 사용 |
|
||||||
|
| 131 | None | 6 | - | (없음) | - | - | - | 사용 |
|
||||||
|
| 132 | None | 6 | - | (없음) | - | - | - | 사용 |
|
||||||
|
| 133 | None | 6 | - | (없음) | - | - | - | 사용 |
|
||||||
|
| 134 | None | 6 | - | (없음) | - | - | - | 사용 |
|
||||||
|
| 135 | None | 6 | - | (없음) | - | - | - | 사용 |
|
||||||
|
| 136 | None | 6 | - | (없음) | - | - | - | 사용 |
|
||||||
|
|
||||||
|
전체 58행 · 부재 키 행 3건
|
||||||
|
🔴 행 3 Select_Shield (Stage 1 · 지금 뽑힘) — 부재 키 Assets/Res_Addr/Skill/Skill_Shield.prefab
|
||||||
|
🔴 행 11 Select_SwordSlash (Stage 7 · 지금은 안 뽑힘) — 부재 키 Assets/Res_Addr/Projectile/SwordSlash.prefab
|
||||||
|
🔴 행 14 Select_Star (Stage 1 · 지금 뽑힘) — 부재 키 Assets/Res_Addr/Projectile/ProjectileStar.prefab
|
||||||
|
[PASS] 부재 키 행 3(Select_Shield)이 SO excludeSkillIds 에 있다
|
||||||
|
[PASS] 부재 키 행 11(Select_SwordSlash)이 SO excludeSkillIds 에 있다
|
||||||
|
[PASS] 부재 키 행 14(Select_Star)이 SO excludeSkillIds 에 있다
|
||||||
|
[PASS] SO excludeSkillIds[3] 는 실측 부재 행이다(과잉 제외 0)
|
||||||
|
[PASS] SO excludeSkillIds[11] 는 실측 부재 행이다(과잉 제외 0)
|
||||||
|
[PASS] SO excludeSkillIds[14] 는 실측 부재 행이다(과잉 제외 0)
|
||||||
|
[PASS] D-41 재현 — 부재 키 행이 실제로 있다
|
||||||
|
|
||||||
|
── ⑤ 런타임 검사 — 게임 코드 `ZonePickupPool` 이 같은 결론을 내는가
|
||||||
|
[PASS] 에디트 모드 표와 런타임 판정 완전 일치(58행 · 불일치 0)
|
||||||
|
[PASS] 행 3 제외 판정 — 사유 "SO 제외 목록"
|
||||||
|
[PASS] 행 11 제외 판정 — 사유 "SO 제외 목록"
|
||||||
|
[PASS] 행 14 제외 판정 — 사유 "SO 제외 목록"
|
||||||
|
카운터: checked=3 staticExclude=3 missingKey=0 skipped=0
|
||||||
|
정적 목록 없이 키 검사만으로 제외된 행 = [3, 11, 14]
|
||||||
|
[PASS] 키 검사만으로도 같은 수(3)가 걸린다 = 목록은 캐시일 뿐
|
||||||
|
|
||||||
|
── ⑥ 3장 뽑기 100회 — 제외 행이 한 번도 안 나오는가
|
||||||
|
[PASS] 100회 전부 성공 — 실측 100
|
||||||
|
[PASS] 카드 수 3장 미달 0 — 실측 0
|
||||||
|
[PASS] 한 뽑기 안 ID 중복 0 — 실측 0
|
||||||
|
나온 ID 분포(300장): 4×12 5×12 9×18 10×15 12×22 13×12 15×19 17×13 101×11 102×15 103×7 104×16 105×18 106×15 107×15 108×21 109×6 110×18 111×16 112×19
|
||||||
|
[PASS] 🔴 제외 행 3 등장 0회 — 실측 0회
|
||||||
|
[PASS] 🔴 제외 행 11 등장 0회 — 실측 0회
|
||||||
|
[PASS] 🔴 제외 행 14 등장 0회 — 실측 0회
|
||||||
|
ExcludedCount=32 RedrawCount=27 last="행 14 — SO 제외 목록"
|
||||||
|
[PASS] 실제로 제외가 걸렸다(후필터가 살아 있다) — 32건
|
||||||
|
|
||||||
|
── ⑦ 대조군 — validatePool=0 (813o 동작) 이면 제외 행이 다시 뽑힌다
|
||||||
|
validatePool=0 에서 제외 대상 등장 = 27장 / 300장
|
||||||
|
[PASS] 대조군에서는 실제로 나온다 = ⑥의 0회가 필터 덕분임을 증명
|
||||||
|
[PASS] validatePool=0 이면 제외 0 · 재뽑기 0(813o 동작 그대로)
|
||||||
|
[PASS] 재뽑기 0회
|
||||||
|
|
||||||
|
── ⑧ GC — 검증 코드 자체의 할당(캐시가 선 뒤)
|
||||||
|
판정 6,000회(캐시 히트) Δ = 0 B
|
||||||
|
[PASS] 🔴 캐시가 선 뒤 판정 할당 = 0 B — 실측 0
|
||||||
|
[PASS] 판정은 ID 당 1회 — 58행×51회 뒤에도 누적 58회(=행 수)
|
||||||
|
검증 ON 뽑기 200회 Δ = 0 B · 원본 Get_DataList 호출 246회(재뽑기 46)
|
||||||
|
검증 OFF 뽑기 200회 Δ = 0 B · 원본 Get_DataList 호출 200회
|
||||||
|
원본 호출 1회당 = ON 0 B · OFF 0 B → 차이는 검증 코드가 아니라 **원본 LINQ 셔플을 몇 번 더 부르느냐**다
|
||||||
|
[PASS] 호출 1회당 할당이 대조군의 2배를 넘지 않는다(검증 코드 몫 ≈ 0)
|
||||||
|
🔴 실사용 빈도 = 존 클리어 1회당 뽑기 1회(런당 최대 3장×존 수) — 매 프레임 경로 아님
|
||||||
|
|
||||||
|
── ⑨ C8 — 스위치 하나로 813o 이전 100%
|
||||||
|
[PASS] validatePool=0 → 검증 축 off
|
||||||
|
[PASS] 제외 0 · 재뽑기 0
|
||||||
|
[PASS] RuntimeDisabled → 픽업 축 전체 off(813o C8 그대로)
|
||||||
|
[PASS] 복구
|
||||||
|
[PASS] suppressUntilNextRunStart=0 → 813x2 원래 동작으로 되돌린다
|
||||||
|
|
||||||
|
── ⑩ 🔴 D-42 — RunEnded 뒤 결과창~다음 런 시작까지 모달 0
|
||||||
|
[PASS] 813p WLRunSettings.Enabled(StartRun 전제) — 실측 True
|
||||||
|
초기 Phase=Idle IsRunning=False
|
||||||
|
[PASS] Idle(런 밖 · 시작 전) → 차단 안 함 = 원본 모달 살아 있음
|
||||||
|
[PASS] 훅 enabled=True
|
||||||
|
StartRun → Phase=Zones IsRunning=True
|
||||||
|
[PASS] 런 중 → 차단(813x2 그대로)
|
||||||
|
[PASS] 런 중 훅 enabled=False(원본 OnDisable 이 구독을 끊는다)
|
||||||
|
EndRun → Phase=Ended IsRunning=False
|
||||||
|
[PASS] 🔴 런 종료 뒤(결과창)에도 차단 유지 = D-42 해소
|
||||||
|
[PASS] 🔴 결과창에서 훅 enabled=False 유지(모달 0)
|
||||||
|
가짜 오류 1건 → captured=1 suppressed=1 written=1
|
||||||
|
[PASS] 🔴 결과창 예외가 「모달차단」으로 기록된다(모달 0)
|
||||||
|
[PASS] 훅은 우리가 내려둔 상태
|
||||||
|
로그 파일 = E:\NerdNavis\WL_wt\ui\Screenshots_WL/WL813x2\errors_190120.txt
|
||||||
|
[PASS] 연장 off → 결과창에서 차단 해제(= Q9 가 본 D-42 상태 재현)
|
||||||
|
[PASS] 연장 off 면 결과창에서 훅 enabled=True(원본 모달 부활)
|
||||||
|
[PASS] 연장 on 으로 되돌리면 다시 내려간다
|
||||||
|
다음 StartRun → Phase=Zones
|
||||||
|
[PASS] Phase=Zones(Ended 를 벗어났다 = 연장 래치 해제)
|
||||||
|
[PASS] 연장분 없이도 런 중이라 차단 = 복원 뒤 곧바로 다시 off
|
||||||
|
[PASS] Phase=Idle(로비/타이틀) → 차단 해제 = 원본 모달 복귀
|
||||||
|
[PASS] 훅 enabled=True 복원 · HookDisabledByUs=False
|
||||||
|
|
||||||
|
── ⑪ 정리
|
||||||
|
임시 오브젝트 제거 · 구독 해제 · 디스크 SO 무변경(값은 메모리 인스턴스만 손댔다)
|
||||||
|
[PASS] 픽업 노드 0
|
||||||
|
[PASS] 정리 완료
|
||||||
|
|
||||||
|
RESULT PASS — pass=67 fail=0
|
||||||
|
|
@ -0,0 +1,599 @@
|
||||||
|
// WL-813o2 프로브 — 선택스킬 카드 풀 검증(D-41) + 결과창 모달 차단 연장(D-42) 실측
|
||||||
|
// (에디트 모드 · Play 0 · 로그인 0 · 씬 저장 0 · 프리팹 저장 0)
|
||||||
|
// unity command run_script --file AgentScripts/WL813o2_Probe.cs --entry WL813o2_Probe.RunAll
|
||||||
|
// 산출물: <worktree>/AgentScripts/WL813o2_PROBE.txt (풀 검증 표 포함 · 커밋)
|
||||||
|
//
|
||||||
|
// 🔴 원본 `Assets/Script/**` 은 **읽기만** 한다(파싱·grep). 수정 0줄.
|
||||||
|
// 🔴 Addressables 키 = 이 프로젝트는 그룹이 **폴더 엔트리**(`m_Address: Assets/Res_Addr/<Folder>`)라
|
||||||
|
// 자식 에셋의 주소 = 그 에셋 경로다 → 키 존재 ⟺ 그 경로에 에셋이 있다(실측으로 확인한다).
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using UnityEngine;
|
||||||
|
using UnityEditor;
|
||||||
|
using WL.UI;
|
||||||
|
using WL.Combat.Run;
|
||||||
|
using WL.Combat.Diagnostics;
|
||||||
|
|
||||||
|
public static class WL813o2_Probe
|
||||||
|
{
|
||||||
|
const string kTableTxt = "Assets/ResWork/Table/table_selectskill.txt";
|
||||||
|
const string kSelectSkillDir = "Assets/Script/Character/Projectile/SelectSkill";
|
||||||
|
const string kAddrGroupDir = "Assets/AddressableAssetsData/AssetGroups";
|
||||||
|
|
||||||
|
static StringBuilder _o;
|
||||||
|
static int _pass, _fail;
|
||||||
|
static GameObject _fakeInfos;
|
||||||
|
static GameObject _fakeHook;
|
||||||
|
|
||||||
|
static void H(string s) { _o.AppendLine(); _o.AppendLine("── " + s); }
|
||||||
|
static void N(string s) { _o.AppendLine(" " + s); }
|
||||||
|
static bool Chk(bool ok, string what)
|
||||||
|
{
|
||||||
|
if (ok) { _pass++; _o.AppendLine(" [PASS] " + what); }
|
||||||
|
else { _fail++; _o.AppendLine(" [FAIL] " + what); }
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
static string Root { get { return Directory.GetParent(Application.dataPath).FullName; } }
|
||||||
|
static string OutPath(string name) { return Path.Combine(Path.Combine(Root, "AgentScripts"), name); }
|
||||||
|
|
||||||
|
public static string RunAll()
|
||||||
|
{
|
||||||
|
_o = new StringBuilder();
|
||||||
|
_pass = _fail = 0;
|
||||||
|
_o.AppendLine("# WL813o2 Probe " + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + " (edit mode · Play 0 · 로그인 0)");
|
||||||
|
_o.AppendLine("playMode=" + Application.isPlaying);
|
||||||
|
|
||||||
|
// 🔴 프로브는 SO 「메모리 인스턴스」의 값을 잠깐 바꾼다(A/B). 원값을 반드시 되돌린다(디스크 diff 0).
|
||||||
|
var s0 = WLZonePickupSettings.Instance;
|
||||||
|
var g0 = WLErrorGuardSettings.Instance;
|
||||||
|
bool bkValidate = s0 != null && s0.validatePool;
|
||||||
|
bool bkRuntimeKey = s0 != null && s0.runtimeKeyCheck;
|
||||||
|
int[] bkExclude = s0 != null ? s0.excludeSkillIds : null;
|
||||||
|
bool bkExtend = g0 != null && g0.suppressUntilNextRunStart;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Step0_Settings();
|
||||||
|
Step1_AddrGroups();
|
||||||
|
Step2_Fakes();
|
||||||
|
Step3_GrepOriginal();
|
||||||
|
Step4_PoolTable();
|
||||||
|
Step5_RuntimeKeyCheck();
|
||||||
|
Step6_Draw100();
|
||||||
|
Step7_Control_ValidateOff();
|
||||||
|
Step8_GC();
|
||||||
|
Step9_C8();
|
||||||
|
Step10_D42();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Chk(false, "예외 — " + ex.GetType().Name + ": " + ex.Message);
|
||||||
|
_o.AppendLine(ex.StackTrace);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (s0 != null) { s0.validatePool = bkValidate; s0.runtimeKeyCheck = bkRuntimeKey; s0.excludeSkillIds = bkExclude; }
|
||||||
|
if (g0 != null) g0.suppressUntilNextRunStart = bkExtend;
|
||||||
|
Cleanup();
|
||||||
|
}
|
||||||
|
|
||||||
|
_o.AppendLine();
|
||||||
|
_o.AppendLine("RESULT " + (_fail == 0 ? "PASS" : "FAIL") + " — pass=" + _pass + " fail=" + _fail);
|
||||||
|
string p = OutPath("WL813o2_PROBE.txt");
|
||||||
|
File.WriteAllText(p, _o.ToString(), new UTF8Encoding(true));
|
||||||
|
return (_fail == 0 ? "PASS" : "FAIL") + " pass=" + _pass + " fail=" + _fail + " → " + p;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────── ⓪ SO 값
|
||||||
|
static WLZonePickupSettings _s;
|
||||||
|
static WLErrorGuardSettings _g;
|
||||||
|
|
||||||
|
static void Step0_Settings()
|
||||||
|
{
|
||||||
|
H("⓪ 값(SO) — 코드 상수 0 (C45)");
|
||||||
|
WLZonePickupSettings.ClearCache();
|
||||||
|
WLErrorGuardSettings.ClearCache();
|
||||||
|
_s = WLZonePickupSettings.Instance;
|
||||||
|
_g = WLErrorGuardSettings.Instance;
|
||||||
|
Chk(_s != null, "WLZonePickupSettings.asset 로드");
|
||||||
|
Chk(_g != null, "WLErrorGuardSettings.asset 로드");
|
||||||
|
if (_s == null || _g == null) return;
|
||||||
|
|
||||||
|
N("validatePool=" + _s.validatePool + " runtimeKeyCheck=" + _s.runtimeKeyCheck +
|
||||||
|
" maxRedraws=" + _s.maxRedraws + " logExclusion=" + _s.logExclusion + " cardCount=" + _s.cardCount);
|
||||||
|
N("excludeSkillIds = [" + Join(_s.excludeSkillIds) + "]");
|
||||||
|
N("noPrefabEffects(" + Len(_s.noPrefabEffects) + ") = " + string.Join(", ", _s.noPrefabEffects ?? new string[0]));
|
||||||
|
for (int i = 0; i < Len(_s.extraKeys); i++)
|
||||||
|
N("extraKeys[" + i + "] " + _s.extraKeys[i].skillEffect + " → " + _s.extraKeys[i].assetKey +
|
||||||
|
" (" + _s.extraKeys[i].source + ")");
|
||||||
|
N("경로형식: select=" + _s.selectSkillPathFormat + " · skill=" + _s.skillPathFormat + " · effect=" + _s.effectPathFormat);
|
||||||
|
N("[813x2] suppressErrorModalInRun=" + _g.suppressErrorModalInRun +
|
||||||
|
" suppressOnlyWhileRunning=" + _g.suppressOnlyWhileRunning +
|
||||||
|
" suppressUntilNextRunStart=" + _g.suppressUntilNextRunStart);
|
||||||
|
Chk(_s.validatePool, "validatePool=1(풀 검증 켜짐)");
|
||||||
|
Chk(Len(_s.excludeSkillIds) > 0, "excludeSkillIds 비어 있지 않음");
|
||||||
|
Chk(Len(_s.extraKeys) > 0, "extraKeys(코드 문자열 키) 채워짐");
|
||||||
|
Chk(_g.suppressUntilNextRunStart, "suppressUntilNextRunStart=1(D-42 연장 켜짐)");
|
||||||
|
Chk(_s.cardCount == 3, "카드 3장(기획안 F-3)");
|
||||||
|
}
|
||||||
|
|
||||||
|
static int Len(Array a) { return a != null ? a.Length : 0; }
|
||||||
|
static string Join(int[] a)
|
||||||
|
{
|
||||||
|
if (a == null || a.Length == 0) return "";
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
for (int i = 0; i < a.Length; i++) { if (i > 0) sb.Append(", "); sb.Append(a[i]); }
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────── ① Addressables 그룹 = 폴더 엔트리인지 실측
|
||||||
|
static void Step1_AddrGroups()
|
||||||
|
{
|
||||||
|
H("① Addressables 그룹 실측 — 키 존재 ⟺ 그 경로의 에셋 존재");
|
||||||
|
string[] want = { "Projectile", "Skill", "Effect", "SelectSkill" };
|
||||||
|
for (int i = 0; i < want.Length; i++)
|
||||||
|
{
|
||||||
|
string p = kAddrGroupDir + "/" + want[i] + ".asset";
|
||||||
|
string txt = File.Exists(Path.Combine(Root, p)) ? File.ReadAllText(Path.Combine(Root, p)) : "";
|
||||||
|
var m = Regex.Match(txt, @"m_Address:\s*(\S+)");
|
||||||
|
string addr = m.Success ? m.Groups[1].Value : "(없음)";
|
||||||
|
N(want[i] + " 그룹 m_Address = " + addr + " (엔트리 " + Regex.Matches(txt, "m_Address:").Count + "개)");
|
||||||
|
Chk(addr == "Assets/Res_Addr/" + want[i], want[i] + " = 폴더 엔트리 1개(자식 주소 = 에셋 경로)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────── ② 원본 테이블·Info 대역 (813o 프로브와 같은 방식)
|
||||||
|
static void Step2_Fakes()
|
||||||
|
{
|
||||||
|
H("② 원본 데이터 경로 — table_selectskill.Awake 를 그대로 태운다");
|
||||||
|
_fakeInfos = new GameObject("WL813o2_Probe_Infos");
|
||||||
|
_fakeInfos.hideFlags = HideFlags.HideAndDontSave;
|
||||||
|
|
||||||
|
var tbl = _fakeInfos.AddComponent<table_selectskill>();
|
||||||
|
tbl.m_json = AssetDatabase.LoadAssetAtPath<TextAsset>(kTableTxt);
|
||||||
|
Chk(tbl.m_json != null, "테이블 TextAsset " + kTableTxt);
|
||||||
|
var awake = typeof(table_selectskill).GetMethod("Awake", BindingFlags.NonPublic | BindingFlags.Instance);
|
||||||
|
if (awake != null) awake.Invoke(tbl, null);
|
||||||
|
Chk(table_selectskill.Ins != null, "table_selectskill.Ins 설정(원본 Awake)");
|
||||||
|
|
||||||
|
var info = _fakeInfos.AddComponent<InGameInfo>();
|
||||||
|
InGameInfo.Ins = info;
|
||||||
|
var actors = _fakeInfos.AddComponent<ActorInfo>();
|
||||||
|
ActorInfo.Ins = actors;
|
||||||
|
Chk(InGameInfo.Ins != null && ActorInfo.Ins != null, "InGameInfo.Ins · ActorInfo.Ins 설정");
|
||||||
|
|
||||||
|
int all = table_selectskill.Ins != null ? table_selectskill.Ins.Get_DataList().Count : 0;
|
||||||
|
N("테이블 행 수 실측 = " + all + " (발주서 「기획안 42행」과 다르다 → 실측값을 쓴다)");
|
||||||
|
Chk(all > 0, "행 파싱 성공");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────── ③ 원본 코드 문자열 grep (읽기만)
|
||||||
|
static readonly List<string> _codeKeys = new List<string>();
|
||||||
|
|
||||||
|
static void Step3_GrepOriginal()
|
||||||
|
{
|
||||||
|
H("③ 원본 효과 클래스 grep — `Shoot_Projectile(\"…\")` 코드 문자열 (읽기만 · 수정 0)");
|
||||||
|
string dir = Path.Combine(Root, kSelectSkillDir);
|
||||||
|
Chk(Directory.Exists(dir), "원본 폴더 " + kSelectSkillDir);
|
||||||
|
if (!Directory.Exists(dir)) return;
|
||||||
|
|
||||||
|
var files = Directory.GetFiles(dir, "*.cs", SearchOption.AllDirectories);
|
||||||
|
N("파일 " + files.Length + "개");
|
||||||
|
var rx = new Regex("Shoot_Projectile\\s*\\(\\s*\"([^\"]+)\"");
|
||||||
|
for (int f = 0; f < files.Length; f++)
|
||||||
|
{
|
||||||
|
var lines = File.ReadAllLines(files[f]);
|
||||||
|
for (int i = 0; i < lines.Length; i++)
|
||||||
|
{
|
||||||
|
string trimmed = lines[i].TrimStart();
|
||||||
|
bool commented = trimmed.StartsWith("//");
|
||||||
|
var m = rx.Match(lines[i]);
|
||||||
|
if (!m.Success) continue;
|
||||||
|
string cls = Path.GetFileNameWithoutExtension(files[f]);
|
||||||
|
N((commented ? " (주석) " : " ") + cls + ".cs:" + (i + 1) + " 키=\"" + m.Groups[1].Value + "\"");
|
||||||
|
if (!commented) _codeKeys.Add(cls + "|" + m.Groups[1].Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Chk(_codeKeys.Count > 0, "살아 있는 Shoot_Projectile 문자열 " + _codeKeys.Count + "건");
|
||||||
|
|
||||||
|
// SO extraKeys 가 이 문자열들을 전부 덮는가
|
||||||
|
for (int i = 0; i < _codeKeys.Count; i++)
|
||||||
|
{
|
||||||
|
var parts = _codeKeys[i].Split('|');
|
||||||
|
string effect = parts[0]; // 클래스명 = eEffect 이름과 같다(원본 프리팹 이름 규칙)
|
||||||
|
string key = "Assets/Res_Addr/Projectile/" + parts[1] + ".prefab";
|
||||||
|
bool covered = false;
|
||||||
|
for (int k = 0; k < Len(_s.extraKeys); k++)
|
||||||
|
if (_s.extraKeys[k].skillEffect == effect && _s.extraKeys[k].assetKey == key) covered = true;
|
||||||
|
Chk(covered, "SO extraKeys 가 " + effect + " → " + key + " 를 덮는다");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────── ④ 풀 검증 표 (전 행 · 에디트 모드)
|
||||||
|
class Row
|
||||||
|
{
|
||||||
|
public int id; public string effect; public int stage; public bool eligible;
|
||||||
|
public string primary = ""; public bool primaryOk = true;
|
||||||
|
public string extra = ""; public bool extraOk = true;
|
||||||
|
public string name = "";
|
||||||
|
}
|
||||||
|
static readonly List<Row> _rows = new List<Row>();
|
||||||
|
|
||||||
|
static bool AssetExists(string path)
|
||||||
|
{
|
||||||
|
return !string.IsNullOrEmpty(path) && AssetDatabase.LoadAssetAtPath<GameObject>(path) != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void Step4_PoolTable()
|
||||||
|
{
|
||||||
|
H("④ 🔴 풀 검증 표 — 전 행 × 참조 키 × 실존 (에디트 모드 · AssetDatabase)");
|
||||||
|
var all = table_selectskill.Ins.Get_DataList();
|
||||||
|
for (int i = 0; i < all.Count; i++)
|
||||||
|
{
|
||||||
|
var r = all[i] as SelectSkillTableData;
|
||||||
|
if (r == null) continue;
|
||||||
|
var row = new Row { id = r.ID, effect = r.SkillEffect.ToString(), stage = r.EnableStage, name = r.Name };
|
||||||
|
row.eligible = r.EnableStage <= 1; // 원본 Get_DataList 의 curstage = 1 (하드코딩 · 읽기만)
|
||||||
|
|
||||||
|
bool noPrefab = false;
|
||||||
|
for (int k = 0; k < Len(_s.noPrefabEffects); k++)
|
||||||
|
if (_s.noPrefabEffects[k] == row.effect) noPrefab = true;
|
||||||
|
|
||||||
|
if (r.SkillEffect != eEffect.None && !noPrefab)
|
||||||
|
{
|
||||||
|
string fmt = row.effect.Contains("Select") ? _s.selectSkillPathFormat
|
||||||
|
: row.effect.Contains("Skill") ? _s.skillPathFormat : _s.effectPathFormat;
|
||||||
|
row.primary = string.Format(fmt, row.effect);
|
||||||
|
row.primaryOk = AssetExists(row.primary);
|
||||||
|
}
|
||||||
|
for (int k = 0; k < Len(_s.extraKeys); k++)
|
||||||
|
{
|
||||||
|
var x = _s.extraKeys[k];
|
||||||
|
if (x == null || x.skillEffect != row.effect) continue;
|
||||||
|
row.extra = x.assetKey;
|
||||||
|
row.extraOk = AssetExists(x.assetKey);
|
||||||
|
}
|
||||||
|
_rows.Add(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
_o.AppendLine();
|
||||||
|
_o.AppendLine(" | ID | SkillEffect | Stg | 뽑기가능 | 1차 키 | 실존 | 추가 키 | 실존 | 판정 |");
|
||||||
|
_o.AppendLine(" |---|---|---|---|---|---|---|---|---|");
|
||||||
|
var missing = new List<Row>();
|
||||||
|
for (int i = 0; i < _rows.Count; i++)
|
||||||
|
{
|
||||||
|
var r = _rows[i];
|
||||||
|
bool bad = !r.primaryOk || !r.extraOk;
|
||||||
|
if (bad) missing.Add(r);
|
||||||
|
_o.AppendLine(" | " + r.id + " | " + r.effect + " | " + r.stage + " | " + (r.eligible ? "O" : "-") +
|
||||||
|
" | " + (r.primary == "" ? "(없음)" : Short(r.primary)) + " | " + (r.primary == "" ? "-" : (r.primaryOk ? "O" : "🔴X")) +
|
||||||
|
" | " + (r.extra == "" ? "-" : Short(r.extra)) + " | " + (r.extra == "" ? "-" : (r.extraOk ? "O" : "🔴X")) +
|
||||||
|
" | " + (bad ? "🔴 제외" : "사용") + " |");
|
||||||
|
}
|
||||||
|
_o.AppendLine();
|
||||||
|
N("전체 " + _rows.Count + "행 · 부재 키 행 " + missing.Count + "건");
|
||||||
|
for (int i = 0; i < missing.Count; i++)
|
||||||
|
N(" 🔴 행 " + missing[i].id + " " + missing[i].effect + " (Stage " + missing[i].stage + " · " +
|
||||||
|
(missing[i].eligible ? "지금 뽑힘" : "지금은 안 뽑힘") + ") — 부재 키 " +
|
||||||
|
(!missing[i].primaryOk ? missing[i].primary : missing[i].extra));
|
||||||
|
|
||||||
|
// SO 목록과 실측이 일치하는가
|
||||||
|
for (int i = 0; i < missing.Count; i++)
|
||||||
|
{
|
||||||
|
bool listed = false;
|
||||||
|
for (int k = 0; k < Len(_s.excludeSkillIds); k++) if (_s.excludeSkillIds[k] == missing[i].id) listed = true;
|
||||||
|
Chk(listed, "부재 키 행 " + missing[i].id + "(" + missing[i].effect + ")이 SO excludeSkillIds 에 있다");
|
||||||
|
}
|
||||||
|
for (int k = 0; k < Len(_s.excludeSkillIds); k++)
|
||||||
|
{
|
||||||
|
bool real = false;
|
||||||
|
for (int i = 0; i < missing.Count; i++) if (missing[i].id == _s.excludeSkillIds[k]) real = true;
|
||||||
|
Chk(real, "SO excludeSkillIds[" + _s.excludeSkillIds[k] + "] 는 실측 부재 행이다(과잉 제외 0)");
|
||||||
|
}
|
||||||
|
Chk(missing.Count > 0, "D-41 재현 — 부재 키 행이 실제로 있다");
|
||||||
|
}
|
||||||
|
|
||||||
|
static string Short(string p) { return p.Replace("Assets/Res_Addr/", ""); }
|
||||||
|
|
||||||
|
// ─────────────── ⑤ 런타임 경로(게임 코드)로 같은 판정을 하는가
|
||||||
|
static void Step5_RuntimeKeyCheck()
|
||||||
|
{
|
||||||
|
H("⑤ 런타임 검사 — 게임 코드 `ZonePickupPool` 이 같은 결론을 내는가");
|
||||||
|
ZonePickupPool.ResetCache();
|
||||||
|
var all = table_selectskill.Ins.Get_DataList();
|
||||||
|
int agree = 0, disagree = 0;
|
||||||
|
for (int i = 0; i < all.Count; i++)
|
||||||
|
{
|
||||||
|
var r = all[i] as SelectSkillTableData;
|
||||||
|
if (r == null) continue;
|
||||||
|
string miss;
|
||||||
|
bool keysOk = ZonePickupPool.KeysExist(_s, r, out miss);
|
||||||
|
Row t = null;
|
||||||
|
for (int k = 0; k < _rows.Count; k++) if (_rows[k].id == r.ID) t = _rows[k];
|
||||||
|
bool expect = t == null || (t.primaryOk && t.extraOk);
|
||||||
|
if (keysOk == expect) agree++;
|
||||||
|
else { disagree++; N(" 불일치 행 " + r.ID + " runtime=" + keysOk + " expect=" + expect + " miss=" + miss); }
|
||||||
|
}
|
||||||
|
Chk(disagree == 0, "에디트 모드 표와 런타임 판정 완전 일치(" + agree + "행 · 불일치 " + disagree + ")");
|
||||||
|
|
||||||
|
// 제외 판정 + 로그 1줄
|
||||||
|
ZonePickupPool.ResetCache();
|
||||||
|
for (int k = 0; k < Len(_s.excludeSkillIds); k++)
|
||||||
|
{
|
||||||
|
var r = table_selectskill.Ins.Get_Data(_s.excludeSkillIds[k]);
|
||||||
|
string why;
|
||||||
|
bool ex = ZonePickupPool.Excluded(_s, r, out why);
|
||||||
|
Chk(ex, "행 " + _s.excludeSkillIds[k] + " 제외 판정 — 사유 \"" + why + "\"");
|
||||||
|
}
|
||||||
|
N("카운터: checked=" + ZonePickupPool.CheckedCount + " staticExclude=" + ZonePickupPool.StaticExcludeCount +
|
||||||
|
" missingKey=" + ZonePickupPool.MissingKeyCount + " skipped=" + ZonePickupPool.SkippedCheckCount);
|
||||||
|
|
||||||
|
// SO 정적 목록을 비우고 **키 검사만으로도** 같은 행이 걸리는가 (근본 검사 증명)
|
||||||
|
var saved = _s.excludeSkillIds;
|
||||||
|
_s.excludeSkillIds = new int[0];
|
||||||
|
ZonePickupPool.ResetCache();
|
||||||
|
var onlyKey = new List<int>();
|
||||||
|
for (int i = 0; i < all.Count; i++)
|
||||||
|
{
|
||||||
|
var r = all[i] as SelectSkillTableData;
|
||||||
|
if (r == null) continue;
|
||||||
|
string why;
|
||||||
|
if (ZonePickupPool.Excluded(_s, r, out why)) onlyKey.Add(r.ID);
|
||||||
|
}
|
||||||
|
N("정적 목록 없이 키 검사만으로 제외된 행 = [" + Join(onlyKey.ToArray()) + "]");
|
||||||
|
Chk(onlyKey.Count == Len(saved), "키 검사만으로도 같은 수(" + onlyKey.Count + ")가 걸린다 = 목록은 캐시일 뿐");
|
||||||
|
_s.excludeSkillIds = saved;
|
||||||
|
ZonePickupPool.ResetCache();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────── ⑥ 3장 뽑기 100회
|
||||||
|
static void Step6_Draw100()
|
||||||
|
{
|
||||||
|
H("⑥ 3장 뽑기 100회 — 제외 행이 한 번도 안 나오는가");
|
||||||
|
ZonePickup.ResetState();
|
||||||
|
ZonePickupPool.ResetCache();
|
||||||
|
var seen = new Dictionary<int, int>();
|
||||||
|
int ok = 0, dupe = 0, short_ = 0;
|
||||||
|
for (int n = 0; n < 100; n++)
|
||||||
|
{
|
||||||
|
if (!ZonePickup.DrawForProbe()) { short_++; continue; }
|
||||||
|
ok++;
|
||||||
|
if (ZonePickup.CardCount != 3) short_++;
|
||||||
|
var ids = new HashSet<int>();
|
||||||
|
for (int i = 0; i < ZonePickup.CardCount; i++)
|
||||||
|
{
|
||||||
|
int id = ZonePickup.CardId(i);
|
||||||
|
if (!ids.Add(id)) dupe++;
|
||||||
|
seen[id] = seen.ContainsKey(id) ? seen[id] + 1 : 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Chk(ok == 100, "100회 전부 성공 — 실측 " + ok);
|
||||||
|
Chk(short_ == 0, "카드 수 3장 미달 0 — 실측 " + short_);
|
||||||
|
Chk(dupe == 0, "한 뽑기 안 ID 중복 0 — 실측 " + dupe);
|
||||||
|
|
||||||
|
var keys = new List<int>(seen.Keys); keys.Sort();
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
for (int i = 0; i < keys.Count; i++) sb.Append(keys[i]).Append("×").Append(seen[keys[i]]).Append(" ");
|
||||||
|
N("나온 ID 분포(300장): " + sb);
|
||||||
|
for (int k = 0; k < Len(_s.excludeSkillIds); k++)
|
||||||
|
{
|
||||||
|
int id = _s.excludeSkillIds[k];
|
||||||
|
int c = seen.ContainsKey(id) ? seen[id] : 0;
|
||||||
|
Chk(c == 0, "🔴 제외 행 " + id + " 등장 0회 — 실측 " + c + "회");
|
||||||
|
}
|
||||||
|
N("ExcludedCount=" + ZonePickup.ExcludedCount + " RedrawCount=" + ZonePickup.RedrawCount +
|
||||||
|
" last=\"" + ZonePickup.LastExcludeReason + "\"");
|
||||||
|
Chk(ZonePickup.ExcludedCount > 0, "실제로 제외가 걸렸다(후필터가 살아 있다) — " + ZonePickup.ExcludedCount + "건");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────── ⑦ 대조군 — validatePool = 0 이면 「별」이 다시 나온다
|
||||||
|
static void Step7_Control_ValidateOff()
|
||||||
|
{
|
||||||
|
H("⑦ 대조군 — validatePool=0 (813o 동작) 이면 제외 행이 다시 뽑힌다");
|
||||||
|
bool saved = _s.validatePool;
|
||||||
|
_s.validatePool = false;
|
||||||
|
ZonePickup.ResetState();
|
||||||
|
ZonePickupPool.ResetCache();
|
||||||
|
var seen = new Dictionary<int, int>();
|
||||||
|
for (int n = 0; n < 100; n++)
|
||||||
|
{
|
||||||
|
if (!ZonePickup.DrawForProbe()) continue;
|
||||||
|
for (int i = 0; i < ZonePickup.CardCount; i++)
|
||||||
|
{
|
||||||
|
int id = ZonePickup.CardId(i);
|
||||||
|
seen[id] = seen.ContainsKey(id) ? seen[id] + 1 : 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
int hit = 0;
|
||||||
|
for (int k = 0; k < Len(_s.excludeSkillIds); k++)
|
||||||
|
if (seen.ContainsKey(_s.excludeSkillIds[k])) hit += seen[_s.excludeSkillIds[k]];
|
||||||
|
N("validatePool=0 에서 제외 대상 등장 = " + hit + "장 / 300장");
|
||||||
|
Chk(hit > 0, "대조군에서는 실제로 나온다 = ⑥의 0회가 필터 덕분임을 증명");
|
||||||
|
Chk(ZonePickup.ExcludedCount == 0, "validatePool=0 이면 제외 0 · 재뽑기 0(813o 동작 그대로)");
|
||||||
|
Chk(ZonePickup.RedrawCount == 0, "재뽑기 0회");
|
||||||
|
_s.validatePool = saved;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────── ⑧ GC
|
||||||
|
static void Step8_GC()
|
||||||
|
{
|
||||||
|
H("⑧ GC — 검증 코드 자체의 할당(캐시가 선 뒤)");
|
||||||
|
_s.validatePool = true;
|
||||||
|
ZonePickupPool.ResetCache();
|
||||||
|
var all = table_selectskill.Ins.Get_DataList();
|
||||||
|
var okRow = table_selectskill.Ins.Get_Data(101); // 사용 행
|
||||||
|
var badRow = table_selectskill.Ins.Get_Data(_s.excludeSkillIds[Len(_s.excludeSkillIds) - 1]); // 제외 행
|
||||||
|
string w0;
|
||||||
|
ZonePickupPool.Excluded(_s, okRow, out w0); // 캐시 워밍
|
||||||
|
ZonePickupPool.Excluded(_s, badRow, out w0);
|
||||||
|
ZonePickupPool.KeyExists(_s.extraKeys[0].assetKey);
|
||||||
|
|
||||||
|
// ⓐ 판정 자체 — 캐시 히트 경로는 문자열도 만들지 않는다
|
||||||
|
long a = GC.GetTotalMemory(true);
|
||||||
|
for (int i = 0; i < 2000; i++)
|
||||||
|
{
|
||||||
|
string w;
|
||||||
|
ZonePickupPool.Excluded(_s, okRow, out w);
|
||||||
|
ZonePickupPool.Excluded(_s, badRow, out w);
|
||||||
|
ZonePickupPool.KeyExists(_s.extraKeys[0].assetKey);
|
||||||
|
}
|
||||||
|
long judge = GC.GetTotalMemory(false) - a;
|
||||||
|
N("판정 6,000회(캐시 히트) Δ = " + judge + " B");
|
||||||
|
Chk(judge == 0, "🔴 캐시가 선 뒤 판정 할당 = 0 B — 실측 " + judge);
|
||||||
|
|
||||||
|
// ⓑ 전 행 1회 판정(캐시 채우기) 뒤에는 판정 횟수가 늘지 않는다
|
||||||
|
ZonePickupPool.ResetCache();
|
||||||
|
for (int i = 0; i < all.Count; i++) { string w; ZonePickupPool.Excluded(_s, all[i] as SelectSkillTableData, out w); }
|
||||||
|
int after1 = ZonePickupPool.CheckedCount;
|
||||||
|
for (int r = 0; r < 50; r++)
|
||||||
|
for (int i = 0; i < all.Count; i++) { string w; ZonePickupPool.Excluded(_s, all[i] as SelectSkillTableData, out w); }
|
||||||
|
Chk(ZonePickupPool.CheckedCount == after1,
|
||||||
|
"판정은 ID 당 1회 — 58행×51회 뒤에도 누적 " + ZonePickupPool.CheckedCount + "회(=행 수)");
|
||||||
|
|
||||||
|
// ⓒ 뽑기 전체 비교(참고) — 차이는 **재뽑기로 늘어난 원본 호출** 몫이다
|
||||||
|
ZonePickup.ResetState();
|
||||||
|
ZonePickup.DrawForProbe();
|
||||||
|
long c0 = GC.GetTotalMemory(true);
|
||||||
|
for (int i = 0; i < 200; i++) ZonePickup.DrawForProbe();
|
||||||
|
long onB = GC.GetTotalMemory(false) - c0;
|
||||||
|
int onCalls = 200 + ZonePickup.RedrawCount;
|
||||||
|
|
||||||
|
bool saved = _s.validatePool;
|
||||||
|
_s.validatePool = false;
|
||||||
|
ZonePickup.ResetState();
|
||||||
|
ZonePickup.DrawForProbe();
|
||||||
|
long c1 = GC.GetTotalMemory(true);
|
||||||
|
for (int i = 0; i < 200; i++) ZonePickup.DrawForProbe();
|
||||||
|
long offB = GC.GetTotalMemory(false) - c1;
|
||||||
|
_s.validatePool = saved;
|
||||||
|
|
||||||
|
N("검증 ON 뽑기 200회 Δ = " + onB + " B · 원본 Get_DataList 호출 " + onCalls + "회(재뽑기 " + (onCalls - 200) + ")");
|
||||||
|
N("검증 OFF 뽑기 200회 Δ = " + offB + " B · 원본 Get_DataList 호출 200회");
|
||||||
|
double perOn = onCalls > 0 ? (double)onB / onCalls : 0, perOff = offB > 0 ? (double)offB / 200 : 0;
|
||||||
|
N("원본 호출 1회당 = ON " + perOn.ToString("F0") + " B · OFF " + perOff.ToString("F0") +
|
||||||
|
" B → 차이는 검증 코드가 아니라 **원본 LINQ 셔플을 몇 번 더 부르느냐**다");
|
||||||
|
Chk(perOff <= 0 || perOn <= perOff * 2.0,
|
||||||
|
"호출 1회당 할당이 대조군의 2배를 넘지 않는다(검증 코드 몫 ≈ 0)");
|
||||||
|
N("🔴 실사용 빈도 = 존 클리어 1회당 뽑기 1회(런당 최대 " + _s.cardCount + "장×존 수) — 매 프레임 경로 아님");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────── ⑨ C8
|
||||||
|
static void Step9_C8()
|
||||||
|
{
|
||||||
|
H("⑨ C8 — 스위치 하나로 813o 이전 100%");
|
||||||
|
_s.validatePool = false;
|
||||||
|
Chk(!ZonePickupPool.Active(_s), "validatePool=0 → 검증 축 off");
|
||||||
|
ZonePickup.ResetState(); ZonePickupPool.ResetCache();
|
||||||
|
ZonePickup.DrawForProbe();
|
||||||
|
Chk(ZonePickup.ExcludedCount == 0 && ZonePickup.RedrawCount == 0, "제외 0 · 재뽑기 0");
|
||||||
|
_s.validatePool = true;
|
||||||
|
|
||||||
|
WLZonePickupSettings.RuntimeDisabled = true;
|
||||||
|
Chk(!WLZonePickupSettings.Enabled, "RuntimeDisabled → 픽업 축 전체 off(813o C8 그대로)");
|
||||||
|
WLZonePickupSettings.RuntimeDisabled = false;
|
||||||
|
Chk(WLZonePickupSettings.Enabled, "복구");
|
||||||
|
|
||||||
|
bool savedG = _g.suppressUntilNextRunStart;
|
||||||
|
_g.suppressUntilNextRunStart = false;
|
||||||
|
Chk(_g.suppressUntilNextRunStart == false, "suppressUntilNextRunStart=0 → 813x2 원래 동작으로 되돌린다");
|
||||||
|
_g.suppressUntilNextRunStart = savedG;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────── ⑩ D-42 — 런 종료 뒤에도 모달 차단, 다음 런 시작에 복원
|
||||||
|
static void Step10_D42()
|
||||||
|
{
|
||||||
|
H("⑩ 🔴 D-42 — RunEnded 뒤 결과창~다음 런 시작까지 모달 0");
|
||||||
|
|
||||||
|
_fakeHook = new GameObject("WL813o2_Probe_Hook");
|
||||||
|
_fakeHook.hideFlags = HideFlags.HideAndDontSave;
|
||||||
|
var hook = _fakeHook.AddComponent<ErrorLogHookManager>();
|
||||||
|
RunErrorModalGuard.ResetHookCache();
|
||||||
|
RunErrorModalGuard.SetHookForTest(hook);
|
||||||
|
RunErrorModalGuard.ResetCounters();
|
||||||
|
RunErrorModalGuard.ForceState = 0;
|
||||||
|
RunErrorModalGuard.Subscribe();
|
||||||
|
|
||||||
|
RunDirector.ResetAll();
|
||||||
|
Chk(WLRunSettings.Enabled, "813p WLRunSettings.Enabled(StartRun 전제) — 실측 " + WLRunSettings.Enabled);
|
||||||
|
N("초기 Phase=" + RunDirector.Phase + " IsRunning=" + RunDirector.IsRunning);
|
||||||
|
RunErrorModalGuard.Apply(RunErrorModalGuard.ShouldSuppress(), _g);
|
||||||
|
Chk(!RunErrorModalGuard.ShouldSuppress(), "Idle(런 밖 · 시작 전) → 차단 안 함 = 원본 모달 살아 있음");
|
||||||
|
Chk(hook.enabled, "훅 enabled=True");
|
||||||
|
|
||||||
|
RunDirector.StartRun("probe");
|
||||||
|
N("StartRun → Phase=" + RunDirector.Phase + " IsRunning=" + RunDirector.IsRunning);
|
||||||
|
RunErrorModalGuard.Apply(RunErrorModalGuard.ShouldSuppress(), _g);
|
||||||
|
Chk(RunErrorModalGuard.ShouldSuppress(), "런 중 → 차단(813x2 그대로)");
|
||||||
|
Chk(!hook.enabled, "런 중 훅 enabled=False(원본 OnDisable 이 구독을 끊는다)");
|
||||||
|
|
||||||
|
RunDirector.EndRun(RunOutcome.Win);
|
||||||
|
N("EndRun → Phase=" + RunDirector.Phase + " IsRunning=" + RunDirector.IsRunning);
|
||||||
|
RunErrorModalGuard.Apply(RunErrorModalGuard.ShouldSuppress(), _g);
|
||||||
|
Chk(RunErrorModalGuard.ShouldSuppress(), "🔴 런 종료 뒤(결과창)에도 차단 유지 = D-42 해소");
|
||||||
|
Chk(!hook.enabled, "🔴 결과창에서 훅 enabled=False 유지(모달 0)");
|
||||||
|
|
||||||
|
// 결과창에서 가짜 예외 1건 → 모달 0(= 훅이 내려가 있어 원본 HandleLog 미호출) · 파일 로그는 계속
|
||||||
|
int before = RunErrorModalGuard.SuppressedCount;
|
||||||
|
Debug.LogError("[WL813o2] 결과창 가짜 오류(모달 0 확인용)");
|
||||||
|
N("가짜 오류 1건 → captured=" + RunErrorModalGuard.CapturedCount +
|
||||||
|
" suppressed=" + RunErrorModalGuard.SuppressedCount + " written=" + RunErrorModalGuard.WrittenCount);
|
||||||
|
Chk(RunErrorModalGuard.SuppressedCount == before + 1, "🔴 결과창 예외가 「모달차단」으로 기록된다(모달 0)");
|
||||||
|
Chk(RunErrorModalGuard.HookDisabledByUs, "훅은 우리가 내려둔 상태");
|
||||||
|
N("로그 파일 = " + RunErrorModalGuard.LastFilePath);
|
||||||
|
|
||||||
|
// 813x2 원래 동작(연장 off)이면 결과창에서 복원된다 = D-42 재현
|
||||||
|
bool saved = _g.suppressUntilNextRunStart;
|
||||||
|
_g.suppressUntilNextRunStart = false;
|
||||||
|
RunErrorModalGuard.Apply(RunErrorModalGuard.ShouldSuppress(), _g);
|
||||||
|
Chk(!RunErrorModalGuard.ShouldSuppress(), "연장 off → 결과창에서 차단 해제(= Q9 가 본 D-42 상태 재현)");
|
||||||
|
Chk(hook.enabled, "연장 off 면 결과창에서 훅 enabled=True(원본 모달 부활)");
|
||||||
|
_g.suppressUntilNextRunStart = saved;
|
||||||
|
RunErrorModalGuard.Apply(RunErrorModalGuard.ShouldSuppress(), _g);
|
||||||
|
Chk(!hook.enabled, "연장 on 으로 되돌리면 다시 내려간다");
|
||||||
|
|
||||||
|
// 다음 런 시작 → 복원(그리고 런 중이니 다시 내려간다)
|
||||||
|
RunDirector.StartRun("probe-next");
|
||||||
|
N("다음 StartRun → Phase=" + RunDirector.Phase);
|
||||||
|
Chk(RunDirector.Phase == RunPhase.Zones, "Phase=Zones(Ended 를 벗어났다 = 연장 래치 해제)");
|
||||||
|
_g.suppressUntilNextRunStart = false; // 연장분만 떼고 보면
|
||||||
|
Chk(RunErrorModalGuard.ShouldSuppress(), "연장분 없이도 런 중이라 차단 = 복원 뒤 곧바로 다시 off");
|
||||||
|
_g.suppressUntilNextRunStart = saved;
|
||||||
|
|
||||||
|
// 로비 복귀(Idle) 에서는 복원된다
|
||||||
|
RunDirector.ResetAll();
|
||||||
|
RunErrorModalGuard.Apply(RunErrorModalGuard.ShouldSuppress(), _g);
|
||||||
|
Chk(!RunErrorModalGuard.ShouldSuppress(), "Phase=Idle(로비/타이틀) → 차단 해제 = 원본 모달 복귀");
|
||||||
|
Chk(hook.enabled, "훅 enabled=True 복원 · HookDisabledByUs=" + RunErrorModalGuard.HookDisabledByUs);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────── 정리
|
||||||
|
static void Cleanup()
|
||||||
|
{
|
||||||
|
H("⑪ 정리");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
RunErrorModalGuard.Restore();
|
||||||
|
RunErrorModalGuard.Unsubscribe();
|
||||||
|
RunErrorModalGuard.ResetHookCache();
|
||||||
|
RunDirector.ResetAll();
|
||||||
|
RunDirector.Unsubscribe();
|
||||||
|
ZonePickup.Teardown();
|
||||||
|
ZonePickupPool.ResetCache();
|
||||||
|
WLZonePickupSettings.RuntimeDisabled = false;
|
||||||
|
if (_fakeHook != null) UnityEngine.Object.DestroyImmediate(_fakeHook);
|
||||||
|
if (_fakeInfos != null) UnityEngine.Object.DestroyImmediate(_fakeInfos);
|
||||||
|
table_selectskill.Ins = null; InGameInfo.Ins = null; ActorInfo.Ins = null;
|
||||||
|
_fakeHook = null; _fakeInfos = null;
|
||||||
|
N("임시 오브젝트 제거 · 구독 해제 · 디스크 SO 무변경(값은 메모리 인스턴스만 손댔다)");
|
||||||
|
Chk(!ZonePickup.Bound, "픽업 노드 0");
|
||||||
|
Chk(true, "정리 완료");
|
||||||
|
}
|
||||||
|
catch (Exception ex) { N("정리 중 예외(무시): " + ex.Message); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -128,7 +128,9 @@ namespace WL.Combat.Diagnostics
|
||||||
if (cfg == null || !cfg.enabled || WLErrorGuardSettings.RuntimeDisabled) return false;
|
if (cfg == null || !cfg.enabled || WLErrorGuardSettings.RuntimeDisabled) return false;
|
||||||
if (!cfg.suppressErrorModalInRun) return false;
|
if (!cfg.suppressErrorModalInRun) return false;
|
||||||
if (!cfg.suppressOnlyWhileRunning) return true;
|
if (!cfg.suppressOnlyWhileRunning) return true;
|
||||||
return WL.Combat.Run.RunDirector.IsRunning;
|
if (WL.Combat.Run.RunDirector.IsRunning) return true;
|
||||||
|
// WL-813o2(Q9 D-42): 런 종료 뒤에도 결과창~다음 런 시작까지 훅을 내린 채 둔다.
|
||||||
|
return cfg.suppressUntilNextRunStart && WL.Combat.Run.RunDirector.Phase == WL.Combat.Run.RunPhase.Ended;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─────────────────────────────────────────── 틱(러너가 부른다 · 프로브도 직접 부를 수 있다)
|
// ─────────────────────────────────────────── 틱(러너가 부른다 · 프로브도 직접 부를 수 있다)
|
||||||
|
|
|
||||||
|
|
@ -67,6 +67,10 @@ namespace WL.Combat.Diagnostics
|
||||||
[Tooltip("1 = 런 중(RunDirector.IsRunning)에만 차단 · 0 = Play 내내 차단. 시연 중 로비 오류까지 막으려면 0.")]
|
[Tooltip("1 = 런 중(RunDirector.IsRunning)에만 차단 · 0 = Play 내내 차단. 시연 중 로비 오류까지 막으려면 0.")]
|
||||||
public bool suppressOnlyWhileRunning = true;
|
public bool suppressOnlyWhileRunning = true;
|
||||||
|
|
||||||
|
[Tooltip("WL-813o2(Q9 D-42) — 런이 끝난 뒤(RunPhase.Ended)에도 훅을 내린 채 둔다. " +
|
||||||
|
"결과창과 [다음 런] 대기 구간까지 모달 0 · 다음 RunStarted(Phase=Zones)에 복원된다. 끄면 813x2 원래 동작.")]
|
||||||
|
public bool suppressUntilNextRunStart = true;
|
||||||
|
|
||||||
[Tooltip("차단한(그리고 차단하지 않은) Error/Exception 을 파일로 남긴다. 끄면 카운터만 센다.")]
|
[Tooltip("차단한(그리고 차단하지 않은) Error/Exception 을 파일로 남긴다. 끄면 카운터만 센다.")]
|
||||||
public bool writeErrorFile = true;
|
public bool writeErrorFile = true;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ MonoBehaviour:
|
||||||
enabled: 1
|
enabled: 1
|
||||||
suppressErrorModalInRun: 1
|
suppressErrorModalInRun: 1
|
||||||
suppressOnlyWhileRunning: 1
|
suppressOnlyWhileRunning: 1
|
||||||
|
suppressUntilNextRunStart: 1
|
||||||
writeErrorFile: 1
|
writeErrorFile: 1
|
||||||
errorFileDir: Screenshots_WL/WL813x2
|
errorFileDir: Screenshots_WL/WL813x2
|
||||||
includeStack: 1
|
includeStack: 1
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,202 @@ namespace WL.UI
|
||||||
Open = 2,
|
Open = 2,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────
|
||||||
|
// WL-813o2 (Q9 D-41) — 카드 풀 검증
|
||||||
|
//
|
||||||
|
// ■ 왜
|
||||||
|
// 「별」(ID 14 · `Select_Star`)은 `Select_Star.cs:27` 에서 0.05 s 마다 32방향으로
|
||||||
|
// `Shoot_Projectile("ProjectileStar", …)` 를 부른다. 그런데
|
||||||
|
// `Assets/Res_Addr/Projectile/ProjectileStar.prefab` 이 **디스크에 없다** →
|
||||||
|
// `InvalidKeyException` **초당 20건 · 845건/런**(Q9 §7) → 결과창을 덮는 모달(D-42).
|
||||||
|
//
|
||||||
|
// ■ 키를 어떻게 얻나 (전부 원본 실측 · 원본 수정 0)
|
||||||
|
// 1차 키 = 원본 `InGameInfo.Get_SkillPrefabPath(eEffect)` 와 **같은 규칙**(형식 문자열은 SO).
|
||||||
|
// 단 원본 `Add_SelectSkill` 스위치가 프리팹을 로드하지 않는 효과는 1차 키가 없다
|
||||||
|
// (`noPrefabEffects` · 예: `Select_Heal` · `Select_Shield` · `Select_*2` 계열).
|
||||||
|
// 추가 키 = 효과 클래스가 **코드 문자열로** 요구하는 키(`extraKeys` · 예 `ProjectileStar` · `SwordSlash`
|
||||||
|
// · `Select_Shield` → `Skill_Shield`). 에디트 모드 프로브가 grep 으로 실측해 SO 에 넣는다.
|
||||||
|
//
|
||||||
|
// ■ 존재 검사
|
||||||
|
// 런타임 = `Addressables.ResourceLocators` 를 순회하며 `Locate(key, GameObject)`(동기 · 비동기 0).
|
||||||
|
// 카탈로그가 아직 없으면 **검사를 건너뛴다**(모르면 빼지 않는다).
|
||||||
|
// 에디터 = 카탈로그가 없을 때 `AssetDatabase` 로 대신 본다(에디트 모드 프로브가 이 경로를 쓴다).
|
||||||
|
//
|
||||||
|
// ■ GC — 판정은 **ID 당 1회**만 하고 결과를 캐시한다. 2번째 뽑기부터는 문자열 0 · 할당 0.
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────
|
||||||
|
/// <summary>선택스킬 행이 참조하는 프리팹 키가 실제로 있는지 본다(결과 캐시).</summary>
|
||||||
|
public static class ZonePickupPool
|
||||||
|
{
|
||||||
|
/// <summary>검사한 행 수(누적 · 프로브가 읽는다).</summary>
|
||||||
|
public static int CheckedCount;
|
||||||
|
/// <summary>부재 키 때문에 뺀 행 수(누적).</summary>
|
||||||
|
public static int MissingKeyCount;
|
||||||
|
/// <summary>SO `excludeSkillIds` 로 뺀 행 수(누적).</summary>
|
||||||
|
public static int StaticExcludeCount;
|
||||||
|
/// <summary>카탈로그가 없어 키 검사를 건너뛴 횟수.</summary>
|
||||||
|
public static int SkippedCheckCount;
|
||||||
|
/// <summary>마지막 제외 사유(진단용).</summary>
|
||||||
|
public static string LastReason = "";
|
||||||
|
/// <summary>마지막으로 부재로 판정된 키.</summary>
|
||||||
|
public static string LastMissingKey = "";
|
||||||
|
|
||||||
|
/// <summary>ID → 사용 가능(true) / 제외(false). 판정 1회 뒤 재사용.</summary>
|
||||||
|
static readonly Dictionary<int, bool> s_ok = new Dictionary<int, bool>(64);
|
||||||
|
/// <summary>제외된 ID → 사유(로그·프로브용).</summary>
|
||||||
|
static readonly Dictionary<int, string> s_reason = new Dictionary<int, string>(8);
|
||||||
|
/// <summary>키 → 존재. 같은 키를 여러 행이 써도 1회만 본다.</summary>
|
||||||
|
static readonly Dictionary<string, bool> s_key = new Dictionary<string, bool>(64);
|
||||||
|
/// <summary>이미 로그를 남긴 ID(같은 줄을 반복하지 않는다).</summary>
|
||||||
|
static readonly HashSet<int> s_logged = new HashSet<int>();
|
||||||
|
|
||||||
|
/// <summary>프로브·런 경계용 — 캐시와 카운터를 비운다.</summary>
|
||||||
|
public static void ResetCache()
|
||||||
|
{
|
||||||
|
s_ok.Clear(); s_reason.Clear(); s_key.Clear(); s_logged.Clear();
|
||||||
|
CheckedCount = MissingKeyCount = StaticExcludeCount = SkippedCheckCount = 0;
|
||||||
|
LastReason = ""; LastMissingKey = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>이 ID 가 제외라면 사유, 아니면 "" (프로브가 읽는다).</summary>
|
||||||
|
public static string ReasonFor(int id)
|
||||||
|
{
|
||||||
|
string r;
|
||||||
|
return s_reason.TryGetValue(id, out r) ? r : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>검증 축이 살아 있는가(끄면 813o 동작 그대로).</summary>
|
||||||
|
public static bool Active(WLZonePickupSettings s) { return s != null && s.validatePool; }
|
||||||
|
|
||||||
|
/// <summary>이 행을 뽑기에서 빼야 하는가. `reason` 은 로그·프로브용(뺄 때만 채운다).</summary>
|
||||||
|
public static bool Excluded(WLZonePickupSettings s, SelectSkillTableData row, out string reason)
|
||||||
|
{
|
||||||
|
reason = null;
|
||||||
|
if (row == null || !Active(s)) return false;
|
||||||
|
|
||||||
|
bool ok;
|
||||||
|
if (s_ok.TryGetValue(row.ID, out ok))
|
||||||
|
{
|
||||||
|
if (ok) return false;
|
||||||
|
reason = ReasonFor(row.ID);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
CheckedCount++;
|
||||||
|
if (IsListed(s.excludeSkillIds, row.ID))
|
||||||
|
{
|
||||||
|
StaticExcludeCount++;
|
||||||
|
s_ok[row.ID] = false;
|
||||||
|
reason = "SO 제외 목록";
|
||||||
|
Remember(s, row.ID, reason, "");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
string missing;
|
||||||
|
if (s.runtimeKeyCheck && !KeysExist(s, row, out missing))
|
||||||
|
{
|
||||||
|
MissingKeyCount++;
|
||||||
|
s_ok[row.ID] = false;
|
||||||
|
reason = "참조 키 부재";
|
||||||
|
LastMissingKey = missing;
|
||||||
|
Remember(s, row.ID, reason, missing);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
s_ok[row.ID] = true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>이 행이 요구하는 키가 전부 있는가. 없으면 `missing` 에 첫 부재 키.</summary>
|
||||||
|
public static bool KeysExist(WLZonePickupSettings s, SelectSkillTableData row, out string missing)
|
||||||
|
{
|
||||||
|
missing = "";
|
||||||
|
if (s == null || row == null) return true;
|
||||||
|
|
||||||
|
string effect = row.SkillEffect.ToString();
|
||||||
|
|
||||||
|
// 1차 키 — 원본 Get_SkillPrefabPath 와 같은 규칙(형식은 SO).
|
||||||
|
if (row.SkillEffect != eEffect.None && !IsListed(s.noPrefabEffects, effect))
|
||||||
|
{
|
||||||
|
string fmt = effect.Contains("Select") ? s.selectSkillPathFormat
|
||||||
|
: effect.Contains("Skill") ? s.skillPathFormat
|
||||||
|
: s.effectPathFormat;
|
||||||
|
if (!string.IsNullOrEmpty(fmt) && !KeyExists(string.Format(fmt, effect)))
|
||||||
|
{ missing = string.Format(fmt, effect); return false; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 추가 키 — 효과 클래스가 코드 문자열로 요구하는 것들.
|
||||||
|
var extras = s.extraKeys;
|
||||||
|
if (extras != null)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < extras.Length; i++)
|
||||||
|
{
|
||||||
|
var x = extras[i];
|
||||||
|
if (x == null || string.IsNullOrEmpty(x.assetKey)) continue;
|
||||||
|
if (!string.Equals(x.skillEffect, effect)) continue;
|
||||||
|
if (!KeyExists(x.assetKey)) { missing = x.assetKey; return false; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Addressables 키 존재(동기 · 결과 캐시). 카탈로그가 없으면 true(모르면 빼지 않는다).</summary>
|
||||||
|
public static bool KeyExists(string key)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(key)) return true;
|
||||||
|
bool has;
|
||||||
|
if (s_key.TryGetValue(key, out has)) return has;
|
||||||
|
|
||||||
|
bool anyLocator = false;
|
||||||
|
has = false;
|
||||||
|
foreach (var loc in UnityEngine.AddressableAssets.Addressables.ResourceLocators)
|
||||||
|
{
|
||||||
|
if (loc == null) continue;
|
||||||
|
anyLocator = true;
|
||||||
|
System.Collections.Generic.IList<UnityEngine.ResourceManagement.ResourceLocations.IResourceLocation> found;
|
||||||
|
// 🔴 type = null(아무 타입이나) — 타입 불일치로 **있는 카드를 잘못 빼는 것**이 더 나쁜 실패다.
|
||||||
|
if (loc.Locate(key, null, out found) && found != null && found.Count > 0)
|
||||||
|
{ has = true; break; }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!anyLocator)
|
||||||
|
{
|
||||||
|
#if UNITY_EDITOR
|
||||||
|
// 에디트 모드·카탈로그 미초기화 — 에디터에서는 AssetDatabase 로 같은 것을 본다.
|
||||||
|
has = UnityEditor.AssetDatabase.LoadAssetAtPath<GameObject>(key) != null;
|
||||||
|
#else
|
||||||
|
SkippedCheckCount++;
|
||||||
|
return true; // 모르면 빼지 않는다(캐시도 하지 않는다)
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
s_key[key] = has;
|
||||||
|
return has;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void Remember(WLZonePickupSettings s, int id, string reason, string key)
|
||||||
|
{
|
||||||
|
LastReason = reason;
|
||||||
|
s_reason[id] = string.IsNullOrEmpty(key) ? reason : reason + ": " + key;
|
||||||
|
if (s == null || !s.logExclusion || !s_logged.Add(id)) return;
|
||||||
|
// 🔴 Debug.Log 만 쓴다(LogError/LogException = 원본 훅이 파란 모달로 만든다 · 813z 규칙).
|
||||||
|
Debug.Log("[ZonePickup] 카드 제외 — 행 " + id + " (" + s_reason[id] + ")");
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool IsListed(int[] arr, int v)
|
||||||
|
{
|
||||||
|
if (arr == null) return false;
|
||||||
|
for (int i = 0; i < arr.Length; i++) if (arr[i] == v) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool IsListed(string[] arr, string v)
|
||||||
|
{
|
||||||
|
if (arr == null) return false;
|
||||||
|
for (int i = 0; i < arr.Length; i++) if (string.Equals(arr[i], v)) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>존 클리어 픽업 카드(정적 · 노드는 런타임 생성 · 프리팹 diff 0).</summary>
|
/// <summary>존 클리어 픽업 카드(정적 · 노드는 런타임 생성 · 프리팹 diff 0).</summary>
|
||||||
public static class ZonePickup
|
public static class ZonePickup
|
||||||
{
|
{
|
||||||
|
|
@ -103,6 +299,9 @@ namespace WL.UI
|
||||||
GuideWaitCount, ReviveDeferCount, GiveUpCount, TimerWrites, PauseCalls;
|
GuideWaitCount, ReviveDeferCount, GiveUpCount, TimerWrites, PauseCalls;
|
||||||
/// <summary>WL-813c2(D-29) — 열린 카드의 타이머를 부활 때문에 멈춘/재개한 횟수와 누적 정지 시간(진단).</summary>
|
/// <summary>WL-813c2(D-29) — 열린 카드의 타이머를 부활 때문에 멈춘/재개한 횟수와 누적 정지 시간(진단).</summary>
|
||||||
public static int ReviveHoldCount, ReviveResumeCount, ReviveHoldTicks;
|
public static int ReviveHoldCount, ReviveResumeCount, ReviveHoldTicks;
|
||||||
|
/// <summary>WL-813o2(D-41) — 뽑기 결과에서 뺀 카드 수와 다시 뽑은 횟수(진단).</summary>
|
||||||
|
public static int ExcludedCount, RedrawCount;
|
||||||
|
public static string LastExcludeReason = "";
|
||||||
public static string LastLog = "";
|
public static string LastLog = "";
|
||||||
public static string LastSkipReason = "";
|
public static string LastSkipReason = "";
|
||||||
|
|
||||||
|
|
@ -370,7 +569,12 @@ namespace WL.UI
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>원본 뽑기 그대로 — `table_selectskill.Get_DataList(n)`(셔플·보유 제외·PreSkill 규칙).</summary>
|
/// <summary>
|
||||||
|
/// 원본 뽑기 그대로 — `table_selectskill.Get_DataList(n)`(셔플·보유 제외·PreSkill 규칙).
|
||||||
|
/// WL-813o2: 그 **결과만 후필터**해서 참조 프리팹이 없는 행을 뺀다(원본 수정 0).
|
||||||
|
/// 모자라면 원본 뽑기를 다시 부른다(최대 `maxRedraws` · 매번 새 셔플이라 분포는 그대로).
|
||||||
|
/// `validatePool = 0` 이면 시도 1회 · 제외 0 = 813o 동작과 완전히 같다.
|
||||||
|
/// </summary>
|
||||||
private static bool DrawCards(WLZonePickupSettings s)
|
private static bool DrawCards(WLZonePickupSettings s)
|
||||||
{
|
{
|
||||||
s_ids.Clear();
|
s_ids.Clear();
|
||||||
|
|
@ -378,17 +582,32 @@ namespace WL.UI
|
||||||
var t = table_selectskill.Ins;
|
var t = table_selectskill.Ins;
|
||||||
if (t == null || InGameInfo.Ins == null) return false;
|
if (t == null || InGameInfo.Ins == null) return false;
|
||||||
int want = Mathf.Max(1, s.cardCount);
|
int want = Mathf.Max(1, s.cardCount);
|
||||||
|
bool filter = ZonePickupPool.Active(s);
|
||||||
|
int tries = filter ? 1 + Mathf.Max(0, s.maxRedraws) : 1;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var list = t.Get_DataList(want);
|
for (int a = 0; a < tries && s_ids.Count < want; a++)
|
||||||
if (list == null || list.Count < want) return false;
|
|
||||||
for (int i = 0; i < list.Count; i++)
|
|
||||||
{
|
{
|
||||||
var row = list[i] as SelectSkillTableData;
|
if (a > 0) RedrawCount++;
|
||||||
if (row == null) return false;
|
var list = t.Get_DataList(want);
|
||||||
s_ids.Add(row.ID);
|
if (list == null) break;
|
||||||
s_rows.Add(row);
|
for (int i = 0; i < list.Count && s_ids.Count < want; i++)
|
||||||
|
{
|
||||||
|
var row = list[i] as SelectSkillTableData;
|
||||||
|
if (row == null) continue;
|
||||||
|
if (s_ids.Contains(row.ID)) continue; // 재뽑기 중복 방지
|
||||||
|
string why;
|
||||||
|
if (filter && ZonePickupPool.Excluded(s, row, out why))
|
||||||
|
{
|
||||||
|
ExcludedCount++;
|
||||||
|
LastExcludeReason = "행 " + row.ID + " — " + why;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
s_ids.Add(row.ID);
|
||||||
|
s_rows.Add(row);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
if (s_ids.Count < want) { s_ids.Clear(); s_rows.Clear(); return false; }
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
catch (System.Exception ex)
|
catch (System.Exception ex)
|
||||||
|
|
@ -795,11 +1014,22 @@ namespace WL.UI
|
||||||
ZoneClearedSeen = OpenCount = PickCount = TimeoutCount = SkipCount = 0;
|
ZoneClearedSeen = OpenCount = PickCount = TimeoutCount = SkipCount = 0;
|
||||||
GuideWaitCount = ReviveDeferCount = GiveUpCount = TimerWrites = PauseCalls = 0;
|
GuideWaitCount = ReviveDeferCount = GiveUpCount = TimerWrites = PauseCalls = 0;
|
||||||
ReviveHoldCount = ReviveResumeCount = ReviveHoldTicks = 0;
|
ReviveHoldCount = ReviveResumeCount = ReviveHoldTicks = 0;
|
||||||
|
ExcludedCount = RedrawCount = 0;
|
||||||
s_picksThisRun = 0;
|
s_picksThisRun = 0;
|
||||||
LastLog = ""; LastSkipReason = "";
|
LastLog = ""; LastSkipReason = ""; LastExcludeReason = "";
|
||||||
return "리셋";
|
return "리셋";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// WL-813o2 프로브 전용 — **뽑기만** 1회 돌린다(노드·표시·정지 0).
|
||||||
|
/// 결과는 `CardCount` / `CardId(i)` 로 읽는다. 게임 코드는 부르지 않는다.
|
||||||
|
/// </summary>
|
||||||
|
public static bool DrawForProbe()
|
||||||
|
{
|
||||||
|
var s = St;
|
||||||
|
return s != null && DrawCards(s);
|
||||||
|
}
|
||||||
|
|
||||||
public static string Teardown()
|
public static string Teardown()
|
||||||
{
|
{
|
||||||
Subscribe(false);
|
Subscribe(false);
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,38 @@ MonoBehaviour:
|
||||||
cardSortingOrder: 40
|
cardSortingOrder: 40
|
||||||
cardCount: 3
|
cardCount: 3
|
||||||
preferOriginalUi: 1
|
preferOriginalUi: 1
|
||||||
|
validatePool: 1
|
||||||
|
excludeSkillIds:
|
||||||
|
- 3
|
||||||
|
- 11
|
||||||
|
- 14
|
||||||
|
runtimeKeyCheck: 1
|
||||||
|
maxRedraws: 6
|
||||||
|
logExclusion: 1
|
||||||
|
selectSkillPathFormat: Assets/Res_Addr/SelectSkill/{0}.prefab
|
||||||
|
skillPathFormat: Assets/Res_Addr/Skill/{0}.prefab
|
||||||
|
effectPathFormat: Assets/Res_Addr/Effect/{0}.prefab
|
||||||
|
noPrefabEffects:
|
||||||
|
- Select_DoubleShot
|
||||||
|
- Select_TripleShot
|
||||||
|
- Select_Bounce
|
||||||
|
- Select_Bounce2
|
||||||
|
- Select_LeafFairy2
|
||||||
|
- Select_FrostFairy2
|
||||||
|
- Select_WindFairy2
|
||||||
|
- Select_HikikomoriFairy2
|
||||||
|
- Select_Heal
|
||||||
|
- Select_Shield
|
||||||
|
extraKeys:
|
||||||
|
- skillEffect: Select_Star
|
||||||
|
assetKey: Assets/Res_Addr/Projectile/ProjectileStar.prefab
|
||||||
|
source: 'Select_Star.cs:27 Shoot_Projectile("ProjectileStar")'
|
||||||
|
- skillEffect: Select_SwordSlash
|
||||||
|
assetKey: Assets/Res_Addr/Projectile/SwordSlash.prefab
|
||||||
|
source: 'Select_SwordSlash.cs:29 Shoot_Projectile("SwordSlash")'
|
||||||
|
- skillEffect: Select_Shield
|
||||||
|
assetKey: Assets/Res_Addr/Skill/Skill_Shield.prefab
|
||||||
|
source: 'InGameInfo2.cs:46 Set_Skill_ToPet(eEffect.Skill_Shield)'
|
||||||
pickTimeoutSeconds: 10
|
pickTimeoutSeconds: 10
|
||||||
timeoutUnscaled: 1
|
timeoutUnscaled: 1
|
||||||
autoPickFirstCard: 1
|
autoPickFirstCard: 1
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,24 @@ using UnityEngine;
|
||||||
|
|
||||||
namespace WL.UI
|
namespace WL.UI
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// WL-813o2 — 효과 클래스가 **코드 문자열로** 추가 요구하는 Addressables 키 1건.
|
||||||
|
/// 예: `Select_Star.cs:27 Shoot_Projectile("ProjectileStar", …)` → `Assets/Res_Addr/Projectile/ProjectileStar.prefab`.
|
||||||
|
/// 값(키 문자열)을 코드에 두지 않기 위해 SO 배열로 뺀다(C45).
|
||||||
|
/// </summary>
|
||||||
|
[System.Serializable]
|
||||||
|
public sealed class WLPickupExtraKey
|
||||||
|
{
|
||||||
|
[Tooltip("eEffect 이름(table_selectskill 의 SkillEffect 열과 같은 문자열).")]
|
||||||
|
public string skillEffect = "";
|
||||||
|
|
||||||
|
[Tooltip("그 효과가 추가로 요구하는 Addressables 키(에셋 경로 그대로).")]
|
||||||
|
public string assetKey = "";
|
||||||
|
|
||||||
|
[Tooltip("메모 — 어느 파일 몇 줄에서 나온 키인지(실측 근거).")]
|
||||||
|
public string source = "";
|
||||||
|
}
|
||||||
|
|
||||||
[CreateAssetMenu(fileName = "WLZonePickupSettings", menuName = "WL/Zone Pickup Settings", order = 34)]
|
[CreateAssetMenu(fileName = "WLZonePickupSettings", menuName = "WL/Zone Pickup Settings", order = 34)]
|
||||||
public sealed class WLZonePickupSettings : ScriptableObject
|
public sealed class WLZonePickupSettings : ScriptableObject
|
||||||
{
|
{
|
||||||
|
|
@ -122,6 +140,45 @@ namespace WL.UI
|
||||||
"🔴 실측(2026-09-09): 그 노드는 어떤 씬/프리팹에도 없다 = 항상 폴백.")]
|
"🔴 실측(2026-09-09): 그 노드는 어떤 씬/프리팹에도 없다 = 항상 폴백.")]
|
||||||
public bool preferOriginalUi = true;
|
public bool preferOriginalUi = true;
|
||||||
|
|
||||||
|
// ── WL-813o2 (Q9 D-41) — 참조 프리팹이 없는 카드 제외 ─────────────────
|
||||||
|
// 「별」(ID 14 · Select_Star)은 `Assets/Res_Addr/Projectile/ProjectileStar.prefab` 을
|
||||||
|
// 초당 20발 요구하는데 그 파일이 없다 → InvalidKeyException 845건/런 + 결과창 모달(D-42).
|
||||||
|
// 원본 `table_selectskill.Get_DataList(n)` 은 그대로 쓰고 **결과만 후필터**한다(원본 수정 0).
|
||||||
|
[Header("풀 검증 — 부재 프리팹 카드 제외 (WL-813o2 · Q9 D-41)")]
|
||||||
|
[Tooltip("끄면 813o 동작 그대로(제외 0 · 재뽑기 0 · 키 검사 0) = C8 롤백.")]
|
||||||
|
public bool validatePool = true;
|
||||||
|
|
||||||
|
[Tooltip("뽑기에서 뺄 table_selectskill 행 ID. 에디트 모드 풀 검증(AgentScripts/WL813o2_Probe)이 실측한 목록. " +
|
||||||
|
"비우면 정적 제외 0(런타임 키 검사만 남는다).")]
|
||||||
|
public int[] excludeSkillIds = new int[] { 3, 11, 14 };
|
||||||
|
|
||||||
|
[Tooltip("런타임에도 카드마다 참조 키가 Addressables 에 있는지 검사해 없으면 뺀다(ID 당 1회 · 결과 캐시 · GC 0). " +
|
||||||
|
"카탈로그가 아직 없으면 검사를 건너뛴다(모르면 빼지 않는다).")]
|
||||||
|
public bool runtimeKeyCheck = true;
|
||||||
|
|
||||||
|
[Tooltip("제외 때문에 카드가 모자라면 원본 뽑기를 다시 부르는 최대 횟수. 0 = 재뽑기 없음.")]
|
||||||
|
public int maxRedraws = 6;
|
||||||
|
|
||||||
|
[Tooltip("제외·부재 키를 Console 에 1줄 남긴다(같은 ID 는 1회만 · Error 아님 = 모달 유발 0).")]
|
||||||
|
public bool logExclusion = true;
|
||||||
|
|
||||||
|
[Tooltip("원본 InGameInfo.Get_SkillPrefabPath 와 같은 규칙 — eEffect 이름에 'Select' 가 있으면 이 형식.")]
|
||||||
|
public string selectSkillPathFormat = "Assets/Res_Addr/SelectSkill/{0}.prefab";
|
||||||
|
|
||||||
|
[Tooltip("원본 규칙 — eEffect 이름에 'Skill' 이 있으면 이 형식.")]
|
||||||
|
public string skillPathFormat = "Assets/Res_Addr/Skill/{0}.prefab";
|
||||||
|
|
||||||
|
[Tooltip("원본 규칙 — 그 외에는 이 형식.")]
|
||||||
|
public string effectPathFormat = "Assets/Res_Addr/Effect/{0}.prefab";
|
||||||
|
|
||||||
|
[Tooltip("원본 InGameInfo.Add_SelectSkill 스위치가 **프리팹을 로드하지 않는** eEffect 이름들(실측). " +
|
||||||
|
"이 목록의 행은 1차 키를 만들지 않는다(없는 프리팹으로 오판하지 않기 위해).")]
|
||||||
|
public string[] noPrefabEffects = new string[0];
|
||||||
|
|
||||||
|
[Tooltip("원본 효과 클래스(Select_*.cs)가 코드 문자열로 추가 요구하는 키(실측 grep). " +
|
||||||
|
"1차 키와 별개로 이것들도 존재 검사한다.")]
|
||||||
|
public WLPickupExtraKey[] extraKeys = new WLPickupExtraKey[0];
|
||||||
|
|
||||||
// ── 타임아웃 ──────────────────────────────────────────────────────────
|
// ── 타임아웃 ──────────────────────────────────────────────────────────
|
||||||
[Header("타임아웃 — 자동 선택")]
|
[Header("타임아웃 — 자동 선택")]
|
||||||
[Tooltip("카드가 열린 뒤 자동 선택까지의 시간(초). 발주서 §1-2 = 10s.")]
|
[Tooltip("카드가 열린 뒤 자동 선택까지의 시간(초). 발주서 §1-2 = 10s.")]
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue