/* * 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; using LightGridEmitter = LightGridManager.LightGridEmitter; namespace PeterHan.PLib.Lighting { /// /// Manages lighting. Instantiated only by the latest PLib version. /// public sealed class PLightManager : PForwardedComponent { /// /// Implemented by classes which want to handle lighting calls. /// /// The parameters to use for lighting, and the location to /// store results. See the LightingArgs class documentation for details. public delegate void CastLightDelegate(LightingArgs args); /// /// A singleton empty list instance for default values. /// private static readonly List EMPTY_SHAPES = new List(1); /// /// The version of this component. Uses the running PLib version. /// internal static readonly Version VERSION = new Version(PVersion.VERSION); /// /// If true, enables the smooth light falloff mode even on vanilla lights. /// internal static bool ForceSmoothLight { get; set; } /// /// The instantiated copy of this class. /// internal static PLightManager Instance { get; private set; } /// /// Calculates the brightness falloff as it would be in the stock game. /// /// The falloff rate to use. /// The cell where falloff is being computed. /// The light origin cell. /// The brightness at that location from 0 to 1. 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))); } /// /// Calculates the brightness falloff similar to the default falloff, but far smoother. /// Slightly heavier on computation however. /// /// The falloff rate to use. /// The cell where falloff is being computed. /// The light origin cell. /// The brightness at that location from 0 to 1. 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)); } /// /// Gets the raycasting shape to use for the given light. /// /// The light which is being drawn. /// The shape to use for its rays. internal static LightShape LightShapeToRayShape(Light2D light) { var shape = light.shape; if (shape != LightShape.Cone && shape != LightShape.Circle) shape = Instance.GetRayShape(shape); return shape; } /// /// Logs a message encountered by the PLib lighting system. /// /// The debug message. internal static void LogLightingDebug(string message) { Debug.LogFormat("[PLibLighting] {0}", message); } /// /// Logs a warning encountered by the PLib lighting system. /// /// The warning message. internal static void LogLightingWarning(string message) { Debug.LogWarningFormat("[PLibLighting] {0}", message); } public override Version Version => VERSION; /// /// The light brightness set by the last lighting brightness request. /// private readonly ConcurrentDictionary brightCache; /// /// The last object that requested a preview. Only one preview can be requested at a /// time, so no need for thread safety. /// internal GameObject PreviewObject { get; set; } /// /// The lighting shapes available, all in this mod's namespace. /// private readonly IList shapes; /// /// Creates a lighting manager to register PLib lighting. /// public PLightManager() { // Needs to be thread safe! brightCache = new ConcurrentDictionary(2, 128); PreviewObject = null; shapes = new List(16); } /// /// Adds a light to the lookup table. /// /// The source of the light. /// The light's owning game object. 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(16)); } /// /// Ends a call to lighting update initiated by CreateLight. /// /// The source of the light. internal void DestroyLight(LightGridEmitter source) { if (source != null) brightCache.TryRemove(source, out _); } /// /// Gets the brightness at a given cell for the specified light source. /// /// The source of the light. /// The location to check. /// The lighting state. /// The brightness there. /// true if that brightness is valid, or false otherwise. 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; } /// /// Checks to see if a light has specified one of the built-in ray options to cast /// the little yellow rays around it. /// /// The light shape to check. /// 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. 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); } /// /// Creates the preview for a given light. /// /// The starting cell. /// The light radius. /// The light shape. /// The base brightness in lux. /// true if the lighting was handled, or false otherwise. 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.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(cell, lightValue)); LightGridManager.previewLux[cell] = lightValue; } } PreviewObject = null; handled = true; cells.Recycle(); } return handled; } /// /// Registers a light shape handler. /// /// A unique identifier for this shape. If another mod has /// already registered that identifier, the previous mod will take precedence. /// The handler for that shape. /// The type of visual rays that are displayed from the light. /// The light shape which can be used. 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; } /// /// Updates the lit cells list. /// /// The source of the light. /// The light emitter state. /// The location where lit cells will be placed. /// true if the lighting was handled, or false otherwise. internal bool UpdateLitCells(LightGridEmitter source, LightGridEmitter.State state, IList 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; } /// /// A cache entry in the light brightness cache. /// private sealed class CacheEntry { /// /// The base intensity in lux. /// internal int BaseLux { get; set; } /// /// The relative brightness per cell. /// internal BrightnessDict Intensity { get; } /// /// The owner which initiated the lighting call. /// 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(64); Owner = owner; } public override string ToString() { return "Lighting Cache Entry for " + (Owner == null ? "" : Owner.name); } } } }