BaseGamePerf: GC smoothing and Nature Renderer grass streaming caps
Spreads incremental GC across more frames and caps grass-cell work so abrupt camera looks hitch less. Density, draw distance, and simulation stay the same. Original values restore when the module is disabled.
This commit is contained in:
parent
7acf6b6841
commit
e8aeb2d6a3
7 changed files with 516 additions and 0 deletions
15
README.md
15
README.md
|
|
@ -19,6 +19,7 @@ I originally planned on releasing individual mods, but considering my workflow o
|
|||
| Map Module | In-game map overlay and detachable popout window for a second monitor. Themes, custom colors, opacity controls, map rotation, track & industry labels, and optional MapEnhancer integration. |
|
||||
| Physics Optimizer | Cuts CPU spent on train physics (LOD fast-path + auto-freeze), with debug car tinting. Console: `/rpf` |
|
||||
| Mesh LOD | Adds level-of-detail to rolling stock: distant cars progressively shed detail and finally collapse to a cheap proxy box, cutting triangle count on large saves. |
|
||||
| Base Game Performance | Smooths Unity's incremental garbage collector and Nature Renderer grass streaming to reduce camera-motion hitches without lowering visual quality. |
|
||||
| Profiler | Unified in-game performance overlay: a frame-time graph plus live readouts that adapt to whichever optimization modules are enabled. Console: `/rpf overlay` |
|
||||
|
||||
All modules are disabled by default; enable them per-module from the S³ settings page. A game restart is required for enable/disable to take effect. More modules will follow; S³ is designed to grow.
|
||||
|
|
@ -159,6 +160,20 @@ All three LOD reductions side by side, with the transition distances set artific
|
|||
|
||||
---
|
||||
|
||||
## Base Game Performance
|
||||
|
||||
Direct per-frame profiling found that the repeatable base-game stalls were garbage-collection
|
||||
frames rather than car culling, scenery loading, world streaming, or camera ground queries.
|
||||
This module reduces Unity's incremental GC slice from the game's 3 ms default to a configurable
|
||||
1 ms starting point. The work is spread across more frames, reducing individual stalls without
|
||||
changing resolution, shadows, draw distance, scenery, rolling-stock detail, or simulation.
|
||||
|
||||
It can also cap Nature Renderer grass-cell streaming (instance budget, queued nearby loads,
|
||||
and staggered cell expiry) so abrupt camera looks hitch less. Density and draw distance stay
|
||||
the same. The original runtime values are restored immediately when the module is disabled.
|
||||
|
||||
---
|
||||
|
||||
## Profiler
|
||||
|
||||
A unified in-game performance overlay. It always shows a render + physics frame-time graph (render, FixedUpdate, Tick, and PosCars times) and a timing report, and it grows extra sections for whichever optimization modules are enabled:
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ public static class Main
|
|||
// constructor. Order here is display order in the settings panel.
|
||||
_registry.Register(new Modules.PhysicsOptimizer.PhysicsOptimizerModule());
|
||||
_registry.Register(new Modules.MeshLod.MeshLodModule());
|
||||
_registry.Register(new Modules.BaseGamePerf.BaseGamePerfModule());
|
||||
_registry.Register(new Modules.Profiler.ProfilerModule());
|
||||
_registry.Register(new Modules.Popout.PopoutModule());
|
||||
|
||||
|
|
|
|||
82
src/Modules/BaseGamePerf/BaseGamePerfModule.cs
Normal file
82
src/Modules/BaseGamePerf/BaseGamePerfModule.cs
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
using S3.Core;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Scripting;
|
||||
|
||||
namespace S3.Modules.BaseGamePerf;
|
||||
|
||||
public sealed class BaseGamePerfModule : IModule
|
||||
{
|
||||
const string SettingsFile = "S3.basegame.json";
|
||||
|
||||
static bool _hasOriginal;
|
||||
static ulong _originalSliceNanoseconds;
|
||||
|
||||
public static BaseGamePerfSettings Settings { get; private set; } = new();
|
||||
|
||||
public BaseGamePerfModule() =>
|
||||
Settings = SettingsStore.Load<BaseGamePerfSettings>(SettingsFile);
|
||||
|
||||
public string Id => "basegame";
|
||||
public string DisplayName => "Base Game Performance";
|
||||
public string Description =>
|
||||
"Evidence-backed vanilla-game hitch controls. Smooths incremental GC and " +
|
||||
"Nature Renderer grass-cell streaming without reducing density, draw distance, " +
|
||||
"resolution, or simulation quality.";
|
||||
|
||||
public bool Enabled
|
||||
{
|
||||
get => Settings.enabled;
|
||||
set => Settings.enabled = value;
|
||||
}
|
||||
|
||||
public void OnEnable()
|
||||
{
|
||||
if (!_hasOriginal)
|
||||
{
|
||||
_originalSliceNanoseconds = GarbageCollector.incrementalTimeSliceNanoseconds;
|
||||
_hasOriginal = true;
|
||||
}
|
||||
try { NatureStreamingOptimizer.Enable(); }
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
Log.Warn(
|
||||
"[basegame] Nature streaming smoothing unavailable: " +
|
||||
ex.GetBaseException().Message);
|
||||
}
|
||||
ApplyRuntimeSettings();
|
||||
}
|
||||
|
||||
public void OnDisable()
|
||||
{
|
||||
NatureStreamingOptimizer.Disable();
|
||||
if (_hasOriginal)
|
||||
{
|
||||
GarbageCollector.incrementalTimeSliceNanoseconds = _originalSliceNanoseconds;
|
||||
Log.Info(
|
||||
$"[basegame] Restored incremental GC slice to " +
|
||||
$"{_originalSliceNanoseconds / 1_000_000.0:0.###}ms.");
|
||||
}
|
||||
}
|
||||
|
||||
public static void ApplyRuntimeSettings()
|
||||
{
|
||||
if (!GarbageCollector.isIncremental)
|
||||
{
|
||||
Log.Warn("[basegame] Unity incremental GC is disabled; smoothing was not applied.");
|
||||
return;
|
||||
}
|
||||
|
||||
ulong desired = Settings.gcSmoothingEnabled
|
||||
? (ulong)(Mathf.Clamp(Settings.incrementalSliceMs, 0.25f, 5f) * 1_000_000f)
|
||||
: _originalSliceNanoseconds;
|
||||
GarbageCollector.incrementalTimeSliceNanoseconds = desired;
|
||||
Log.Info(
|
||||
$"[basegame] Incremental GC slice={desired / 1_000_000.0:0.###}ms " +
|
||||
$"(smoothing={Settings.gcSmoothingEnabled}).");
|
||||
NatureStreamingOptimizer.ApplySettings();
|
||||
}
|
||||
|
||||
public void SaveSettings() => Persist();
|
||||
public static void Persist() => SettingsStore.Save(SettingsFile, Settings);
|
||||
public void DrawSettings() => BaseGamePerfSettingsUI.Draw();
|
||||
}
|
||||
16
src/Modules/BaseGamePerf/BaseGamePerfSettings.cs
Normal file
16
src/Modules/BaseGamePerf/BaseGamePerfSettings.cs
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
using System;
|
||||
|
||||
namespace S3.Modules.BaseGamePerf;
|
||||
|
||||
[Serializable]
|
||||
public sealed class BaseGamePerfSettings
|
||||
{
|
||||
public bool enabled = false;
|
||||
public bool gcSmoothingEnabled = true;
|
||||
public float incrementalSliceMs = 1f;
|
||||
public bool natureStreamingSmoothingEnabled = true;
|
||||
public int grassInstanceBudgetPerFrame = 256;
|
||||
public bool queueNearbyGrassLoads = true;
|
||||
public int grassUnloadSpreadFrames = 120;
|
||||
public bool distanceCullNatureTerrains = false;
|
||||
}
|
||||
112
src/Modules/BaseGamePerf/BaseGamePerfSettingsUI.cs
Normal file
112
src/Modules/BaseGamePerf/BaseGamePerfSettingsUI.cs
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
using UnityEngine;
|
||||
|
||||
namespace S3.Modules.BaseGamePerf;
|
||||
|
||||
static class BaseGamePerfSettingsUI
|
||||
{
|
||||
public static void Draw()
|
||||
{
|
||||
BaseGamePerfSettings s = BaseGamePerfModule.Settings;
|
||||
bool changed = false;
|
||||
|
||||
GUILayout.BeginVertical();
|
||||
GUILayout.Label("<b>Base Game Performance</b> — conservative vanilla hitch controls");
|
||||
GUILayout.Space(4f);
|
||||
GUILayout.Label(
|
||||
" Smooths Unity's incremental garbage collection. This does not alter\n" +
|
||||
" resolution, shadows, draw distance, scenery, or rolling-stock detail.",
|
||||
GUI.skin.label);
|
||||
GUILayout.Space(8f);
|
||||
|
||||
bool enabled = GUILayout.Toggle(
|
||||
s.gcSmoothingEnabled,
|
||||
" Shorter incremental GC slices");
|
||||
if (enabled != s.gcSmoothingEnabled)
|
||||
{
|
||||
s.gcSmoothingEnabled = enabled;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
GUILayout.BeginHorizontal();
|
||||
GUILayout.Label($"Slice budget: {s.incrementalSliceMs:0.00}ms", GUILayout.Width(150f));
|
||||
float slice = GUILayout.HorizontalSlider(
|
||||
s.incrementalSliceMs, 0.25f, 3f, GUILayout.Width(200f));
|
||||
GUILayout.EndHorizontal();
|
||||
if (Mathf.Abs(slice - s.incrementalSliceMs) > 0.01f)
|
||||
{
|
||||
s.incrementalSliceMs = slice;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
GUILayout.Label(
|
||||
" Lower values reduce individual GC stalls but spread collection work\n" +
|
||||
" across more frames. 1.00ms is the measured starting point.",
|
||||
GUI.skin.label);
|
||||
GUILayout.Space(10f);
|
||||
|
||||
bool nature = GUILayout.Toggle(
|
||||
s.natureStreamingSmoothingEnabled,
|
||||
" Smooth Nature Renderer grass streaming");
|
||||
if (nature != s.natureStreamingSmoothingEnabled)
|
||||
{
|
||||
s.natureStreamingSmoothingEnabled = nature;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
GUILayout.BeginHorizontal();
|
||||
GUILayout.Label(
|
||||
$"Grass load budget: {s.grassInstanceBudgetPerFrame} instances/terrain",
|
||||
GUILayout.Width(250f));
|
||||
int grassBudget = Mathf.RoundToInt(GUILayout.HorizontalSlider(
|
||||
s.grassInstanceBudgetPerFrame, 64f, 1024f, GUILayout.Width(200f)));
|
||||
GUILayout.EndHorizontal();
|
||||
if (grassBudget != s.grassInstanceBudgetPerFrame)
|
||||
{
|
||||
s.grassInstanceBudgetPerFrame = grassBudget;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
bool queueNearby = GUILayout.Toggle(
|
||||
s.queueNearbyGrassLoads,
|
||||
" Queue nearby grass cells instead of force-loading them");
|
||||
if (queueNearby != s.queueNearbyGrassLoads)
|
||||
{
|
||||
s.queueNearbyGrassLoads = queueNearby;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
GUILayout.BeginHorizontal();
|
||||
GUILayout.Label(
|
||||
$"Grass unload spread: {s.grassUnloadSpreadFrames} frames",
|
||||
GUILayout.Width(250f));
|
||||
int unloadSpread = Mathf.RoundToInt(GUILayout.HorizontalSlider(
|
||||
s.grassUnloadSpreadFrames, 0f, 300f, GUILayout.Width(200f)));
|
||||
GUILayout.EndHorizontal();
|
||||
if (unloadSpread != s.grassUnloadSpreadFrames)
|
||||
{
|
||||
s.grassUnloadSpreadFrames = unloadSpread;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
bool distanceCull = GUILayout.Toggle(
|
||||
s.distanceCullNatureTerrains,
|
||||
" Unload Nature Renderer data for distant terrain tiles (experimental)");
|
||||
if (distanceCull != s.distanceCullNatureTerrains)
|
||||
{
|
||||
s.distanceCullNatureTerrains = distanceCull;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
GUILayout.Label(
|
||||
" The default 256/120 settings preserve density and draw distance while\n" +
|
||||
" spreading cell uploads and expiry across frames. Distant-terrain\n" +
|
||||
" lifecycle is off by default until portal-camera visuals are validated.",
|
||||
GUI.skin.label);
|
||||
GUILayout.EndVertical();
|
||||
|
||||
if (!changed) return;
|
||||
if (Main.Registry.IsActive("basegame"))
|
||||
BaseGamePerfModule.ApplyRuntimeSettings();
|
||||
BaseGamePerfModule.Persist();
|
||||
}
|
||||
}
|
||||
258
src/Modules/BaseGamePerf/NatureStreamingOptimizer.cs
Normal file
258
src/Modules/BaseGamePerf/NatureStreamingOptimizer.cs
Normal file
|
|
@ -0,0 +1,258 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using HarmonyLib;
|
||||
using S3.Core;
|
||||
using UnityEngine;
|
||||
using VisualDesignCafe.Rendering.Instancing;
|
||||
using VisualDesignCafe.Rendering.Nature;
|
||||
|
||||
namespace S3.Modules.BaseGamePerf;
|
||||
|
||||
/// <summary>
|
||||
/// Conservative smoothing for Nature Renderer's terrain-detail streaming.
|
||||
/// It changes scheduling and residency only; density, render distance,
|
||||
/// materials, shadows, and terrain content are untouched.
|
||||
/// </summary>
|
||||
static class NatureStreamingOptimizer
|
||||
{
|
||||
sealed class RendererState
|
||||
{
|
||||
public WeakReference<NatureRenderer> Renderer = null!;
|
||||
public bool OnlyInitializeWithinRenderingDistance;
|
||||
}
|
||||
|
||||
static readonly Dictionary<int, RendererState> OriginalRenderers = new();
|
||||
static readonly FieldInfo? RenderingDistanceLimitField =
|
||||
AccessTools.Field(typeof(NatureRenderer), "_renderingDistanceLimit");
|
||||
static readonly FieldInfo? StreamingBudgetField =
|
||||
AccessTools.Field(typeof(TerrainGrassStreamer), "_globalStreamingBudget");
|
||||
static readonly FieldInfo? ForceNearbyField =
|
||||
AccessTools.Field(typeof(TerrainGrassStreamer), "_globalNearbyCellLoading");
|
||||
static readonly FieldInfo? TimersField =
|
||||
AccessTools.Field(typeof(TerrainGrassStreamer), "_inRangeOfAnyCamera");
|
||||
|
||||
static Harmony? _harmony;
|
||||
static int _originalStreamingBudget = 1024;
|
||||
static bool _originalForceNearby = true;
|
||||
|
||||
public static void Enable()
|
||||
{
|
||||
if (_harmony != null) return;
|
||||
CaptureGrassGlobals();
|
||||
_harmony = new Harmony("S3.basegame.nature");
|
||||
_harmony.CreateClassProcessor(
|
||||
typeof(GrassBudgetSetterPatch)).Patch();
|
||||
_harmony.CreateClassProcessor(
|
||||
typeof(GrassNearbySetterPatch)).Patch();
|
||||
_harmony.CreateClassProcessor(
|
||||
typeof(NatureRendererOnEnablePatch)).Patch();
|
||||
_harmony.CreateClassProcessor(
|
||||
typeof(NatureRendererRangePatch)).Patch();
|
||||
_harmony.CreateClassProcessor(
|
||||
typeof(TerrainGrassExpiryPatch)).Patch();
|
||||
ApplySettings();
|
||||
}
|
||||
|
||||
public static void Disable()
|
||||
{
|
||||
_harmony?.UnpatchAll("S3.basegame.nature");
|
||||
_harmony = null;
|
||||
RestoreRendererSettings();
|
||||
try
|
||||
{
|
||||
TerrainGrassStreamer.SetStreamingBudget(_originalStreamingBudget);
|
||||
TerrainGrassStreamer.SetForceLoadNearbyCells(_originalForceNearby);
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
public static void ApplySettings()
|
||||
{
|
||||
BaseGamePerfSettings settings = BaseGamePerfModule.Settings;
|
||||
if (!settings.natureStreamingSmoothingEnabled)
|
||||
{
|
||||
RestoreRendererSettings();
|
||||
TerrainGrassStreamer.SetStreamingBudget(_originalStreamingBudget);
|
||||
TerrainGrassStreamer.SetForceLoadNearbyCells(_originalForceNearby);
|
||||
return;
|
||||
}
|
||||
|
||||
TerrainGrassStreamer.SetStreamingBudget(
|
||||
Mathf.Clamp(settings.grassInstanceBudgetPerFrame, 64, 4096));
|
||||
TerrainGrassStreamer.SetForceLoadNearbyCells(
|
||||
!settings.queueNearbyGrassLoads);
|
||||
|
||||
NatureRenderer[] renderers =
|
||||
Resources.FindObjectsOfTypeAll<NatureRenderer>();
|
||||
for (int i = 0; i < renderers.Length; i++)
|
||||
TrackAndApply(renderers[i]);
|
||||
|
||||
Log.Info(
|
||||
$"[basegame] Nature streaming: grassBudget=" +
|
||||
$"{settings.grassInstanceBudgetPerFrame} instances/frame, " +
|
||||
$"queueNearby={settings.queueNearbyGrassLoads}, " +
|
||||
$"unloadSpread={settings.grassUnloadSpreadFrames} frames, " +
|
||||
$"distanceLifecycle={settings.distanceCullNatureTerrains}, " +
|
||||
$"terrains={OriginalRenderers.Count}.");
|
||||
}
|
||||
|
||||
static void CaptureGrassGlobals()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (StreamingBudgetField?.GetValue(null) is int budget)
|
||||
_originalStreamingBudget = budget;
|
||||
if (ForceNearbyField?.GetValue(null) is bool force)
|
||||
_originalForceNearby = force;
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
static void TrackAndApply(NatureRenderer? renderer)
|
||||
{
|
||||
if (renderer == null || !renderer.gameObject.scene.IsValid()) return;
|
||||
int id = renderer.GetInstanceID();
|
||||
if (!OriginalRenderers.ContainsKey(id))
|
||||
{
|
||||
OriginalRenderers[id] = new RendererState
|
||||
{
|
||||
Renderer = new WeakReference<NatureRenderer>(renderer),
|
||||
OnlyInitializeWithinRenderingDistance =
|
||||
renderer.OnlyInitializeWithinRenderingDistance,
|
||||
};
|
||||
}
|
||||
renderer.OnlyInitializeWithinRenderingDistance =
|
||||
BaseGamePerfModule.Settings.distanceCullNatureTerrains;
|
||||
}
|
||||
|
||||
static void RestoreRendererSettings()
|
||||
{
|
||||
foreach (RendererState state in OriginalRenderers.Values)
|
||||
{
|
||||
if (!state.Renderer.TryGetTarget(out NatureRenderer? renderer) ||
|
||||
renderer == null)
|
||||
continue;
|
||||
renderer.OnlyInitializeWithinRenderingDistance =
|
||||
state.OnlyInitializeWithinRenderingDistance;
|
||||
}
|
||||
OriginalRenderers.Clear();
|
||||
}
|
||||
|
||||
static bool CorrectRangeCheck(
|
||||
NatureRenderer renderer,
|
||||
double threshold)
|
||||
{
|
||||
Terrain? terrain = renderer.Terrain;
|
||||
TerrainData? data = renderer.TerrainData;
|
||||
if (terrain == null || data == null) return false;
|
||||
|
||||
Bounds bounds = data.bounds;
|
||||
bounds.center += terrain.GetPosition();
|
||||
float configuredLimit = 5000f;
|
||||
try
|
||||
{
|
||||
if (RenderingDistanceLimitField?.GetValue(renderer) is float value)
|
||||
configuredLimit = value;
|
||||
}
|
||||
catch { }
|
||||
|
||||
foreach (CameraRenderer cameraRenderer in RendererPool.GetCameras())
|
||||
{
|
||||
Camera? camera = cameraRenderer?.Camera;
|
||||
if (camera == null ||
|
||||
camera.cameraType == CameraType.Preview ||
|
||||
camera.cameraType == CameraType.SceneView)
|
||||
continue;
|
||||
|
||||
Vector3 position = camera.transform.position;
|
||||
Vector3 center = bounds.center;
|
||||
Vector3 extents = bounds.extents;
|
||||
double dx = Math.Max(
|
||||
0.0, Math.Abs(position.x - center.x) - extents.x);
|
||||
double dy = Math.Max(
|
||||
0.0, Math.Abs(position.y - center.y) - extents.y);
|
||||
double dz = Math.Max(
|
||||
0.0, Math.Abs(position.z - center.z) - extents.z);
|
||||
double distance = Math.Sqrt(dx * dx + dy * dy + dz * dz);
|
||||
double limit = Math.Min(configuredLimit, camera.farClipPlane);
|
||||
if (distance < limit + threshold) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
[HarmonyPatch(
|
||||
typeof(TerrainGrassStreamer),
|
||||
nameof(TerrainGrassStreamer.SetStreamingBudget))]
|
||||
static class GrassBudgetSetterPatch
|
||||
{
|
||||
static void Prefix(ref int __0)
|
||||
{
|
||||
BaseGamePerfSettings settings = BaseGamePerfModule.Settings;
|
||||
if (!settings.natureStreamingSmoothingEnabled) return;
|
||||
__0 = Mathf.Min(
|
||||
__0, Mathf.Clamp(settings.grassInstanceBudgetPerFrame, 64, 4096));
|
||||
}
|
||||
}
|
||||
|
||||
[HarmonyPatch(
|
||||
typeof(TerrainGrassStreamer),
|
||||
nameof(TerrainGrassStreamer.SetForceLoadNearbyCells))]
|
||||
static class GrassNearbySetterPatch
|
||||
{
|
||||
static void Prefix(ref bool __0)
|
||||
{
|
||||
BaseGamePerfSettings settings = BaseGamePerfModule.Settings;
|
||||
if (settings.natureStreamingSmoothingEnabled &&
|
||||
settings.queueNearbyGrassLoads)
|
||||
__0 = false;
|
||||
}
|
||||
}
|
||||
|
||||
[HarmonyPatch(typeof(NatureRenderer), "OnEnable")]
|
||||
static class NatureRendererOnEnablePatch
|
||||
{
|
||||
static void Postfix(NatureRenderer __instance)
|
||||
{
|
||||
if (BaseGamePerfModule.Settings.natureStreamingSmoothingEnabled)
|
||||
TrackAndApply(__instance);
|
||||
}
|
||||
}
|
||||
|
||||
[HarmonyPatch(typeof(NatureRenderer), "IsInRangeOfAnyCamera")]
|
||||
static class NatureRendererRangePatch
|
||||
{
|
||||
static bool Prefix(
|
||||
NatureRenderer __instance,
|
||||
double __0,
|
||||
ref bool __result)
|
||||
{
|
||||
BaseGamePerfSettings settings = BaseGamePerfModule.Settings;
|
||||
if (!settings.natureStreamingSmoothingEnabled ||
|
||||
!settings.distanceCullNatureTerrains)
|
||||
return true;
|
||||
__result = CorrectRangeCheck(__instance, __0);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
[HarmonyPatch(typeof(TerrainGrassStreamer), "OnCellOutOfRange")]
|
||||
static class TerrainGrassExpiryPatch
|
||||
{
|
||||
static void Postfix(TerrainGrassStreamer __instance, int __1)
|
||||
{
|
||||
BaseGamePerfSettings settings = BaseGamePerfModule.Settings;
|
||||
int spread = Mathf.Clamp(settings.grassUnloadSpreadFrames, 0, 600);
|
||||
if (!settings.natureStreamingSmoothingEnabled ||
|
||||
spread <= 0 ||
|
||||
TimersField?.GetValue(__instance) is not int[] timers ||
|
||||
__1 < 0 ||
|
||||
__1 >= timers.Length)
|
||||
return;
|
||||
|
||||
int hash = unchecked(
|
||||
__instance.GetHashCode() * 397 ^ __1 * 7919) & int.MaxValue;
|
||||
timers[__1] += hash % (spread + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -29,6 +29,18 @@
|
|||
<HintPath>$(GameManaged)\Map.Runtime.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
</Reference>
|
||||
<Reference Include="Definition">
|
||||
<HintPath>$(GameManaged)\Definition.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
</Reference>
|
||||
<Reference Include="VisualDesignCafe.Rendering.Nature">
|
||||
<HintPath>$(GameManaged)\VisualDesignCafe.Rendering.Nature.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
</Reference>
|
||||
<Reference Include="VisualDesignCafe.Rendering.Instancing">
|
||||
<HintPath>$(GameManaged)\VisualDesignCafe.Rendering.Instancing.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Unity -->
|
||||
|
|
@ -49,6 +61,10 @@
|
|||
<HintPath>$(GameManaged)\UnityEngine.PhysicsModule.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.TerrainModule">
|
||||
<HintPath>$(GameManaged)\UnityEngine.TerrainModule.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.JSONSerializeModule">
|
||||
<HintPath>$(GameManaged)\UnityEngine.JSONSerializeModule.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
|
|
@ -65,6 +81,10 @@
|
|||
<HintPath>$(GameManaged)\UnityEngine.InputLegacyModule.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
</Reference>
|
||||
<Reference Include="Unity.InputSystem">
|
||||
<HintPath>$(GameManaged)\Unity.InputSystem.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
</Reference>
|
||||
<Reference Include="Unity.TextMeshPro">
|
||||
<HintPath>$(GameManaged)\Unity.TextMeshPro.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
|
|
@ -73,6 +93,14 @@
|
|||
<HintPath>$(GameManaged)\Unity.RenderPipelines.Universal.Runtime.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.ImageConversionModule">
|
||||
<HintPath>$(GameManaged)\UnityEngine.ImageConversionModule.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.TextRenderingModule">
|
||||
<HintPath>$(GameManaged)\UnityEngine.TextRenderingModule.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
|
||||
<!-- UMM + Harmony -->
|
||||
|
|
@ -85,6 +113,10 @@
|
|||
<HintPath>$(UmmDir)\0Harmony.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
</Reference>
|
||||
<Reference Include="Newtonsoft.Json">
|
||||
<HintPath>$(GameManaged)\Newtonsoft.Json.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
|
|||
Loading…
Reference in a new issue