SHA256
187 lines
8.3 KiB
C#
187 lines
8.3 KiB
C#
using System.IO;
|
|
using ADOFAIRenderer.Core;
|
|
using ADOFAIRenderer.Util;
|
|
using HarmonyLib;
|
|
using UnityEngine;
|
|
|
|
namespace ADOFAIRenderer.Patch.Game;
|
|
|
|
/// <summary>
|
|
/// Patches of <c>scnGame</c> (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.
|
|
/// </summary>
|
|
[HarmonyPatch(typeof(scnGame))]
|
|
// ReSharper disable once InconsistentNaming
|
|
public static class scnGamePatch
|
|
{
|
|
/// <summary>
|
|
/// Postfix of <c>scnGame.Play(int seqID, bool remakeFloors)</c>, which starts (or restarts) play of the custom level:
|
|
/// it resets the conductor, camera and controller state for the given start tile.
|
|
/// <para>
|
|
/// Active from stage Load when <c>AdvancedConfig.NonTile</c> is enabled. Disables the renderers of every floor in
|
|
/// <c>scrLevelMaker.listFloors</c> (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 <c>RenderManager.OnLastUpdate</c> disables
|
|
/// them again every frame.
|
|
/// </para>
|
|
/// </summary>
|
|
[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;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Prefix of <c>scnGame.UpdateVideo()</c>, which sets up the level's background video on the native Unity
|
|
/// <c>VideoPlayer</c> (<c>scnGame.videoBG</c>): it reads <c>miscSettings.bgVideo</c>, loads a <c>VideoClip</c> for internal
|
|
/// (Resources / Addressables) and bundle levels or sets <c>videoBG.url</c> to the file next to the custom level, then calls
|
|
/// <c>Stop()</c>, <c>Prepare()</c> and sets <c>isLooping</c> from <c>loopVideo</c>. The game calls it from
|
|
/// <c>scnGame.FinishCustomLevelLoading</c> and from <c>scnGame.ReloadAssets</c> when <c>force</c> is true.
|
|
/// <para>
|
|
/// While <see cref="RenderManager.IsActive"/> (stage >= OnScene), an external video file of a custom level is played by
|
|
/// <see cref="VideoBGPlayer"/> (FFmpeg decoder), which shows the frame for the render time of each frame. The native
|
|
/// <c>VideoPlayer</c> 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 <c>VideoClip</c> assets, not files) and a missing file use the original method.
|
|
/// </para>
|
|
/// <para>Outside a render, any <see cref="VideoBGPlayer"/> left on the video object is destroyed and the native player re-enabled.</para>
|
|
/// </summary>
|
|
/// <param name="__instance">The scene controller.</param>
|
|
/// <returns>True to run the original UpdateVideo; false when the mod handled the video (or there is none).</returns>
|
|
[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<VideoBGPlayer>();
|
|
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<Camera>();
|
|
}
|
|
}
|
|
|
|
// 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<VideoBGPlayer>(out var player))
|
|
{
|
|
player = __instance.videoBG.gameObject.AddComponent<VideoBGPlayer>();
|
|
}
|
|
|
|
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
|
|
}
|
|
} |