oni-priority-ux/mod/PLibLighting/PLightManager.cs

379 lines
14 KiB
C#

/*
* Copyright 2022 Peter Han
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
* and associated documentation files (the "Software"), to deal in the Software without
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all copies or
* substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
using HarmonyLib;
using PeterHan.PLib.Core;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using UnityEngine;
using BrightnessDict = System.Collections.Generic.IDictionary<int, float>;
using LightGridEmitter = LightGridManager.LightGridEmitter;
namespace PeterHan.PLib.Lighting {
/// <summary>
/// Manages lighting. Instantiated only by the latest PLib version.
/// </summary>
public sealed class PLightManager : PForwardedComponent {
/// <summary>
/// Implemented by classes which want to handle lighting calls.
/// </summary>
/// <param name="args">The parameters to use for lighting, and the location to
/// store results. See the LightingArgs class documentation for details.</param>
public delegate void CastLightDelegate(LightingArgs args);
/// <summary>
/// A singleton empty list instance for default values.
/// </summary>
private static readonly List<object> EMPTY_SHAPES = new List<object>(1);
/// <summary>
/// The version of this component. Uses the running PLib version.
/// </summary>
internal static readonly Version VERSION = new Version(PVersion.VERSION);
/// <summary>
/// If true, enables the smooth light falloff mode even on vanilla lights.
/// </summary>
internal static bool ForceSmoothLight { get; set; }
/// <summary>
/// The instantiated copy of this class.
/// </summary>
internal static PLightManager Instance { get; private set; }
/// <summary>
/// Calculates the brightness falloff as it would be in the stock game.
/// </summary>
/// <param name="falloffRate">The falloff rate to use.</param>
/// <param name="cell">The cell where falloff is being computed.</param>
/// <param name="origin">The light origin cell.</param>
/// <returns>The brightness at that location from 0 to 1.</returns>
public static float GetDefaultFalloff(float falloffRate, int cell, int origin) {
return 1.0f / Math.Max(1.0f, Mathf.RoundToInt(falloffRate * Math.Max(Grid.
GetCellDistance(origin, cell), 1)));
}
/// <summary>
/// Calculates the brightness falloff similar to the default falloff, but far smoother.
/// Slightly heavier on computation however.
/// </summary>
/// <param name="falloffRate">The falloff rate to use.</param>
/// <param name="cell">The cell where falloff is being computed.</param>
/// <param name="origin">The light origin cell.</param>
/// <returns>The brightness at that location from 0 to 1.</returns>
public static float GetSmoothFalloff(float falloffRate, int cell, int origin) {
Vector2I newCell = Grid.CellToXY(cell), start = Grid.CellToXY(origin);
return 1.0f / Math.Max(1.0f, falloffRate * PUtil.Distance(start.X, start.Y,
newCell.X, newCell.Y));
}
/// <summary>
/// Gets the raycasting shape to use for the given light.
/// </summary>
/// <param name="light">The light which is being drawn.</param>
/// <returns>The shape to use for its rays.</returns>
internal static LightShape LightShapeToRayShape(Light2D light) {
var shape = light.shape;
if (shape != LightShape.Cone && shape != LightShape.Circle)
shape = Instance.GetRayShape(shape);
return shape;
}
/// <summary>
/// Logs a message encountered by the PLib lighting system.
/// </summary>
/// <param name="message">The debug message.</param>
internal static void LogLightingDebug(string message) {
Debug.LogFormat("[PLibLighting] {0}", message);
}
/// <summary>
/// Logs a warning encountered by the PLib lighting system.
/// </summary>
/// <param name="message">The warning message.</param>
internal static void LogLightingWarning(string message) {
Debug.LogWarningFormat("[PLibLighting] {0}", message);
}
public override Version Version => VERSION;
/// <summary>
/// The light brightness set by the last lighting brightness request.
/// </summary>
private readonly ConcurrentDictionary<LightGridEmitter, CacheEntry> brightCache;
/// <summary>
/// The last object that requested a preview. Only one preview can be requested at a
/// time, so no need for thread safety.
/// </summary>
internal GameObject PreviewObject { get; set; }
/// <summary>
/// The lighting shapes available, all in this mod's namespace.
/// </summary>
private readonly IList<ILightShape> shapes;
/// <summary>
/// Creates a lighting manager to register PLib lighting.
/// </summary>
public PLightManager() {
// Needs to be thread safe!
brightCache = new ConcurrentDictionary<LightGridEmitter, CacheEntry>(2, 128);
PreviewObject = null;
shapes = new List<ILightShape>(16);
}
/// <summary>
/// Adds a light to the lookup table.
/// </summary>
/// <param name="source">The source of the light.</param>
/// <param name="owner">The light's owning game object.</param>
internal void AddLight(LightGridEmitter source, GameObject owner) {
if (owner == null)
throw new ArgumentNullException(nameof(owner));
if (source == null)
throw new ArgumentNullException(nameof(source));
// The default equality comparer will be used; since each Light2D is supposed
// to have exactly one LightGridEmitter, this should be fine
brightCache.TryAdd(source, new CacheEntry(owner));
}
public override void Bootstrap(Harmony plibInstance) {
SetSharedData(new List<object>(16));
}
/// <summary>
/// Ends a call to lighting update initiated by CreateLight.
/// </summary>
/// <param name="source">The source of the light.</param>
internal void DestroyLight(LightGridEmitter source) {
if (source != null)
brightCache.TryRemove(source, out _);
}
/// <summary>
/// Gets the brightness at a given cell for the specified light source.
/// </summary>
/// <param name="source">The source of the light.</param>
/// <param name="location">The location to check.</param>
/// <param name="state">The lighting state.</param>
/// <param name="result">The brightness there.</param>
/// <returns>true if that brightness is valid, or false otherwise.</returns>
internal bool GetBrightness(LightGridEmitter source, int location,
LightGridEmitter.State state, out int result) {
bool valid;
var shape = state.shape;
if (shape != LightShape.Cone && shape != LightShape.Circle) {
valid = brightCache.TryGetValue(source, out CacheEntry cacheEntry);
if (valid) {
valid = cacheEntry.Intensity.TryGetValue(location, out float ratio);
if (valid)
result = Mathf.RoundToInt(cacheEntry.BaseLux * ratio);
else {
#if DEBUG
LogLightingDebug("GetBrightness for invalid cell at {0:D}".F(location));
#endif
result = 0;
}
} else {
#if DEBUG
LogLightingDebug("GetBrightness for invalid emitter at {0:D}".F(location));
#endif
result = 0;
}
} else if (ForceSmoothLight) {
// Use smooth light even for vanilla Cone and Circle
result = Mathf.RoundToInt(state.intensity * GetSmoothFalloff(state.falloffRate,
location, state.origin));
valid = true;
} else {
// Stock
result = 0;
valid = false;
}
return valid;
}
/// <summary>
/// Checks to see if a light has specified one of the built-in ray options to cast
/// the little yellow rays around it.
/// </summary>
/// <param name="shape">The light shape to check.</param>
/// <returns>The light shape to use for ray casting, or the original shape if it is
/// a stock shape or a light shape not known to PLib Lighting.</returns>
internal LightShape GetRayShape(LightShape shape) {
int index = shape - LightShape.Cone - 1;
ILightShape ps;
if (index >= 0 && index < shapes.Count && (ps = shapes[index]) != null) {
var newShape = ps.RayMode;
if (newShape >= LightShape.Circle)
shape = newShape;
}
return shape;
}
public override void Initialize(Harmony plibInstance) {
Instance = this;
shapes.Clear();
foreach (var light in GetSharedData(EMPTY_SHAPES)) {
var ls = PRemoteLightWrapper.LightToInstance(light);
shapes.Add(ls);
if (ls == null)
// Moe must clean it!
LogLightingWarning("Foreign contaminant in PLightManager!");
}
LightingPatches.ApplyPatches(plibInstance);
}
/// <summary>
/// Creates the preview for a given light.
/// </summary>
/// <param name="origin">The starting cell.</param>
/// <param name="radius">The light radius.</param>
/// <param name="shape">The light shape.</param>
/// <param name="lux">The base brightness in lux.</param>
/// <returns>true if the lighting was handled, or false otherwise.</returns>
internal bool PreviewLight(int origin, float radius, LightShape shape, int lux) {
bool handled = false;
var owner = PreviewObject;
int index = shape - LightShape.Cone - 1;
if (index >= 0 && index < shapes.Count && owner != null) {
var cells = DictionaryPool<int, float, PLightManager>.Allocate();
// Found handler!
shapes[index]?.FillLight(new LightingArgs(owner, origin,
(int)radius, cells));
foreach (var pair in cells) {
int cell = pair.Key;
if (Grid.IsValidCell(cell)) {
// Allow any fraction, not just linear falloff
int lightValue = Mathf.RoundToInt(lux * pair.Value);
LightGridManager.previewLightCells.Add(new Tuple<int, int>(cell,
lightValue));
LightGridManager.previewLux[cell] = lightValue;
}
}
PreviewObject = null;
handled = true;
cells.Recycle();
}
return handled;
}
/// <summary>
/// Registers a light shape handler.
/// </summary>
/// <param name="identifier">A unique identifier for this shape. If another mod has
/// already registered that identifier, the previous mod will take precedence.</param>
/// <param name="handler">The handler for that shape.</param>
/// <param name="rayMode">The type of visual rays that are displayed from the light.</param>
/// <returns>The light shape which can be used.</returns>
public ILightShape Register(string identifier, CastLightDelegate handler,
LightShape rayMode = (LightShape)(-1)) {
if (string.IsNullOrEmpty(identifier))
throw new ArgumentNullException(nameof(identifier));
if (handler == null)
throw new ArgumentNullException(nameof(handler));
ILightShape lightShape = null;
RegisterForForwarding();
// Try to find a match for this identifier
var registered = GetSharedData(EMPTY_SHAPES);
int n = registered.Count;
foreach (var obj in registered) {
var light = PRemoteLightWrapper.LightToInstance(obj);
// Might be from another assembly so the types may or may not be compatible
if (light != null && light.Identifier == identifier) {
LogLightingDebug("Found existing light shape: " + identifier + " from " +
(obj.GetType().Assembly.GetNameSafe() ?? "?"));
lightShape = light;
break;
}
}
if (lightShape == null) {
// Not currently existing
lightShape = new PLightShape(n + 1, identifier, handler, rayMode);
LogLightingDebug("Registered new light shape: " + identifier);
registered.Add(lightShape);
}
return lightShape;
}
/// <summary>
/// Updates the lit cells list.
/// </summary>
/// <param name="source">The source of the light.</param>
/// <param name="state">The light emitter state.</param>
/// <param name="litCells">The location where lit cells will be placed.</param>
/// <returns>true if the lighting was handled, or false otherwise.</returns>
internal bool UpdateLitCells(LightGridEmitter source, LightGridEmitter.State state,
IList<int> litCells) {
bool handled = false;
int index = state.shape - LightShape.Cone - 1;
if (source == null)
throw new ArgumentNullException(nameof(source));
if (index >= 0 && index < shapes.Count && litCells != null && brightCache.
TryGetValue(source, out CacheEntry entry)) {
var ps = shapes[index];
var brightness = entry.Intensity;
// Proper owner found
brightness.Clear();
entry.BaseLux = state.intensity;
ps.FillLight(new LightingArgs(entry.Owner, state.origin, (int)state.
radius, brightness));
foreach (var point in brightness)
litCells.Add(point.Key);
handled = true;
}
return handled;
}
/// <summary>
/// A cache entry in the light brightness cache.
/// </summary>
private sealed class CacheEntry {
/// <summary>
/// The base intensity in lux.
/// </summary>
internal int BaseLux { get; set; }
/// <summary>
/// The relative brightness per cell.
/// </summary>
internal BrightnessDict Intensity { get; }
/// <summary>
/// The owner which initiated the lighting call.
/// </summary>
internal GameObject Owner { get; }
internal CacheEntry(GameObject owner) {
// Do not use the pool because these might last a long time and be numerous
Intensity = new Dictionary<int, float>(64);
Owner = owner;
}
public override string ToString() {
return "Lighting Cache Entry for " + (Owner == null ? "" : Owner.name);
}
}
}
}