From ccc0ac40726e4dc8abc97b88bae9ce7c4fec272ebd31f6cf8c94f1b5b6170b9a Mon Sep 17 00:00:00 2001
From: koren <131921956+kkorenn@users.noreply.github.com>
Date: Sat, 8 Aug 2026 22:22:40 +0900
Subject: [PATCH] non-tile addition
---
Core/NonTileHider.cs | 166 ++++++++++++++++++++++++++
Patch/Game/scrPortalParticlesPatch.cs | 13 ++
2 files changed, 179 insertions(+)
create mode 100644 Core/NonTileHider.cs
diff --git a/Core/NonTileHider.cs b/Core/NonTileHider.cs
new file mode 100644
index 0000000..024d09c
--- /dev/null
+++ b/Core/NonTileHider.cs
@@ -0,0 +1,166 @@
+using System;
+using System.Collections.Generic;
+using MelonLoader;
+using UnityEngine;
+
+namespace ADOFAIRenderer.Core
+{
+ ///
+ /// 논타일(BGA) 모드: 렌더링 중 타일과 행성을 숨겨 배경/장식만 영상에 담습니다.
+ /// Quartz 에디터 모듈의 BGA 모드 로직을 이식한 것으로, 게임 코드가 렌더러를
+ /// 다시 켜는 경우(행성 전환, 타일 등장 애니메이션 등)를 상쇄하기 위해
+ /// 매 프레임 재적용(Reconcile)하는 방식을 사용합니다.
+ ///
+ public static class NonTileHider
+ {
+ private static bool _applied;
+ private static readonly Dictionary ParticleRendererCache = new();
+
+ ///
+ /// 논타일 렌더링 세션 여부. 레벨 로딩 중(OverrideLoading)에도 참이어야
+ /// 마지막 타일의 종료 포탈 생성(scrFloor.SpawnPortalParticles)을 막을 수 있습니다.
+ ///
+ public static bool SuppressPortal =>
+ RenderManager.Instance
+ && RenderManager.Instance.Configuration != null
+ && RenderManager.Instance.Configuration.AdvancedConfig.NonTile
+ && (RenderManager.IsActive || RenderManager.Instance.OverrideLoading);
+
+ ///
+ /// 렌더링 중 매 프레임 호출해 타일/행성을 숨긴 상태로 유지합니다.
+ /// 게임의 타일 컬링이 카메라 근처 타일의 렌더러를 매번 다시 켜므로
+ /// (첫/마지막 타일만 검사하는 방식으로는 감지 불가) 전체 타일을 매 프레임 재적용합니다.
+ /// Renderer.enabled는 값이 바뀔 때만 쓰므로 반복 비용은 순회뿐입니다.
+ ///
+ public static void Reconcile()
+ {
+ try
+ {
+ // 포탈 생성 자체는 Harmony 패치로 막지만, 패치 적용 전(또는 설정 확정 전)에
+ // 이미 생성된 종료 포탈이 있을 수 있으므로 최초 적용 시 한 번 정리합니다.
+ if (!_applied) SetPortalsVisible(false);
+
+ SetPlanetsVisible(false);
+
+ var floors = ADOBase.lm ? ADOBase.lm.listFloors : null;
+ if (floors != null && floors.Count > 0)
+ SetFloorsVisible(floors, false);
+
+ _applied = true;
+ }
+ catch (Exception e)
+ {
+ Melon.Logger.Warning($"[NonTile] 타일/행성 숨김 처리 실패: {e.Message}");
+ }
+ }
+
+ /// 렌더 종료 시 호출해 숨겼던 타일/행성을 복원합니다.
+ public static void Restore()
+ {
+ if (!_applied) return;
+ try
+ {
+ var floors = ADOBase.lm ? ADOBase.lm.listFloors : null;
+ if (floors != null) SetFloorsVisible(floors, true);
+ SetPlanetsVisible(true);
+ SetPortalsVisible(true);
+ }
+ catch (Exception e)
+ {
+ Melon.Logger.Warning($"[NonTile] 타일/행성 복원 실패: {e.Message}");
+ }
+ _applied = false;
+ ParticleRendererCache.Clear();
+ }
+
+ private static void SetFloorsVisible(List floors, bool visible)
+ {
+ foreach (var floor in floors)
+ {
+ if (!floor) continue;
+ if (floor.floorRenderer != null) Set(floor.floorRenderer.renderer, visible);
+ Set(floor.legacyFloorSpriteRenderer, visible);
+ Set(floor.iconsprite, visible);
+ Set(floor.outlineSprite, visible);
+ Set(floor.topGlow, visible);
+ Set(floor.bottomGlow, visible);
+ Set(floor.multiplanetLine, visible);
+ if (floor.holdRenderer != null) Set(floor.holdRenderer.m_meshRenderer, visible);
+
+ // 멀티플래닛 타일에 붙는 더미 행성도 함께 숨깁니다.
+ var floorDummies = floor.dummyPlanets;
+ if (floorDummies != null)
+ {
+ foreach (var dummy in floorDummies)
+ SetPlanetRendererVisible(dummy, visible);
+ }
+ }
+ }
+
+ private static void SetPlanetsVisible(bool visible)
+ {
+ var pm = scrPlayerManager.instance;
+ var players = pm ? pm.players : null;
+ if (players != null)
+ {
+ foreach (var player in players)
+ {
+ var planets = player ? player.planetarySystem?.planetList : null;
+ if (planets == null) continue;
+ foreach (var planet in planets)
+ if (planet != null) SetPlanetRendererVisible(planet.planetRenderer, visible);
+ }
+ }
+
+ var dummies = ADOBase.controller ? ADOBase.controller.dummyPlanets : null;
+ if (dummies != null)
+ {
+ foreach (var dummy in dummies)
+ SetPlanetRendererVisible(dummy, visible);
+ }
+ }
+
+ private static void SetPlanetRendererVisible(PlanetRenderer? pr, bool visible)
+ {
+ if (pr == null) return;
+ Set(pr.sprite != null ? pr.sprite.meshRenderer : null, visible);
+ Set(ParticleRenderer(pr.coreParticles), visible);
+ Set(ParticleRenderer(pr.tailParticles), visible);
+ Set(ParticleRenderer(pr.tailParticlesCoop), visible);
+ Set(ParticleRenderer(pr.sparks), visible);
+ Set(pr.ring, visible);
+ Set(pr.glow, visible);
+ Set(pr.faceSprite, visible);
+ Set(pr.faceDetails, visible);
+ Set(pr.samuraiSprite, visible);
+ }
+
+ // 마지막 타일의 종료 포탈(animatedPortalPrefab 인스턴스)은 타일의 자식 오브젝트라
+ // scrFloor 필드 순회로는 잡히지 않으므로 컴포넌트 검색으로 처리합니다.
+ // 신규 생성은 scrFloorPortalPatch가 막으므로 최초 적용/복원 시에만 호출됩니다.
+ private static void SetPortalsVisible(bool visible)
+ {
+ foreach (var portal in UnityEngine.Object.FindObjectsByType(FindObjectsInactive.Include, FindObjectsSortMode.None))
+ {
+ if (portal == null) continue;
+ foreach (var r in portal.GetComponentsInChildren(true))
+ Set(r, visible);
+ }
+ }
+
+ private static Renderer? ParticleRenderer(ParticleSystem? ps)
+ {
+ if (ps == null) return null;
+ var id = ps.GetInstanceID();
+ if (ParticleRendererCache.TryGetValue(id, out var cached) && cached != null) return cached;
+ var r = ps.GetComponent();
+ if (r != null) ParticleRendererCache[id] = r;
+ return r;
+ }
+
+ private static void Set(Renderer? r, bool visible)
+ {
+ if (r != null && r.enabled != visible) r.enabled = visible;
+ }
+ }
+}
diff --git a/Patch/Game/scrPortalParticlesPatch.cs b/Patch/Game/scrPortalParticlesPatch.cs
index 1a07347..865a8a6 100644
--- a/Patch/Game/scrPortalParticlesPatch.cs
+++ b/Patch/Game/scrPortalParticlesPatch.cs
@@ -3,6 +3,19 @@ using UnityEngine;
namespace ADOFAIRenderer.Patch.Game
{
+ [HarmonyPatch(typeof(scrFloor))]
+ // ReSharper disable once InconsistentNaming
+ public static class scrFloorPortalPatch
+ {
+ [HarmonyPrefix]
+ [HarmonyPatch("SpawnPortalParticles")]
+ public static bool SkipPortalInNonTileRender()
+ {
+ // 논타일 렌더링 중에는 마지막 타일의 종료 포탈을 아예 생성하지 않습니다.
+ return !Core.NonTileHider.SuppressPortal;
+ }
+ }
+
[HarmonyPatch(typeof(scrPortalParticles))]
// ReSharper disable once InconsistentNaming
public static class scrPortalParticlesPatch