Project_WL/Assets/WL/Look/Farm/WLIslandLook.cs

521 lines
27 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// ─────────────────────────────────────────────────────────────────────────────
// WLIslandLook.cs — **실제 게임이 로드하는 섬 씬**(FarmingIsland/Scenes/Level01)에
// 데모 룩과 풀밭을 런타임으로 얹는다 (WL-816e · #816)
//
// ■ 왜 필요한가(발주서 §0)
// 816a 는 복사본 씬 `Assets/WL/Look/Farm/Scenes/WL_FarmLook.unity` 에만 룩을 저장했다.
// 게임은 `Level01` 을 로드하므로 PD 캡처의 섬은 여전히 **원본 색**(쨍한 파란 하늘·원색 초록)이었다.
// → 원본 씬을 고치지 않고(`Assets/FarmingIsland/**` 0줄) **로드 직후** 얹는다.
//
// ■ 걸리는 지점
// `[RuntimeInitializeOnLoadMethod]` + `SceneManager.sceneLoaded` — 기존 파일 **0줄 수정**.
// (816d 의 `WLIslandBridge` 를 고치지 않는다 = 다른 worktree 와 충돌 0)
//
// ■ C8 되돌리기 — `WLIslandLookSettings.enabled_ = 0` → 아무것도 얹지 않는다.
// 씬·머티리얼·프리팹 **파일을 쓰지 않으므로** 복원 작업 자체가 없다.
//
// 🔴 Debug.LogError / Debug.LogException 을 쓰지 않는다(ErrorLogHookManager 팝업).
// ─────────────────────────────────────────────────────────────────────────────
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.SceneManagement;
using CryingSnow.FarmingIsland;
using FIIsland = CryingSnow.FarmingIsland.Island;
using FISoil = CryingSnow.FarmingIsland.Soil;
using FIFarm = CryingSnow.FarmingIsland.Farm;
namespace WL.Look.Farm
{
/// <summary>섬 씬에 룩·풀밭을 얹는 정적 진입점.</summary>
public static class WLIslandLook
{
public const string RunnerName = "~WLIslandLookRunner";
public const string ReferenceLookName = "WL_ReferenceLook";
// ── 진단 ────────────────────────────────────────────────────────
public static int SwappedRenderers, SwappedSlots, Rescans, LightingApplied, GrassRebuilds;
public static bool LookApplied, ReferenceLookApplied, GrassSpawned;
public static string LastLog = "";
static bool s_installed;
static WLIslandLookRunner s_runner;
static readonly HashSet<Renderer> s_done = new HashSet<Renderer>();
static readonly HashSet<FIIsland> s_hooked = new HashSet<FIIsland>();
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)]
public static void Install()
{
if (s_installed) return;
var cfg = WLIslandLookSettings.Instance;
if (cfg == null || cfg.enabled_ == 0) return;
s_installed = true;
SceneManager.sceneLoaded += OnSceneLoaded;
SceneManager.sceneUnloaded += OnSceneUnloaded;
for (int i = 0; i < SceneManager.sceneCount; i++)
{
var sc = SceneManager.GetSceneAt(i);
if (sc.isLoaded) OnSceneLoaded(sc, LoadSceneMode.Additive);
}
}
/// <summary>에디터 프로브가 Play 중에 다시 걸 때 쓴다.</summary>
public static void ForceApply(Scene scene)
{
var cfg = WLIslandLookSettings.Instance;
if (cfg == null) return;
s_done.Clear();
EnsureRunner();
s_runner.StartCoroutine(Co_Apply(cfg, scene));
}
static void OnSceneLoaded(Scene scene, LoadSceneMode mode)
{
var cfg = WLIslandLookSettings.Instance;
if (cfg == null || cfg.enabled_ == 0) return;
if (!IsIslandScene(cfg, scene)) return;
EnsureRunner();
s_runner.StartCoroutine(Co_Apply(cfg, scene));
}
static void OnSceneUnloaded(Scene scene)
{
s_done.Clear();
s_hooked.Clear();
s_tonedFarms.Clear();
LookApplied = false; GrassSpawned = false; ReferenceLookApplied = false;
}
static bool IsIslandScene(WLIslandLookSettings cfg, Scene scene)
{
if (!scene.IsValid() || !scene.isLoaded) return false;
if (cfg.islandSceneNames != null)
for (int i = 0; i < cfg.islandSceneNames.Length; i++)
if (scene.name == cfg.islandSceneNames[i]) return true;
// 이름을 몰라도 IslandManager 가 있으면 섬이다.
var roots = scene.GetRootGameObjects();
for (int i = 0; i < roots.Length; i++)
if (roots[i].GetComponentInChildren<IslandManager>(true) != null) return true;
return false;
}
static void EnsureRunner()
{
if (s_runner != null) return;
var go = new GameObject(RunnerName);
go.hideFlags = HideFlags.DontSave;
Object.DontDestroyOnLoad(go);
s_runner = go.AddComponent<WLIslandLookRunner>();
}
// ─────────────────────────────────────────────────────────────────
// 본체
// ─────────────────────────────────────────────────────────────────
static IEnumerator Co_Apply(WLIslandLookSettings cfg, Scene scene)
{
yield return null; // FI 매니저들의 Awake 를 기다린다
if (cfg.disableFiGraphicsManager != 0) DisableGraphicsManager(scene);
if (cfg.applyLook != 0)
{
SwapMaterials(cfg, scene);
LookApplied = true;
}
if (cfg.applyLighting != 0) ApplyLighting(cfg, scene);
if (cfg.applyLook != 0 && cfg.applyReferenceLook != 0) ApplyReferenceLook(cfg, scene);
if (cfg.soilToneEnabled != 0) ToneSoil(cfg, scene);
if (cfg.grassEnabled != 0) SpawnGrass(cfg, scene);
if (cfg.shoreFoamEnabled != 0) SpawnShoreFoam(cfg, scene);
HookIslands(cfg, scene);
cfg.Log("섬 룩 적용 — 렌더러 " + SwappedRenderers + " · 슬롯 " + SwappedSlots
+ " · 조명 " + (cfg.applyLighting != 0 ? "데모" : "원본")
+ " · ReferenceLook " + (ReferenceLookApplied ? "on" : "off")
+ " · 풀 " + WLIslandGrass.LastLog
+ " · 둘레거품 " + WLShoreFoam.LastLog);
// 늦게 생기는 오브젝트(상인·작물·아이템)까지 다시 훑는다
if (cfg.rescanAtSeconds != null)
{
float prev = 0f;
for (int i = 0; i < cfg.rescanAtSeconds.Length; i++)
{
float w = cfg.rescanAtSeconds[i] - prev;
prev = cfg.rescanAtSeconds[i];
if (w > 0f) yield return new WaitForSeconds(w);
if (!scene.isLoaded) yield break;
if (cfg.applyLook != 0) { SwapMaterials(cfg, scene); Rescans++; }
// 🔴 816f 실측 — PD 실행 경로(Additive)에서 `RenderSettings` 는 **활성 씬(InGame)**
// 소유이고, 그 씬에는 조명을 덮어쓰는 코드(`MapData`)가 없다(실측).
// 그래서 조명은 재훑기에서 **다시 걸지 않는다** — 다시 걸면 `WLReferenceLook` 의
// 무드(Flat)를 Skybox 로 되돌려 오히려 룩이 바뀐다(실측으로 확인).
if (cfg.soilToneEnabled != 0) ToneSoil(cfg, scene);
HookIslands(cfg, scene);
}
}
// 섬 확장 감시 — 새로 열린 타일에도 자동으로 풀이 깔린다
if (cfg.rebuildPollSeconds > 0f)
{
int last = CountUnlocked(scene);
float nextCloud = Time.time + Mathf.Max(0f, cfg.cloudEdgeRefreshSeconds);
float nextView = Time.time + Mathf.Max(0f, cfg.viewPollSeconds);
float nextPoll = Time.time + cfg.rebuildPollSeconds;
while (scene.isLoaded)
{
// 816q — 카메라 감시는 확장 감시(1 s)보다 촘촘해야 해서 따로 돈다
float wait = cfg.viewCullEnabled != 0 && cfg.viewPollSeconds > 0f
? Mathf.Min(cfg.rebuildPollSeconds, cfg.viewPollSeconds) : cfg.rebuildPollSeconds;
yield return new WaitForSeconds(wait);
if (!scene.isLoaded) yield break;
if (cfg.viewCullEnabled != 0 && cfg.viewPollSeconds > 0f && Time.time >= nextView)
{
nextView = Time.time + cfg.viewPollSeconds;
var gv = Object.FindFirstObjectByType<WLIslandGrass>(FindObjectsInactive.Include);
if (gv != null && gv.ViewMovedEnough()) gv.RefreshView();
}
if (Time.time < nextPoll) continue;
nextPoll = Time.time + cfg.rebuildPollSeconds;
HookIslands(cfg, scene);
int now = CountUnlocked(scene);
if (now != last)
{
last = now;
if (cfg.applyLook != 0) SwapMaterials(cfg, scene);
RequestGrassRebuild(cfg);
nextCloud = Time.time + Mathf.Max(0f, cfg.cloudEdgeRefreshSeconds);
continue;
}
// 816o — 구름이 흐른 만큼 띠를 다시 고른다(타일·제외 사각형은 다시 훑지 않는다)
if (cfg.cloudEdgeEnabled != 0 && cfg.cloudEdgeRefreshSeconds > 0f && Time.time >= nextCloud)
{
nextCloud = Time.time + cfg.cloudEdgeRefreshSeconds;
var g = Object.FindFirstObjectByType<WLIslandGrass>(FindObjectsInactive.Include);
if (g != null) g.RefreshCloudEdges();
}
}
}
}
static int CountUnlocked(Scene scene)
{
var islands = Object.FindObjectsByType<FIIsland>(FindObjectsInactive.Exclude, FindObjectsSortMode.None);
int n = 0;
for (int i = 0; i < islands.Length; i++)
if (islands[i] != null && islands[i].IsUnlocked && islands[i].gameObject.activeInHierarchy) n++;
return n;
}
/// <summary>FI `FIIsland.OnActivated`(public event)에 붙는다 — FI 코드 0줄.</summary>
static void HookIslands(WLIslandLookSettings cfg, Scene scene)
{
var islands = Object.FindObjectsByType<FIIsland>(FindObjectsInactive.Include, FindObjectsSortMode.None);
for (int i = 0; i < islands.Length; i++)
{
var isl = islands[i];
if (isl == null || isl.gameObject.scene != scene) continue;
if (s_hooked.Contains(isl)) continue;
s_hooked.Add(isl);
isl.OnActivated += () => OnIslandActivated(cfg, isl);
}
}
static void OnIslandActivated(WLIslandLookSettings cfg, FIIsland isl)
{
if (cfg == null || cfg.enabled_ == 0) return;
EnsureRunner();
s_runner.StartCoroutine(Co_AfterActivate(cfg, isl));
}
static IEnumerator Co_AfterActivate(WLIslandLookSettings cfg, FIIsland isl)
{
// 확장 애니메이션(DOTween DOScale 1초) + 소품 배치가 끝나기를 기다린다
yield return new WaitForSeconds(cfg.rebuildDelaySeconds);
if (isl == null) yield break;
if (cfg.applyLook != 0) SwapMaterials(cfg, isl.gameObject.scene);
RequestGrassRebuild(cfg);
}
static void RequestGrassRebuild(WLIslandLookSettings cfg)
{
if (cfg.grassEnabled != 0)
{
var g = Object.FindFirstObjectByType<WLIslandGrass>(FindObjectsInactive.Include);
if (g != null) { g.Rebuild(); GrassRebuilds++; }
}
// 🔴 816j2 — 섬이 확장되면 **둘레 거품 띠도 같은 훅으로** 다시 만든다(새 가장자리 자동).
if (cfg.shoreFoamEnabled != 0)
{
var f = Object.FindFirstObjectByType<WLShoreFoam>(FindObjectsInactive.Include);
if (f != null) { f.cfg = cfg; f.Rebuild(); ShoreFoamRebuilds++; }
}
}
public static int ShoreFoamRebuilds;
public static bool ShoreFoamSpawned;
/// <summary>섬 둘레 거품 띠 — 풀과 같은 자리에서 만들고 같은 훅으로 다시 만든다(816j2).</summary>
public static void SpawnShoreFoam(WLIslandLookSettings cfg, Scene scene)
{
var f = Object.FindFirstObjectByType<WLShoreFoam>(FindObjectsInactive.Include);
if (f == null)
{
var go = new GameObject(WLShoreFoam.ObjectName);
SceneManager.MoveGameObjectToScene(go, scene);
go.transform.SetPositionAndRotation(Vector3.zero, Quaternion.identity);
f = go.AddComponent<WLShoreFoam>();
f.cfg = cfg;
f.Rebuild(); // OnEnable 이 cfg 전에 돌 수 있다
}
else { f.cfg = cfg; f.Rebuild(); }
ShoreFoamSpawned = true;
}
// ─────────────────────────────────────────────────────────────────
// ① 머티리얼 — 원본을 **읽기만** 하고 렌더러의 sharedMaterials 만 바꾼다
// ─────────────────────────────────────────────────────────────────
public static void SwapMaterials(WLIslandLookSettings cfg, Scene scene)
{
if (cfg.materialRemap == null || cfg.materialRemap.Length == 0) return;
var rends = Object.FindObjectsByType<Renderer>(FindObjectsInactive.Include, FindObjectsSortMode.None);
for (int i = 0; i < rends.Length; i++)
{
var r = rends[i];
if (r == null || r.gameObject.scene != scene) continue;
if (s_done.Contains(r)) continue;
if (r is ParticleSystemRenderer) continue;
// 🔴 농사 타일은 코드가 `material.color` 로 직접 색을 칠한다(Soil.Initialize).
// Toon 셰이더에는 그 프로퍼티가 없어 갈아끼우면 밭 색이 죽는다 → 원본 유지.
if (cfg.skipSoilRenderers != 0 && r.GetComponent<FISoil>() != null) { s_done.Add(r); continue; }
bool isIslandBody = r.GetComponent<FIIsland>() != null;
var mats = r.sharedMaterials;
bool changed = false;
for (int m = 0; m < mats.Length; m++)
{
var src = mats[m];
if (src == null) continue;
Material dst = null;
if (isIslandBody && cfg.islandTopMaterial != null) dst = cfg.islandTopMaterial;
else dst = Lookup(cfg, src);
if (dst == null || dst == src) continue;
mats[m] = dst; changed = true; SwappedSlots++;
}
if (changed) { r.sharedMaterials = mats; SwappedRenderers++; }
s_done.Add(r);
}
}
/// <summary>
/// 🔴 816h — 섬 둘레의 바다(`Water` · 1000×1000 평면)를 **WL 워터 셰이더**로.
/// `Assets/FarmingIsland/**` 는 0줄 — 렌더러의 `sharedMaterials` 만 런타임에 바꾼다.
/// `waterMode = 0` 이면 이 판정 자체를 건너뛰어 지금(FarmingIsland 물)으로 100 % 돌아간다.
/// </summary>
static bool IsWater(WLIslandLookSettings cfg, Material src)
{
if (src == null) return false;
if (cfg.waterFrom != null && src == cfg.waterFrom) return true;
if (src.shader != null && src.shader.name == "Shader Graphs/Simple Water") return true;
// 이미 우리 복사본(Farm_SimpleWater_Demo)으로 바뀐 뒤 다시 훑는 경우
return src.name.StartsWith("Farm_SimpleWater");
}
public static int WaterSwapped;
static Material Lookup(WLIslandLookSettings cfg, Material src)
{
if (cfg.waterMode != 0 && cfg.waterTo != null && IsWater(cfg, src))
{
if (src != cfg.waterTo) WaterSwapped++;
return cfg.waterTo;
}
for (int i = 0; i < cfg.materialRemap.Length; i++)
{
var e = cfg.materialRemap[i];
if (e == null || e.enabled_ == 0 || e.from == null || e.to == null) continue;
if (e.from == src) return e.to;
// 런타임 인스턴스("Palette (Instance)")도 잡는다
if (src.name.StartsWith(e.from.name) && src.shader == e.from.shader) return e.to;
}
return null;
}
// ─────────────────────────────────────────────────────────────────
// ② 조명 · 앰비언트 · 스카이박스 (816a 실측 = 데모/아레나 값)
// ─────────────────────────────────────────────────────────────────
public static void ApplyLighting(WLIslandLookSettings cfg, Scene scene)
{
// 🔴 816h — 하늘(앰비언트·스카이박스·안개·반사)은 **활성 씬(InGame)** 소유라
// 여기서 그냥 쓰면 섬에서 나간 뒤 전투 맵·로비까지 그대로 남는다.
// 그래서 `WLSkyScope` 가 **스냅샷 → 적용 → 나갈 때 원복** 을 맡는다.
// (값은 그대로 이 SO 것 = 아레나/데모 값. 새 값 0)
if (cfg.skyScopeEnabled != 0) WLSkyScope.Acquire(cfg, scene.name);
else
{
RenderSettings.ambientMode = AmbientMode.Skybox;
RenderSettings.ambientSkyColor = cfg.ambientSky;
RenderSettings.ambientEquatorColor = cfg.ambientEquator;
RenderSettings.ambientGroundColor = cfg.ambientGround;
RenderSettings.ambientLight = cfg.ambientSky;
RenderSettings.ambientIntensity = cfg.ambientIntensity;
if (cfg.fogOff != 0) RenderSettings.fog = false;
if (cfg.skybox != null) RenderSettings.skybox = cfg.skybox;
RenderSettings.defaultReflectionMode = DefaultReflectionMode.Skybox;
}
var lights = Object.FindObjectsByType<Light>(FindObjectsInactive.Exclude, FindObjectsSortMode.None);
for (int i = 0; i < lights.Length; i++)
{
var l = lights[i];
if (l == null || l.gameObject.scene != scene) continue;
if (l.type != LightType.Directional) continue;
l.color = cfg.dirLightColor;
l.intensity = cfg.dirLightIntensity;
if (cfg.dirLightSoftShadows != 0 && l.shadows != LightShadows.None) l.shadows = LightShadows.Soft;
RenderSettings.sun = l;
LightingApplied++;
}
}
// ─────────────────────────────────────────────────────────────────
// ②-b 밭(Soil) 칸 표시 — **기능은 그대로 두고 색만** 데모 톤으로 (816f · PD 지시)
// FI `Farm` 의 4색(마름1·마름2·젖음1·젖음2)을 런타임에 낮추고
// `Soil.Initialize(farm)`(public) 를 다시 불러 칠만 갱신한다.
// → 물주기 피드백(마름↔젖음)·칸 구분은 전부 살아 있다. `Assets/FarmingIsland/**` 0줄.
// ─────────────────────────────────────────────────────────────────
static readonly string[] s_soilFields = { "soilDryColor1", "soilDryColor2", "soilWetColor1", "soilWetColor2" };
static readonly HashSet<FIFarm> s_tonedFarms = new HashSet<FIFarm>();
public static int SoilFarmsToned, SoilTilesRepainted;
public static void ToneSoil(WLIslandLookSettings cfg, Scene scene)
{
var farms = Object.FindObjectsByType<FIFarm>(FindObjectsInactive.Include, FindObjectsSortMode.None);
var t = typeof(FIFarm);
for (int i = 0; i < farms.Length; i++)
{
var farm = farms[i];
if (farm == null || farm.gameObject.scene != scene) continue;
if (s_tonedFarms.Contains(farm)) continue;
// 1) 마름1/마름2 · 젖음1/젖음2 각각의 평균으로 모으고(대비 축소) 틴트를 곱한다
var vals = new Color[4];
var fis = new System.Reflection.FieldInfo[4];
bool ok = true;
for (int k = 0; k < 4; k++)
{
fis[k] = t.GetField(s_soilFields[k],
System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic);
if (fis[k] == null) { ok = false; break; }
vals[k] = (Color)fis[k].GetValue(farm);
}
if (!ok) continue;
Apply(cfg, fis, farm, vals, 0, 1); // 마름 한 쌍
Apply(cfg, fis, farm, vals, 2, 3); // 젖음 한 쌍
s_tonedFarms.Add(farm);
SoilFarmsToned++;
// 2) 이미 칠해진 타일을 다시 칠한다 — FI 의 public 진입점 그대로
var soils = farm.GetComponentsInChildren<FISoil>(true);
for (int s = 0; s < soils.Length; s++)
{
if (soils[s] == null) continue;
try { soils[s].Initialize(farm); SoilTilesRepainted++; } catch { }
}
}
}
static void Apply(WLIslandLookSettings cfg, System.Reflection.FieldInfo[] fis, FIFarm farm, Color[] v, int a, int b)
{
var mid = (v[a] + v[b]) * 0.5f;
float c = cfg.soilCheckerContrast;
var ca = Color.Lerp(mid, v[a], c);
var cb = Color.Lerp(mid, v[b], c);
fis[a].SetValue(farm, Mul(ca, cfg.soilTint));
fis[b].SetValue(farm, Mul(cb, cfg.soilTint));
}
static Color Mul(Color a, Color b) { return new Color(a.r * b.r, a.g * b.g, a.b * b.b, a.a); }
// ─────────────────────────────────────────────────────────────────
// ③ 아레나와 같은 외곽선·대비·무드 (씬 비의존 — 816a 실측)
// ─────────────────────────────────────────────────────────────────
public static void ApplyReferenceLook(WLIslandLookSettings cfg, Scene scene)
{
var existing = Object.FindFirstObjectByType<WL.Look.Arena.WLReferenceLook>(FindObjectsInactive.Include);
if (existing != null)
{
if (!existing.gameObject.activeSelf) existing.gameObject.SetActive(true);
ReferenceLookApplied = true;
return;
}
var go = new GameObject(ReferenceLookName);
SceneManager.MoveGameObjectToScene(go, scene);
go.AddComponent<WL.Look.Arena.WLReferenceLook>(); // OnEnable 이 적용한다
ReferenceLookApplied = true;
}
// ─────────────────────────────────────────────────────────────────
// ④ 풀밭
// ─────────────────────────────────────────────────────────────────
public static void SpawnGrass(WLIslandLookSettings cfg, Scene scene)
{
var g = Object.FindFirstObjectByType<WLIslandGrass>(FindObjectsInactive.Include);
if (g == null)
{
var go = new GameObject(WLIslandGrass.ObjectName);
SceneManager.MoveGameObjectToScene(go, scene);
go.transform.SetPositionAndRotation(Vector3.zero, Quaternion.identity);
g = go.AddComponent<WLIslandGrass>(); // OnEnable 이 만든다
g.cfg = cfg;
}
else
{
g.cfg = cfg;
g.Rebuild();
}
GrassSpawned = true;
}
// ─────────────────────────────────────────────────────────────────
// ⑤ FI GraphicsManager — Play 중 FI 의 URP 에셋 **파일**을 고친다(816a 실측)
// 우리 활성 URP 가 아니라 화면 영향 0. `Assets/FarmingIsland/**` 0줄을 지키려면 꺼야 한다.
// ─────────────────────────────────────────────────────────────────
static void DisableGraphicsManager(Scene scene)
{
var roots = scene.GetRootGameObjects();
for (int i = 0; i < roots.Length; i++)
{
var comps = roots[i].GetComponentsInChildren<MonoBehaviour>(true);
for (int c = 0; c < comps.Length; c++)
{
if (comps[c] == null) continue;
if (comps[c].GetType().Name != "GraphicsManager") continue;
comps[c].enabled = false;
comps[c].gameObject.SetActive(false);
}
}
}
}
/// <summary>코루틴 숙주. 씬을 넘어 살아남는다.</summary>
public sealed class WLIslandLookRunner : MonoBehaviour { }
}