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

375 lines
22 KiB
C#
Raw Normal View History

// ─────────────────────────────────────────────────────────────────────────────
// WLIslandLookSettings.cs — 실제 게임 섬(FarmingIsland Level01)에 **런타임으로** 얹는
// ① 데모 룩(머티리얼·조명) ② 데모 풀밭의 단일 출처 (WL-816e · #816)
//
// PD 지시(2026-09-13) 「섬 배경이 단순 타일로만 되어있어. 기본 타일의 디자인을
// 데모씬에서 사용한 풀밭처럼 꾸미고, 전체적인 비주얼을 맞춰줘」
//
// ■ 왜 런타임인가 — 816a 는 **복사본 씬**(WL_FarmLook)에만 룩을 저장했다.
// 게임이 실제로 로드하는 것은 `Assets/FarmingIsland/Scenes/Level01.unity` 라
// 그 씬은 아직 원본 색이다. 원본 씬을 고치지 않고(FI 0줄) 로드 직후 얹는다.
//
// ■ C8 되돌리기 — `enabled_ = 0` 이면 아무것도 얹지 않는다 = 지금 상태 100 %.
// 씬 파일·FI 에셋을 한 글자도 쓰지 않으므로 되돌리기에 복원 작업이 필요 없다.
//
// ■ C45 — 밀도·크기·제외 여백 같은 값은 전부 이 에셋에 있다(코드 상수 0).
//
// 🔴 Debug.LogError / Debug.LogException 을 쓰지 않는다(ErrorLogHookManager 팝업).
// 🔴 어셈블리 주의: Assets/WL/Look/ 에 .asmdef 를 만들지 말 것(Assembly-CSharp 단일).
// ─────────────────────────────────────────────────────────────────────────────
using System;
using UnityEngine;
namespace WL.Look.Farm
{
/// <summary>원본 머티리얼 → 데모 톤 복사본. 한 줄 = 머티리얼 하나.</summary>
[Serializable]
public sealed class WLMatRemap
{
[Tooltip("0 이면 이 줄은 없는 것으로 친다.")]
public int enabled_ = 1;
[Tooltip("FarmingIsland 원본 머티리얼(한 글자도 안 고친다 · 참조만).")]
public Material from;
[Tooltip("816e/816a 가 만든 데모 톤 복사본.")]
public Material to;
}
/// <summary>
/// 메시 하나의 **윗면(걸어다니는 면)** 을 XZ 격자 비트맵으로 구운 것.
/// FI 메시는 `isReadable = 0` 이라 런타임에 정점을 못 읽는다 → 에디터에서 구워 둔다.
/// (FI 에셋의 임포트 설정을 고치지 않기 위한 선택 — `Assets/FarmingIsland/**` 0줄)
/// </summary>
[Serializable]
public sealed class WLTileMask
{
public string meshName = "";
[Tooltip("격자 해상도(res × res).")] public int res = 64;
[Tooltip("마스크가 덮는 반폭(m). 섬 타일 8 m → 4.")] public float half = 4f;
[Tooltip("res*res 비트를 담은 패킹 배열.")] public byte[] bits;
public bool At(float lx, float lz)
{
if (bits == null || res <= 0) return false;
int cx = (int)((lx + half) / (half * 2f) * res);
int cz = (int)((lz + half) / (half * 2f) * res);
if (cx < 0 || cz < 0 || cx >= res || cz >= res) return false;
int i = cz * res + cx;
int b = i >> 3;
if (b >= bits.Length) return false;
return (bits[b] & (1 << (i & 7))) != 0;
}
public void Set(int cx, int cz, bool v)
{
if (bits == null) bits = new byte[(res * res + 7) / 8];
int i = cz * res + cx;
int b = i >> 3;
if (b >= bits.Length) return;
if (v) bits[b] |= (byte)(1 << (i & 7));
else bits[b] &= (byte)~(1 << (i & 7));
}
}
/// <summary>흩뿌릴 것 하나(풀 · 꽃 · 자갈). 데모 `InstancingSettings` 와 같은 값 구성.</summary>
[Serializable]
public sealed class WLScatterDef
{
public int enabled_ = 1;
[Tooltip("이름표(로그용).")] public string label = "풀";
public Mesh mesh;
public Material material;
[Tooltip("같은 층 안에서의 가중치. 데모 = 풀 100 : 꽃 1.")] public float probability = 100f;
[Tooltip("메시 배율. 데모 = 0.35.")] public float scale = 0.35f;
[Tooltip("법선 방향으로 띄우는 거리(m). 데모 = 0.04.")] public float normalOffset = 0.04f;
[Tooltip("이 층의 밀도(개/m 한 변). 실제 밀도 = 값². 데모 1층 = 2.5(6.25개/㎡).")]
public float density = 2.5f;
}
[CreateAssetMenu(fileName = "WLIslandLookSettings", menuName = "WL/Island Look Settings", order = 39)]
public sealed class WLIslandLookSettings : ScriptableObject
{
public const string ResourcesPath = "WL/WLIslandLookSettings";
static WLIslandLookSettings s_inst;
static bool s_tried;
public static WLIslandLookSettings Instance
{
get
{
if (s_inst == null && !s_tried) { s_tried = true; s_inst = Resources.Load<WLIslandLookSettings>(ResourcesPath); }
return s_inst;
}
}
public static void Invalidate() { s_inst = null; s_tried = false; }
// ───────────────────────────────── 마스터
[Header("되돌리기 (C8)")]
[Tooltip("0 이면 섬에 아무것도 얹지 않는다 = 지금(원본) 상태 100 %.")]
public int enabled_ = 1;
[Tooltip("1 이면 적용 결과를 Console 에 남긴다.")]
public int verboseLog = 1;
// ───────────────────────────────── §0 룩
[Header("§0 — 실게임 섬(Level01)에 데모 룩 적용")]
[Tooltip("1 이면 섬 씬 로드 직후 머티리얼을 데모 톤 복사본으로 바꾼다(씬 파일 무변경 · 런타임만).")]
public int applyLook = 1;
[Tooltip("1 이면 조명·앰비언트·스카이박스도 데모/아레나 값으로 맞춘다.")]
public int applyLighting = 1;
[Tooltip("1 이면 아레나와 같은 `WL_ReferenceLook`(외곽선·대비·무드)을 섬에도 얹는다.")]
public int applyReferenceLook = 1;
[Tooltip("머티리얼 교체 표. 한 줄 = 원본 → 복사본.")]
public WLMatRemap[] materialRemap = new WLMatRemap[0];
[Tooltip("섬 타일 윗면(걸어다니는 면)에 쓸 머티리얼. 비우면 위 표의 Island01 줄을 쓴다. " +
"체크무늬 두 초록을 데모 잔디색 한 가지로 합친 것.")]
public Material islandTopMaterial;
[Tooltip("1 이면 농사 타일(Soil)은 원본 머티리얼 그대로 둔다(색을 코드가 직접 칠하는 곳이라 건드리면 안 된다).")]
public int skipSoilRenderers = 1;
[WL-816f] 바닥을 데모와 일치 — 잔디 단색 + 풀 대비↓ + 밀도 2.5→1.8 + 밭 칸 표시 톤↓ (#816) PD 「내가 실행할 때는 전혀 풀밭이 아니야 · 바닥이 완전히 같은 느낌이 나도록」 ■ ① PD 실행 경로 실측 = 훅은 실제로 돈다(버그 아님) InGame 활성 + Level01 Additive 재현: 머티리얼 1,575 렌더러 교체 · 풀 생성 · ReferenceLook on · 콘솔 error 0. 「안 나온다」가 아니라 「데모와 전혀 다르게 보인다」였다. 다만 이 경로에서만 생기는 실측 차이 2가지를 찾아 보고에 적었다 (세이브 파일이 활성 씬 이름으로 갈린다 · RenderSettings 가 InGame 소유). ■ ② 바닥 3요소 (같은 화각·같은 거리 캡처로 판정) 체크무늬 : 잔디 타일 텍스처는 단색(#A0E4A2) — 격자 0. 밭(Soil)의 칸 표시는 게임 기능이라 남기고 **색만** 데모 톤으로(soilTint·soilCheckerContrast). 바닥 색 : #AFEDAE → #A0E4A2 (화면색 평균 데모 #94BE81 ↔ 우리 #9AC37E) 풀 색 : 새 복사본 Farm_Grass_Demo.mat (_DiffuseColor #A6E39F · _ShadowDiffuseColor #95CC8F) → 풀이 바닥과 같은 색으로 깔려 「카펫」이 사라진다(에지% 8.09 → 1.96 · 데모 1.79) 밀도 : 2.5 → 1.8 (6.25 → 3.24 개/㎡) 🔴 Assets/FarmingIsland/** 0줄 · Assets/Script/** 0줄 · 3DPixelArtEnvironment 0줄 · ProjectSettings 0줄 · 전역 URP/품질 0. 되돌리기 = enabled_ = 0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 05:19:39 +00:00
// ── 816f: 밭(Soil) 체크무늬 **기능은 유지**하고 색만 데모 톤으로 (PD 2026-09-13)
[Header("§0-816f — 밭(Soil) 칸 표시 톤 (기능 유지 · 색만)")]
[Tooltip("1 이면 FI `Farm` 의 마른/젖은 흙 색 4개를 런타임에 데모 톤으로 낮춘다. " +
"칸 구분(마름/젖음 표시)은 그대로 남는다 — 색만 바뀐다. 0 이면 FI 원본 색.")]
public int soilToneEnabled = 1;
[Tooltip("흙 색에 곱하는 값(0~1). 낮출수록 주황이 죽는다.")]
public Color soilTint = new Color(0.84f, 0.86f, 0.90f, 1f);
[Tooltip("체크무늬 두 색의 차이를 이 비율로 줄인다(1 = FI 원본 차이 · 0 = 차이 없음=칸 안 보임). " +
"🔴 0 으로 두지 말 것 — 어디를 갈았는지 안 보인다.")]
[Range(0f, 1f)] public float soilCheckerContrast = 0.6f;
// ── 816r: 밭 흙색을 데모 흙 톤으로 「옮긴다」 (곱셈 틴트로는 못 가는 색)
[Tooltip("0 = 816f 상태(곱셈 틴트만) · 1 이면 아래 목표색으로 밭 바닥색을 옮긴다. " +
"칸 차이(어디를 갈았나)와 마름↔젖음 밝기비는 그대로 보존한다.")]
public int soilTargetEnabled = 1;
[Tooltip("밭 「마름」 바닥의 목표색(리니어). 816r 실측 — 데모 흙 화면색에 맞춘 값. " +
"젖은 흙은 이 색에 원본의 젖음/마름 밝기비를 곱해 자동으로 어두워진다.")]
public Color soilTargetColor = new Color(0.3049f, 0.4033f, 0.2961f, 1f);
[Header("§0 — 조명 값 (816a 실측 = 데모/아레나와 동일)")]
public Color ambientSky = new Color(0.212f, 0.227f, 0.259f, 1f);
public Color ambientEquator = new Color(0.114f, 0.125f, 0.133f, 1f);
public Color ambientGround = new Color(0.047f, 0.043f, 0.035f, 1f);
public float ambientIntensity = 1f;
[Tooltip("빌트인 Default-Skybox. 비우면 스카이박스는 안 건드린다.")]
public Material skybox;
public Color dirLightColor = Color.white;
public float dirLightIntensity = 1f;
[Tooltip("1 = Soft(데모) · 0 = 원본(Hard) 유지.")]
public int dirLightSoftShadows = 1;
[Tooltip("1 이면 안개를 끈다(데모 = off).")]
public int fogOff = 1;
[Tooltip("1 이면 FI `GraphicsManager` 를 끈다. 이 컴포넌트는 Play 중 FI 의 URP 에셋 파일에 " +
"renderScale 을 **직접 써 넣어** `Assets/FarmingIsland/**` 를 더럽힌다(816a 실측). " +
"우리 프로젝트의 활성 URP 에셋이 아니라 화면에는 영향 0.")]
public int disableFiGraphicsManager = 1;
[Tooltip("런타임에 뒤늦게 생기는 오브젝트(상인·작물·아이템)까지 잡기 위해 다시 훑는 시각(초).")]
public float[] rescanAtSeconds = new float[] { 0.5f, 2f, 5f };
// ───────────────────────────────── §816h-1 하늘 범위
[Header("§816h — 하늘(RenderSettings)은 「활성 씬」 소유다")]
[Tooltip("🔴 816f/816h 실측 — 섬·던전·전투 맵은 전부 `InGame` 위에 **Additive** 로 붙는다. " +
"그래서 앰비언트·스카이박스·안개는 붙는 씬이 아니라 **활성 씬(InGame)** 것이 쓰인다. " +
"1 이면 아래 `skyScenes` 에 들어간 맵에 들어갈 때만 활성 씬의 하늘을 **스냅샷 뜨고** 우리 값으로 바꾸고, " +
"그 맵에서 나가면 **스냅샷 그대로 되돌린다**(= 다른 맵 오염 0).")]
public int skyScopeEnabled = 1;
[Tooltip("하늘을 통일할 맵. 섬과 던전이 같은 하늘을 쓰게 한다. 전투 맵은 여기 넣지 않는다(원래대로).")]
public string[] skyScenes = new string[] { "Level01", "WL_FarmLook", "WL_Dungeon01", "WL_Dungeon02" };
[Tooltip("맵을 바꿀 때 `SceneInfo` 는 내리기와 올리기를 같은 프레임에 시작한다(실측). " +
"이만큼 기다린 뒤에도 범위 맵이 하나도 없을 때만 하늘을 반납한다 — 섬↔던전 이동에서 한 번 튀는 것을 막는다.")]
public float skyReleaseDelaySeconds = 1.5f;
// ───────────────────────────────── §816h-2 물
[Header("§816h — 섬의 물을 WL 워터 셰이더로 (PD 지시)")]
[Tooltip("0 = 지금(FarmingIsland 물 · 100 % 복귀) · 1 = WL 워터(`ToonWaterU`).")]
public int waterMode = 1;
[Tooltip("교체 대상 판별 — FarmingIsland 원본 물 머티리얼(참조만 · 0줄 수정).")]
public Material waterFrom;
[Tooltip("WL 워터 복사본(`Assets/WL/Materials/WL_Water_Ocean.mat` 기준). 비우면 교체하지 않는다.")]
public Material waterTo;
// ───────────────────────────────── §816j2 섬 둘레 거품 띠
[Header("§816j2 — 섬 둘레 흰 거품 띠 (PD 「둘레에 띠를 둘러줘」)")]
[Tooltip("0 = 띠 없음(816j 상태로 100 % 복귀) · 1 = 섬 둘레를 따라 메시 띠 1장(드로우콜 +1).")]
public int shoreFoamEnabled = 1;
[Tooltip("띠에 쓰는 머티리얼(URP Unlit · 반투명). 비우면 띠를 만들지 않는다.")]
public Material shoreFoamMaterial;
[Tooltip("띠가 물 쪽으로 뻗는 폭(m). 캐릭터 키 1.19 m · 섬 타일 8 m 기준.")]
public float shoreFoamWidth = 1.6f;
[Tooltip("섬 안쪽으로 겹쳐 들어가는 길이(m) — 물가에 틈이 생기지 않게. 섬 벽이 가려 준다.")]
public float shoreFoamInner = 0.5f;
[Tooltip("띠 메시의 격자 간격(m). 크면 싸고 거칠다. 비용이 문제면 이것부터 키운다.")]
public float shoreFoamCell = 0.5f;
[Tooltip("물 표면보다 얼마나 위에 깔 것인가(m). 너무 작으면 z-fighting.")]
public float shoreFoamY = 0.02f;
[Tooltip("씬에서 `Water` 를 못 찾았을 때 쓸 물 높이(m).")]
public float shoreFoamFallbackY = -1f;
[Tooltip("거품 무늬가 해안을 따라 반복되는 길이(m).")]
public float shoreFoamTileLength = 6f;
[Tooltip("거품이 흐르는 속도(1 = 초당 무늬 1칸). 0 이면 정지.")]
public float shoreFoamScroll = 0.06f;
// ───────────────────────────────── §1 풀밭
[Header("§1 — 섬 타일 위 풀밭 (데모와 같은 인스턴싱)")]
[Tooltip("0 이면 풀을 깔지 않는다.")]
public int grassEnabled = 1;
[Tooltip("흩뿌릴 것들. 같은 density 를 가진 줄끼리 한 층으로 묶여 확률 분배된다(데모와 같은 방식).")]
public WLScatterDef[] scatter = new WLScatterDef[0];
[Tooltip("격자 흔들기(격자 칸 단위). 데모 = 0.5.")]
[Range(0f, 0.5f)] public float positionVariance = 0.5f;
[Tooltip("크기 흔들기. 데모 = 0.2 → ×0.8~1.2.")]
[Range(0f, 0.9f)] public float scaleVariance = 0.2f;
[Tooltip("1 이면 풀 하나하나를 Y 축으로 랜덤 회전한다(데모는 회전 0 = 빌보드라 불필요). " +
"빌보드 셰이더에는 영향이 없다.")]
public int randomYaw = 0;
[Header("§1 — 풀을 깔지 않는 곳 (제외 규칙)")]
[Tooltip("섬 타일 가장자리에서 이만큼(m) 안쪽까지만 깐다. 물 위로 삐져나오지 않게.")]
public float edgeInset = 0.15f;
[Tooltip("길(Road 메시)에서 이만큼(m) 떨어뜨린다.")]
public float roadMargin = 0.15f;
[Tooltip("농사 타일(Farm 트리거 · Soil 1×1)에서 이만큼(m) 떨어뜨린다.")]
public float farmMargin = 0.35f;
[Tooltip("건물·소품 콜라이더에서 이만큼(m) 떨어뜨린다.")]
public float propMargin = 0.05f;
[Tooltip("이 크기(m) 이상인 콜라이더는 섬 본체로 보고 제외 대상에서 뺀다.")]
public float propMaxSize = 7.5f;
[Header("§1 — 모바일 예산")]
[Tooltip("인스턴스 총개수 상한. 넘으면 밀도를 자동으로 낮춘다(0 = 무제한).")]
public int maxInstances = 40000;
[Tooltip("섬이 확장되면 다시 깐다 — 이 주기(초)로 잠긴 섬이 열렸는지 본다. 0 이면 감시 안 함.")]
public float rebuildPollSeconds = 1f;
[Tooltip("확장 애니메이션(DOTween 1초)이 끝나기를 기다리는 초.")]
public float rebuildDelaySeconds = 1.4f;
// ───────────────────────────────── §2 구름 경계 배치 (816o)
[Header("§2 — 구름 그림자 경계에만 풀을 심는다 (816o · PD 「모바일 부하 없게」)")]
[Tooltip("🔴 되돌리기 스위치 — 0 이면 816n 상태 100 % 복귀(섬 전체에 균일 격자). " +
"1 이면 바닥 색 띠가 바뀌는 곳(구름 그림자 경계) 근처에만 심는다. " +
"816n 실측: 보이는 풀의 88 % 가 이 띠에 몰려 있다 = 나머지는 삼각형만 먹는다.")]
public int cloudEdgeEnabled = 1;
[Tooltip("경계 판정 격자 칸(m). 띠 폭의 최소 단위 = 이 값. 여기는 노이즈를 계산하지 않는다(보간).")]
public float cloudEdgeCell = 0.3f;
[Tooltip("판정 격자 칸 수 상한. 섬이 커지면 칸을 자동으로 키운다(0 = 무제한).")]
public int cloudEdgeMaxCells = 120000;
[Tooltip("🔴 비용이 나는 유일한 곳 — 구름 노이즈(6 옥타브)를 실제로 계산하는 간격(m). " +
"구름의 가장 작은 무늬가 1/(32×_Cloud_Density) ≈ 3 m 라 1 m 면 충분하다. " +
"사이 값은 겹선형 보간으로 채운다.")]
public float cloudEdgeSampleCell = 1f;
[Tooltip("노이즈를 계산하는 점의 개수 상한. 섬이 커지면 간격을 자동으로 늘려 비용을 묶어 둔다.")]
public int cloudEdgeMaxSamples = 4096;
[Tooltip("경계에서 몇 칸 더 넓힐 것인가. 띠 폭 ≈ (1 + 2 × 이 값) × 칸(m). " +
"구름이 흐르는 만큼(`cloudEdgeDriftCover`)은 여기에 자동으로 더해진다.")]
public int cloudEdgeDilate = 1;
[Tooltip("1 이면 갱신 주기 동안 구름이 흐르는 거리만큼 띠를 자동으로 더 넓힌다 — " +
"띠가 움직여도 풀이 뒤따라오지 못해 생기는 빈틈을 막는다. 0 이면 위 값만 쓴다.")]
public int cloudEdgeDriftCover = 1;
[Tooltip("구름이 흐르므로 이 주기(초)마다 띠를 다시 고른다. 0 이면 한 번 굽고 고정. " +
"🔴 실제 주기는 `rebuildPollSeconds` 단위로 반올림된다.")]
public float cloudEdgeRefreshSeconds = 1f;
[Tooltip("띠 안의 밀도 배율. 면적이 줄어든 만큼 띠를 더 촘촘히 해 데모 느낌을 살린다. " +
"1 = 816n 과 같은 밀도(= 인스턴스가 띠 면적 비율만큼 그대로 줄어든다).")]
public float cloudEdgeDensityScale = 1.25f;
[Tooltip("구름 값을 읽어 올 머티리얼. 비우면 `islandTopMaterial`(바닥)을 쓴다 — " +
"바닥과 **같은 식**이어야 띠가 어긋나지 않는다.")]
public Material cloudEdgeSourceMaterial;
// ───────────────────────────────── §3 가시 범위 배치 (816q)
[Header("§3 — 카메라가 보는 범위에만 인스턴스를 둔다 (816q · 배치가 아니라 범위로 최적화)")]
[Tooltip("🔴 되돌리기 스위치 — 0 이면 섬 전체에 인스턴스를 둔다(816n 상태). " +
"1 이면 카메라 시야 사각형 + 여유 폭 안에만 둔다. " +
"격자는 월드 고정이라 같은 자리의 풀은 항상 같은 자리다(순간이동 0) — " +
"늘고 주는 것은 **화면 밖 여유 폭 안에서만** 일어난다.")]
public int viewCullEnabled = 1;
[Tooltip("시야 사각형 바깥으로 더 두는 여유 폭(m). " +
"🔴 `viewRebuildStep` 보다 커야 한다 — 차이만큼이 「화면 밖에서만 바뀐다」는 보장 폭이다.")]
public float viewMargin = 4f;
[Tooltip("카메라가 이만큼(m) 움직였을 때만 다시 고른다. " +
"작을수록 자주 고르고, 클수록 여유 폭을 많이 먹는다.")]
public float viewRebuildStep = 1.5f;
[Tooltip("카메라가 이만큼(°) 돌았을 때도 다시 고른다. 회전은 시야를 통째로 돌리므로 위치보다 민감하다.")]
public float viewRebuildAngle = 3f;
[Tooltip("시야 사다리꼴의 먼 쪽을 이 거리(m)에서 끊는다. 하늘을 향하는 모서리도 여기로 자른다. " +
"PD 구도(12 m·45°)에서 화면 위끝의 바닥은 약 32 m 라 40 이면 남는다.")]
public float viewMaxDistance = 40f;
[Tooltip("시야 사각형을 다시 재는 주기(초). 0 이면 감시 안 함(한 번 고르고 고정).")]
public float viewPollSeconds = 0.25f;
// ───────────────────────────────── 구운 마스크
[Header("§1 — 구운 윗면 마스크 (에디터에서 생성 · 손대지 말 것)")]
public WLTileMask[] tileMasks = new WLTileMask[0];
public WLTileMask[] roadMasks = new WLTileMask[0];
// ───────────────────────────────── 섬 씬 판별
[Header("어느 씬에 적용하는가")]
[Tooltip("이 이름의 씬에 적용한다(비워도 IslandManager 가 있으면 섬으로 본다).")]
public string[] islandSceneNames = new string[] { "Level01", "WL_FarmLook" };
public WLTileMask FindTileMask(string meshName) { return Find(tileMasks, meshName); }
public WLTileMask FindRoadMask(string meshName) { return Find(roadMasks, meshName); }
static WLTileMask Find(WLTileMask[] a, string n)
{
if (a == null || string.IsNullOrEmpty(n)) return null;
// `MeshFilter.mesh` 로 읽으면 이름 뒤에 " Instance" 가 붙는다 — 떼고 비교한다.
int k = n.IndexOf(" Instance");
if (k > 0) n = n.Substring(0, k);
for (int i = 0; i < a.Length; i++) if (a[i] != null && a[i].meshName == n) return a[i];
return null;
}
public void Log(string msg) { if (verboseLog != 0) Debug.Log("[WL-816e] " + msg); }
}
}