using System.IO; using ADOFAIRenderer.Core; using ADOFAIRenderer.Util; using HarmonyLib; using UnityEngine; namespace ADOFAIRenderer.Patch.Game; /// /// Patches of scnGame (custom level scene controller, also used by the editor's play mode): hides tiles and planets /// for the "non-tile" render option and replaces the background video player during a render. /// [HarmonyPatch(typeof(scnGame))] // ReSharper disable once InconsistentNaming public static class scnGamePatch { /// /// Postfix of scnGame.Play(int seqID, bool remakeFloors), which starts (or restarts) play of the custom level: /// it resets the conductor, camera and controller state for the given start tile. /// /// Active from stage Load when AdvancedConfig.NonTile is enabled. Disables the renderers of every floor in /// scrLevelMaker.listFloors (floor mesh, legacy sprite, icon, outline, top/bottom glow, multi-planet line, /// hold mesh, dummy planets) and of every planet of every player (sprite mesh, core/tail/sparks particles, ring, glow, /// face, samurai sprite), so only the background and decorations remain in the rendered video. The renderers are only /// disabled; the objects keep running. Hold meshes re-enable themselves, so RenderManager.OnLastUpdate disables /// them again every frame. /// /// [HarmonyPostfix] [HarmonyPatch("Play")] public static void ProcessFloors() { if (RenderManager.Instance && RenderManager.Instance.stage >= RenderStage.Load && RenderManager.Instance.Configuration.AdvancedConfig.DisableTileCulling) { foreach (var floor in scrLevelMaker.instance.listFloors) { if (floor) floor.enabled = true; } } DecorationPatch.ProcessDecorations(); var nonTile = RenderManager.Instance && RenderManager.Instance.stage >= RenderStage.Load && RenderManager.Instance.Configuration?.AdvancedConfig?.NonTile == true; if (!nonTile) return; foreach (var floor in scrLevelMaker.instance.listFloors) { DisableRenderers( floor.floorRenderer.renderer, floor.legacyFloorSpriteRenderer, floor.iconsprite, floor.outlineSprite, floor.topGlow, floor.bottomGlow, floor.multiplanetLine ); if (floor.holdRenderer) DisableRenderer(floor.holdRenderer.m_meshRenderer); if (floor.dummyPlanets == null) continue; foreach (var planet in floor.dummyPlanets) DisablePlanetRenderer(planet); } var players = scrPlayerManager.instance.players; if (players == null) return; foreach (var player in players) { var planets = player.planetarySystem?.planetList; if (planets == null) continue; foreach (var planet in planets) DisablePlanetRenderer(planet.planetRenderer); } return; // Disables every renderer that draws one planet (fire, ice, extra planets and floor dummy planets). void DisablePlanetRenderer(PlanetRenderer planet) { if (planet.sprite) DisableRenderer(planet.sprite.meshRenderer); DisableRenderers( planet.coreParticles.GetRenderer(), planet.tailParticles.GetRenderer(), planet.tailParticlesCoop.GetRenderer(), planet.sparks.GetRenderer(), planet.ring, planet.glow, planet.faceSprite, planet.faceDetails, planet.samuraiSprite ); } void DisableRenderers(params Renderer?[] renderers) { foreach (var renderer in renderers) DisableRenderer(renderer); } // Sets Renderer.enabled = false; null or destroyed renderers are skipped (Unity null check). void DisableRenderer(Renderer? renderer) { if (renderer) renderer.enabled = false; } } /// /// Prefix of scnGame.UpdateVideo(), which sets up the level's background video on the native Unity /// VideoPlayer (scnGame.videoBG): it reads miscSettings.bgVideo, loads a VideoClip for internal /// (Resources / Addressables) and bundle levels or sets videoBG.url to the file next to the custom level, then calls /// Stop(), Prepare() and sets isLooping from loopVideo. The game calls it from /// scnGame.FinishCustomLevelLoading and from scnGame.ReloadAssets when force is true. /// /// While (stage >= OnScene), an external video file of a custom level is played by /// (FFmpeg decoder), which shows the frame for the render time of each frame. The native /// VideoPlayer plays in real time and would drift from the offline render clock, so it is disabled for that case. /// Internal and bundle levels (built-in VideoClip assets, not files) and a missing file use the original method. /// /// Outside a render, any left on the video object is destroyed and the native player re-enabled. /// /// The scene controller. /// True to run the original UpdateVideo; false when the mod handled the video (or there is none). [HarmonyPrefix] [HarmonyPatch("UpdateVideo")] // ReSharper disable once InconsistentNaming public static bool ReplaceVideo(scnGame __instance) { // Only replace when rendering is active if (!RenderManager.IsActive) { // Remove FFmpeg player if it was attached previously var oldPlayer = __instance.videoBG.gameObject.GetComponent(); if (oldPlayer) Object.Destroy(oldPlayer); // Re-enable native player component __instance.videoBG.enabled = true; return true; // Execute original } string text = __instance.levelData.miscSettings.GetString("bgVideo"); if (string.IsNullOrEmpty(text)) { __instance.videoBG.gameObject.SetActive(false); return false; // Skip original } __instance.videoBG.gameObject.SetActive(true); // We can only play external video files via FFmpegNativeDecoder. // Internal/Bundle level background videos are built-in VideoClips, so we fall back to native Unity VideoPlayer. if (ADOBase.isInternalLevel || ADOBase.isBundleLevel) { __instance.videoBG.enabled = true; return true; // Execute original } // Custom level video path var videoPath = Path.Combine(Path.GetDirectoryName(ADOBase.levelPath) ?? "", text); if (!File.Exists(videoPath)) { return true; // Let the original print error } // Camera for the CameraFarPlane render mode: the VideoPlayer's target camera, or the "BGStaticCam" scene object // when none is assigned. var targetCamera = __instance.videoBG.targetCamera; if (!targetCamera) { var camGo = GameObject.Find("BGStaticCam"); if (camGo != null) { targetCamera = camGo.GetComponent(); } } // Disable the native VideoPlayer so it does not play the video in real time alongside VideoBGPlayer. __instance.videoBG.enabled = false; // Add or retrieve our custom FFmpeg video background player if (!__instance.videoBG.gameObject.TryGetComponent(out var player)) { player = __instance.videoBG.gameObject.AddComponent(); } var isLooping = __instance.levelData.miscSettings.GetBool("loopVideo"); // Initialize player. If targetTexture is null (CameraFarPlane mode), it will build a quad in front of targetCamera. player.Init(videoPath, __instance.videoBG.targetTexture, isLooping, targetCamera, __instance.videoBG.gameObject); return false; // Skip original UpdateVideo } }