Initial commit

This commit is contained in:
2022-12-30 00:58:33 +01:00
commit 3f1075e67f
176 changed files with 25715 additions and 0 deletions

View File

@@ -0,0 +1,45 @@
/*
* 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.
*/
namespace PeterHan.PLib.Lighting {
/// <summary>
/// An interface describing local and remote instances of PLightShape.
/// </summary>
public interface ILightShape {
/// <summary>
/// The light shape identifier.
/// </summary>
string Identifier { get; }
/// <summary>
/// The Klei LightShape represented by this light shape, used in Light2D definitions.
/// </summary>
LightShape KleiLightShape { get; }
/// <summary>
/// The raycast mode used by this light shape. (-1) if no rays are to be emitted.
/// </summary>
LightShape RayMode { get; }
/// <summary>
/// Invokes the light handler with the provided light information.
/// </summary>
/// <param name="args">The arguments passed to the user light handler.</param>
void FillLight(LightingArgs args);
}
}

View File

@@ -0,0 +1,133 @@
/*
* 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 System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace PeterHan.PLib.Lighting {
/// <summary>
/// Arguments which are passed to lighting callbacks to perform lighting calculations.
///
/// The range is the light radius supplied during the Light2D creation; do not light up
/// tiles outside of this radius (measured by a square around SourceCell)!
///
/// The source cell is the cell nearest to where the Light2D is currently located.
///
/// Use the IDictionary interface to store the relative brightness of cells by their cell
/// location. These values should be between 0 and 1 normally, with the maximum brightness
/// being set by the intensity parameter of the Light2D. The user is responsible for
/// ensuring that cells are valid before lighting them up.
/// </summary>
public sealed class LightingArgs : EventArgs, IDictionary<int, float> {
/// <summary>
/// The location where lighting results are stored.
/// </summary>
public IDictionary<int, float> Brightness { get; }
/// <summary>
/// The maximum range to use for cell lighting. Do not light up cells beyond this
/// range from SourceCell.
/// </summary>
public int Range { get; }
/// <summary>
/// The source of the light.
/// </summary>
public GameObject Source { get; }
/// <summary>
/// The originating cell. Actual lighting can begin elsewhere, but the range limit is
/// measured from this cell.
/// </summary>
public int SourceCell { get; }
internal LightingArgs(GameObject source, int cell, int range,
IDictionary<int, float> output) {
if (source == null)
// Cannot use "throw" expression because of UnityEngine.Object.operator==
throw new ArgumentNullException(nameof(source));
Brightness = output ?? throw new ArgumentNullException(nameof(output));
Range = range;
Source = source;
SourceCell = cell;
}
#region IDictionary
public ICollection<int> Keys => Brightness.Keys;
public ICollection<float> Values => Brightness.Values;
public int Count => Brightness.Count;
public bool IsReadOnly => Brightness.IsReadOnly;
public float this[int key] { get => Brightness[key]; set => Brightness[key] = value; }
public bool ContainsKey(int key) {
return Brightness.ContainsKey(key);
}
public void Add(int key, float value) {
Brightness.Add(key, value);
}
public bool Remove(int key) {
return Brightness.Remove(key);
}
public bool TryGetValue(int key, out float value) {
return Brightness.TryGetValue(key, out value);
}
public void Add(KeyValuePair<int, float> item) {
Brightness.Add(item);
}
public void Clear() {
Brightness.Clear();
}
public bool Contains(KeyValuePair<int, float> item) {
return Brightness.Contains(item);
}
public void CopyTo(KeyValuePair<int, float>[] array, int arrayIndex) {
Brightness.CopyTo(array, arrayIndex);
}
public bool Remove(KeyValuePair<int, float> item) {
return Brightness.Remove(item);
}
public IEnumerator<KeyValuePair<int, float>> GetEnumerator() {
return Brightness.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator() {
return Brightness.GetEnumerator();
}
#endregion
public override string ToString() {
return string.Format("LightingArgs[source={0:D},range={1:D}]", SourceCell, Range);
}
}
}

View File

@@ -0,0 +1,170 @@
/*
* 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 PeterHan.PLib.Detours;
using System;
using System.Collections.Generic;
using UnityEngine;
using IntHandle = HandleVector<int>.Handle;
using LightGridEmitter = LightGridManager.LightGridEmitter;
using TranspiledMethod = System.Collections.Generic.IEnumerable<HarmonyLib.CodeInstruction>;
namespace PeterHan.PLib.Lighting {
/// <summary>
/// Contains all patches (many!) required by the PLib Lighting subsystem. Only applied by
/// the latest version of PLightManager.
/// </summary>
internal static class LightingPatches {
private delegate IntHandle AddToLayerDelegate(Light2D instance, Extents ext,
ScenePartitionerLayer layer);
private static readonly DetouredMethod<AddToLayerDelegate> ADD_TO_LAYER =
typeof(Light2D).DetourLazy<AddToLayerDelegate>("AddToLayer");
private static readonly IDetouredField<Light2D, int> ORIGIN = PDetours.
DetourFieldLazy<Light2D, int>("origin");
private static readonly IDetouredField<LightShapePreview, int> PREVIOUS_CELL =
PDetours.DetourFieldLazy<LightShapePreview, int>("previousCell");
private static bool ComputeExtents_Prefix(Light2D __instance, ref Extents __result) {
var lm = PLightManager.Instance;
bool cont = true;
if (lm != null && __instance != null) {
var shape = __instance.shape;
int rad = Mathf.CeilToInt(__instance.Range), cell;
lm.AddLight(__instance.emitter, __instance.gameObject);
if (shape > LightShape.Cone && rad > 0 && Grid.IsValidCell(cell = ORIGIN.Get(
__instance))) {
Grid.CellToXY(cell, out int x, out int y);
// Better safe than sorry, check whole possible radius
__result = new Extents(x - rad, y - rad, 2 * rad, 2 * rad);
cont = false;
}
}
return cont;
}
/// <summary>
/// Applies the required lighting related patches.
/// </summary>
/// <param name="plibInstance">The Harmony instance to use for patching.</param>
public static void ApplyPatches(Harmony plibInstance) {
// Light2D
plibInstance.Patch(typeof(Light2D), "ComputeExtents",
prefix: PatchMethod(nameof(ComputeExtents_Prefix)));
plibInstance.Patch(typeof(Light2D), nameof(Light2D.FullRemove), postfix:
PatchMethod(nameof(Light2D_FullRemove_Postfix)));
plibInstance.Patch(typeof(Light2D), nameof(Light2D.RefreshShapeAndPosition),
postfix: PatchMethod(nameof(Light2D_RefreshShapeAndPosition_Postfix)));
// LightBuffer
try {
plibInstance.PatchTranspile(typeof(LightBuffer), "LateUpdate", PatchMethod(
nameof(LightBuffer_LateUpdate_Transpile)));
} catch (Exception e) {
// Only visual, log as warning
PUtil.LogExcWarn(e);
}
// LightGridEmitter
plibInstance.Patch(typeof(LightGridEmitter), "ComputeLux", prefix:
PatchMethod(nameof(ComputeLux_Prefix)));
plibInstance.Patch(typeof(LightGridEmitter), nameof(LightGridEmitter.
UpdateLitCells), prefix: PatchMethod(nameof(UpdateLitCells_Prefix)));
// LightGridManager
plibInstance.Patch(typeof(LightGridManager), nameof(LightGridManager.
CreatePreview), prefix: PatchMethod(nameof(CreatePreview_Prefix)));
// LightShapePreview
plibInstance.Patch(typeof(LightShapePreview), "Update", prefix:
PatchMethod(nameof(LightShapePreview_Update_Prefix)));
// Rotatable
plibInstance.Patch(typeof(Rotatable), "OrientVisualizer", postfix:
PatchMethod(nameof(OrientVisualizer_Postfix)));
}
private static bool ComputeLux_Prefix(LightGridEmitter __instance, int cell,
LightGridEmitter.State ___state, ref int __result) {
var lm = PLightManager.Instance;
return lm == null || !lm.GetBrightness(__instance, cell, ___state, out __result);
}
private static bool CreatePreview_Prefix(int origin_cell, float radius,
LightShape shape, int lux) {
var lm = PLightManager.Instance;
return lm == null || !lm.PreviewLight(origin_cell, radius, shape, lux);
}
private static void Light2D_FullRemove_Postfix(Light2D __instance) {
var lm = PLightManager.Instance;
if (lm != null && __instance != null)
lm.DestroyLight(__instance.emitter);
}
private static void Light2D_RefreshShapeAndPosition_Postfix(Light2D __instance,
Light2D.RefreshResult __result) {
var lm = PLightManager.Instance;
if (lm != null && __instance != null && __result == Light2D.RefreshResult.Updated)
lm.AddLight(__instance.emitter, __instance.gameObject);
}
private static TranspiledMethod LightBuffer_LateUpdate_Transpile(
TranspiledMethod body) {
var target = typeof(Light2D).GetPropertySafe<LightShape>(nameof(Light2D.shape),
false)?.GetGetMethod(true);
return (target == null) ? body : PPatchTools.ReplaceMethodCallSafe(body, target,
typeof(PLightManager).GetMethodSafe(nameof(PLightManager.LightShapeToRayShape),
true, typeof(Light2D)));
}
private static void LightShapePreview_Update_Prefix(LightShapePreview __instance) {
var lm = PLightManager.Instance;
// Pass the preview object into LightGridManager
if (lm != null && __instance != null)
lm.PreviewObject = __instance.gameObject;
}
private static void OrientVisualizer_Postfix(Rotatable __instance) {
// Force regeneration on next Update()
if (__instance != null && __instance.TryGetComponent(out LightShapePreview
preview))
PREVIOUS_CELL.Set(preview, -1);
}
/// <summary>
/// Gets a HarmonyMethod instance for manual patching using a method from this class.
/// </summary>
/// <param name="name">The method name.</param>
/// <returns>A reference to that method as a HarmonyMethod for patching.</returns>
private static HarmonyMethod PatchMethod(string name) {
return new HarmonyMethod(typeof(LightingPatches), name);
}
private static bool UpdateLitCells_Prefix(LightGridEmitter __instance,
List<int> ___litCells, LightGridEmitter.State ___state) {
var lm = PLightManager.Instance;
return lm == null || !lm.UpdateLitCells(__instance, ___state, ___litCells);
}
}
}

View File

@@ -0,0 +1,116 @@
/*
* 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 PeterHan.PLib.Core;
using PeterHan.PLib.Detours;
using System;
using System.Collections.Generic;
using BrightnessDict = System.Collections.Generic.IDictionary<int, float>;
using Octant = DiscreteShadowCaster.Octant;
namespace PeterHan.PLib.Lighting {
/// <summary>
/// A builder class which creates default light patterns based on octants.
/// </summary>
public sealed class OctantBuilder {
/// <summary>
/// The delegate type called to run the default DiscreteShadowCaster.ScanOctant.
/// </summary>
private delegate void ScanOctantFunc(Vector2I cellPos, int range, int depth,
Octant octant, double startSlope, double endSlope, List<int> visiblePoints);
/// <summary>
/// The method to call to scan octants.
/// </summary>
private static readonly ScanOctantFunc OCTANT_SCAN;
static OctantBuilder() {
// Cache the method for faster execution
OCTANT_SCAN = typeof(DiscreteShadowCaster).Detour<ScanOctantFunc>("ScanOctant");
if (OCTANT_SCAN == null)
PLightManager.LogLightingWarning("OctantBuilder cannot find default octant scanner!");
}
/// <summary>
/// The fallout to use when building the light.
/// </summary>
public float Falloff { get; set; }
/// <summary>
/// If false, uses the default game smoothing. If true, uses better smoothing.
/// </summary>
public bool SmoothLight { get; set; }
/// <summary>
/// The origin cell.
/// </summary>
public int SourceCell { get; }
/// <summary>
/// The location where light cells are added.
/// </summary>
private readonly BrightnessDict destination;
/// <summary>
/// Creates a new octant builder.
/// </summary>
/// <param name="destination">The location where the lit cells will be placed.</param>
/// <param name="sourceCell">The origin cell of the light.</param>
public OctantBuilder(BrightnessDict destination, int sourceCell) {
if (!Grid.IsValidCell(sourceCell))
throw new ArgumentOutOfRangeException(nameof(sourceCell));
this.destination = destination ?? throw new ArgumentNullException(nameof(
destination));
destination[sourceCell] = 1.0f;
Falloff = 0.5f;
// Use the default game's light algorithm
SmoothLight = false;
SourceCell = sourceCell;
}
/// <summary>
/// Adds an octant of light.
/// </summary>
/// <param name="range">The range of the light.</param>
/// <param name="octant">The octant to scan.</param>
/// <returns>This object, for call chaining.</returns>
public OctantBuilder AddOctant(int range, Octant octant) {
var points = ListPool<int, OctantBuilder>.Allocate();
OCTANT_SCAN?.Invoke(Grid.CellToXY(SourceCell), range, 1, octant, 1.0, 0.0, points);
// Transfer to our array using:
foreach (int cell in points) {
float intensity;
if (SmoothLight)
// Better, not rounded falloff
intensity = PLightManager.GetSmoothFalloff(Falloff, cell, SourceCell);
else
// Default falloff
intensity = PLightManager.GetDefaultFalloff(Falloff, cell, SourceCell);
destination[cell] = intensity;
}
points.Recycle();
return this;
}
public override string ToString() {
return string.Format("OctantBuilder[Cell {0:D}, {1:D} lit]", SourceCell,
destination.Count);
}
}
}

View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Title>PLib Lighting</Title>
<AssemblyTitle>PLib.Lighting</AssemblyTitle>
<Version>4.11.0.0</Version>
<UsesPLib>false</UsesPLib>
<RootNamespace>PeterHan.PLib.Lighting</RootNamespace>
<AssemblyVersion>4.11.0.0</AssemblyVersion>
<DistributeMod>false</DistributeMod>
<PLibCore>true</PLibCore>
<Platforms>Vanilla;Mergedown</Platforms>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<DocumentationFile>bin\$(Platform)\Release\PLibLighting.xml</DocumentationFile>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="../PLibCore/PLibCore.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,378 @@
/*
* 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);
}
}
}
}

View File

@@ -0,0 +1,85 @@
/*
* 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 System;
using UnityEngine;
using BrightnessDict = System.Collections.Generic.IDictionary<int, float>;
namespace PeterHan.PLib.Lighting {
/// <summary>
/// Represents a light shape which can be used by mods.
/// </summary>
internal sealed class PLightShape : ILightShape {
/// <summary>
/// The handler for this light shape.
/// </summary>
private readonly PLightManager.CastLightDelegate handler;
public string Identifier { get; }
public LightShape KleiLightShape {
get {
return ShapeID + LightShape.Cone;
}
}
public LightShape RayMode { get; }
/// <summary>
/// The light shape ID.
/// </summary>
internal int ShapeID { get; }
internal PLightShape(int id, string identifier, PLightManager.CastLightDelegate handler,
LightShape rayMode) {
this.handler = handler ?? throw new ArgumentNullException(nameof(handler));
Identifier = identifier ?? throw new ArgumentNullException(nameof(identifier));
RayMode = rayMode;
ShapeID = id;
}
public override bool Equals(object obj) {
return obj is PLightShape other && other.Identifier == Identifier;
}
/// <summary>
/// Invokes the light handler with the provided light information.
/// </summary>
/// <param name="source">The source of the light.</param>
/// <param name="cell">The origin cell.</param>
/// <param name="range">The range to fill.</param>
/// <param name="brightness">The location where lit points will be stored.</param>
internal void DoFillLight(GameObject source, int cell, int range,
BrightnessDict brightness) {
handler.Invoke(new LightingArgs(source, cell, range, brightness));
}
public void FillLight(LightingArgs args) {
handler.Invoke(args);
}
public override int GetHashCode() {
return Identifier.GetHashCode();
}
public override string ToString() {
return "PLightShape[ID=" + Identifier + "]";
}
}
}

View File

@@ -0,0 +1,83 @@
/*
* 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 PeterHan.PLib.Core;
using System;
using UnityEngine;
using BrightnessDict = System.Collections.Generic.IDictionary<int, float>;
namespace PeterHan.PLib.Lighting {
/// <summary>
/// Wraps a lighting system call from another mod's namespace.
/// </summary>
internal sealed class PRemoteLightWrapper : ILightShape {
// The delegate type covering calls to FillLight from other mods.
private delegate void FillLightDelegate(GameObject source, int cell, int range,
BrightnessDict brightness);
/// <summary>
/// Creates a light shape instance from another mod.
/// </summary>
/// <param name="other">The object to convert.</param>
/// <returns>A light shape object in this mod's namespace that delegates lighting
/// calls to the other mod if necessary.</returns>
internal static ILightShape LightToInstance(object other) {
return (other == null || other.GetType().Name != nameof(PLightShape)) ? null :
((other is ILightShape ls) ? ls : new PRemoteLightWrapper(other));
}
/// <summary>
/// The method to call when lighting system handling is requested.
/// </summary>
private readonly FillLightDelegate fillLight;
public string Identifier { get; }
public LightShape KleiLightShape { get; }
public LightShape RayMode { get; }
internal PRemoteLightWrapper(object other) {
if (other == null)
throw new ArgumentNullException(nameof(other));
if (!PPatchTools.TryGetPropertyValue(other, nameof(ILightShape.KleiLightShape),
out LightShape ls))
throw new ArgumentException("Light shape is missing KleiLightShape");
KleiLightShape = ls;
if (!PPatchTools.TryGetPropertyValue(other, nameof(ILightShape.Identifier),
out string id) || id == null)
throw new ArgumentException("Light shape is missing Identifier");
Identifier = id;
if (!PPatchTools.TryGetPropertyValue(other, nameof(ILightShape.RayMode),
out LightShape rm))
rm = (LightShape)(-1);
RayMode = rm;
var otherType = other.GetType();
fillLight = otherType.CreateDelegate<FillLightDelegate>(nameof(PLightShape.
DoFillLight), other, typeof(GameObject), typeof(int), typeof(int),
typeof(BrightnessDict));
if (fillLight == null)
throw new ArgumentException("Light shape is missing FillLight");
}
public void FillLight(LightingArgs args) {
fillLight.Invoke(args.Source, args.SourceCell, args.Range, args.Brightness);
}
}
}