Skip to content

Commit d9f450a

Browse files
committed
Merged the features of Terrain Extender into Terrain Patcher.
2 parents f9b14a6 + a68f9a6 commit d9f450a

File tree

5 files changed

+261
-13
lines changed

5 files changed

+261
-13
lines changed

README.md

+15-9
Original file line numberDiff line numberDiff line change
@@ -34,18 +34,14 @@ issues for bugs encountered on an old version of Terrain Patcher.
3434

3535
### Terrain Extender
3636

37-
As of version 1.1.0, Terrain Patcher is no longer compatible with Terrain Extender (v1.0.0). This is
38-
because Terrain Extender relies on a private field that needed to be changed in the v1.1.0 release.
39-
For compatibility with Terrain Extender, use the previous release of Terrain Patcher, v1.0.2. It can
40-
be downloaded from the [releases
37+
Terrain Patcher is not compatible with Terrain Extender. The features of Terrain Extender have been
38+
merged into Terrain Patcher and will be packaged with the next release. For compatibility with
39+
Terrain Extender, use the v1.0.2 release of Terrain Patcher. It can be downloaded from the [releases
4140
page](https://github.com/Esper89/Subnautica-TerrainPatcher/releases/tag/v1.0.2), below the
4241
changelog. Installation and usage instructions for v1.0.2 can be found [in an older version of the
4342
repository](https://github.com/Esper89/Subnautica-TerrainPatcher/tree/v1.0.2). Please do not submit
4443
issues for bugs encountered on an old version of Terrain Patcher.
4544

46-
This incompatibility will likely be fixed when Terrain Extender's features are integrated into
47-
Terrain Patcher.
48-
4945
## Library Usage
5046

5147
The following is for modders who want to use Terrain Patcher in your mod. Keep in mind that if your
@@ -146,10 +142,12 @@ aren't removed when Terrain Patcher is disabled or uninstalled.
146142

147143
- Enabling and disabling patch loading in-game.
148144

149-
### Planned Features
150-
151145
- Extending the current edge of the world to allow for more terrain.
152146

147+
- Extending the edge of the world where entities can spawn and save.
148+
149+
### Planned Features
150+
153151
- Patching biomes and entities.
154152

155153
## Contributing
@@ -170,6 +168,14 @@ directories for easier testing.
170168
To build Terrain Patcher in release mode, run `dotnet build --configuration Release`. This will
171169
also create a `dist.zip` file in `target` for easy distribution.
172170

171+
### Contributors
172+
173+
- Esper Thomson ([@Esper89](https://github.com/Esper89))
174+
175+
- Metious ([@Metious](https://github.com/Metious))
176+
177+
- Jonah Butler ([@jonahnm](https://github.com/jonahnm))
178+
173179
## License
174180

175181
Copyright © 2021, 2023–2024 Esper Thomson

src/Mod.cs

+7-1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using BepInEx;
66
using BepInEx.Bootstrap;
77
using BepInEx.Configuration;
8+
using HarmonyLib;
89

910
namespace TerrainPatcher
1011
{
@@ -18,7 +19,11 @@ private void Awake()
1819
this._settings = new Settings(base.Config);
1920
Mod.Instance = this;
2021

21-
Patches.Register();
22+
var harmony = new Harmony("Esper89.TerrainPatcher");
23+
BatchPatches.Patch(harmony);
24+
Array3Patches.Patch(harmony);
25+
WorldStreamerPatches.Patch(harmony);
26+
2227
FileLoading.FindAndLoadPatches();
2328

2429
if (Chainloader.PluginInfos.ContainsKey("com.snmodding.nautilus"))
@@ -52,6 +57,7 @@ private static Mod Instance
5257
}
5358

5459
// Writes a message to the BepInEx log with the specified log level.
60+
internal static void LogDebug(string message) => Mod.Instance.Logger.LogDebug(message);
5561
internal static void LogInfo(string message) => Mod.Instance.Logger.LogInfo(message);
5662
internal static void LogWarning(string message) => Mod.Instance.Logger.LogWarning(message);
5763
internal static void LogError(string message) => Mod.Instance.Logger.LogError(message);

src/Patches/Array3.cs

+100
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
using System.Collections.Generic;
2+
using HarmonyLib;
3+
4+
namespace TerrainPatcher
5+
{
6+
// Allows entities to save and exist on negative batches.
7+
[HarmonyPatch(typeof(Array3<>))]
8+
internal static class Array3Patches
9+
{
10+
internal static void Patch(Harmony harmony)
11+
{
12+
var type = typeof(Array3<>).MakeGenericType(typeof(EntityCell));
13+
var getMethod = AccessTools.Method(type, "Get");
14+
harmony.Patch(getMethod, prefix: new HarmonyMethod(
15+
AccessTools.Method(typeof(Array3Patches), nameof(GetPrefix))
16+
));
17+
var setMethod = AccessTools.Method(type, "Set");
18+
harmony.Patch(setMethod, prefix: new HarmonyMethod(
19+
AccessTools.Method(typeof(Array3Patches), nameof(SetPrefix))
20+
));
21+
harmony.PatchAll(typeof(IngameMenu_QuitGameAsync_Patch));
22+
}
23+
24+
private class NegativeEntityCell
25+
{
26+
public Dictionary<Int3, EntityCell> EntityCells { get; }
27+
28+
public NegativeEntityCell(Dictionary<Int3, EntityCell> entityCells)
29+
{
30+
EntityCells = entityCells;
31+
}
32+
}
33+
34+
private static Dictionary<Array3<EntityCell>, NegativeEntityCell> vanillaToNegativeArrays =
35+
new Dictionary<Array3<EntityCell>, NegativeEntityCell>();
36+
37+
private static bool GetPrefix(
38+
Array3<EntityCell> __instance,
39+
int x, int y, int z,
40+
ref EntityCell __result
41+
)
42+
{
43+
var index = __instance.GetIndex(x, y, z);
44+
if (index >= 0)
45+
{
46+
return true;
47+
}
48+
49+
// At this point the index is negative, so we'll handle it ourselves.
50+
if (vanillaToNegativeArrays.TryGetValue(__instance, out var negativesArray) &&
51+
negativesArray.EntityCells.TryGetValue(new Int3(x, y, z), out var entityCell))
52+
{
53+
__result = entityCell;
54+
Mod.LogDebug($"Get negative entity cell for ({x}, {y}, {z})");
55+
return false;
56+
}
57+
58+
Mod.LogDebug($"Couldn't find negative entity cell for ({x}, {y}, {z})");
59+
return false;
60+
}
61+
62+
private static bool SetPrefix(
63+
Array3<EntityCell> __instance,
64+
int x, int y, int z,
65+
EntityCell value
66+
)
67+
{
68+
var index = __instance.GetIndex(x, y, z);
69+
if (index >= 0)
70+
{
71+
return true;
72+
}
73+
74+
// At this point the index is negative, so we'll set it to our collection.
75+
if (!vanillaToNegativeArrays.TryGetValue(__instance, out var negativeEntityCell))
76+
{
77+
negativeEntityCell = new NegativeEntityCell(
78+
new Dictionary<Int3, EntityCell>(Int3.equalityComparer)
79+
);
80+
vanillaToNegativeArrays[__instance] = negativeEntityCell;
81+
}
82+
83+
negativeEntityCell.EntityCells[new Int3(x, y, z)] = value;
84+
Mod.LogDebug($"Set negative entity cell for ({x}, {y}, {z})");
85+
86+
return false;
87+
}
88+
89+
// Clear `vanillaToNegativeArrays` when the user quits the game.
90+
[HarmonyPatch(typeof(IngameMenu), nameof(IngameMenu.QuitGameAsync))]
91+
internal static class IngameMenu_QuitGameAsync_Patch
92+
{
93+
private static void Postfix()
94+
{
95+
Mod.LogDebug("Clearing negative entity cells on quit");
96+
vanillaToNegativeArrays.Clear();
97+
}
98+
}
99+
}
100+
}

src/Patches.cs src/Patches/Batch.cs

+20-3
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,13 @@
66
namespace TerrainPatcher
77
{
88
// Harmony patches that make the game open patched terrain files instead of the originals.
9-
internal static class Patches
9+
internal static class BatchPatches
1010
{
11-
internal static void Register()
11+
internal static void Patch(Harmony harmony)
1212
{
13-
var harmony = new Harmony("Esper89.TerrainPatcher");
1413
harmony.PatchAll(typeof(LargeWorldStreamer_GetCompiledOctreesCachePath_Patch));
1514
harmony.PatchAll(typeof(BatchOctreesStreamer_GetPath_Patch));
15+
harmony.PatchAll(typeof(LargeWorldStreamer_CheckBatch_Patches));
1616
}
1717

1818
[HarmonyPatch(
@@ -73,5 +73,22 @@ private static bool SetResult(Int3 batchId, ref string result, bool runOriginal)
7373
return true;
7474
}
7575
}
76+
77+
// Permits any batch location.
78+
[HarmonyPatch(typeof(LargeWorldStreamer))]
79+
internal static class LargeWorldStreamer_CheckBatch_Patches
80+
{
81+
[HarmonyPatch(nameof(LargeWorldStreamer.CheckBatch))]
82+
[HarmonyPatch(
83+
nameof(LargeWorldStreamer.CheckRoot),
84+
typeof(int), typeof(int), typeof(int)
85+
)]
86+
[HarmonyPrefix]
87+
private static bool AllowOutOfBounds(ref bool __result)
88+
{
89+
__result = true;
90+
return false;
91+
}
92+
}
7693
}
7794
}

src/Patches/WorldStreamer.cs

+119
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
using System.Collections.Generic;
2+
using System.Reflection.Emit;
3+
using HarmonyLib;
4+
using UnityEngine;
5+
using WorldStreaming;
6+
7+
namespace TerrainPatcher
8+
{
9+
[HarmonyPatch(typeof(WorldStreamer))]
10+
internal static class WorldStreamerPatches
11+
{
12+
internal static void Patch(Harmony harmony)
13+
{
14+
harmony.PatchAll(typeof(WorldStreamerPatches));
15+
}
16+
17+
[HarmonyPatch(nameof(WorldStreamer.CreateStreamers))]
18+
[HarmonyPrefix]
19+
private static void CreateStreamersPrefix(WorldStreamer __instance)
20+
{
21+
// Allows the streamer to stream octrees as far as the patched batches go.
22+
23+
var biggestBatch =
24+
BiggestBatch(TerrainRegistry.patchedBatches.Keys, __instance.settings.numOctrees) +
25+
__instance.settings.numOctreesPerBatch *
26+
(__instance.settings.numOctreesPerBatch * 2);
27+
28+
__instance.settings.numOctrees = biggestBatch;
29+
}
30+
31+
[HarmonyPatch(nameof(WorldStreamer.CreateStreamers))]
32+
[HarmonyTranspiler]
33+
private static IEnumerable<CodeInstruction> CreateStreamersTranspiler(
34+
IEnumerable<CodeInstruction> instructions
35+
)
36+
{
37+
// Changes minimum and maximum center positions. Also necessary to stream batches.
38+
39+
var int3Zero = AccessTools.Field(typeof(Int3), nameof(Int3.zero));
40+
var found = false;
41+
var found2 = false;
42+
foreach (var instruction in instructions)
43+
{
44+
if (instruction.Is(OpCodes.Ldsfld, int3Zero))
45+
{
46+
yield return CodeInstruction.Call(
47+
typeof(WorldStreamerPatches), nameof(MinimumBoundary)
48+
);
49+
found = true;
50+
}
51+
else if (instruction.Calls(AccessTools.Method(typeof(WorldStreamer),
52+
nameof(WorldStreamer.ParseStreamingSettings))))
53+
{
54+
yield return instruction;
55+
yield return CodeInstruction.Call(
56+
typeof(WorldStreamerPatches), nameof(ParseStreamingSettings)
57+
);
58+
found2 = true;
59+
}
60+
else
61+
{
62+
yield return instruction;
63+
}
64+
}
65+
66+
if (found && found2)
67+
{
68+
Mod.LogDebug($"{nameof(CreateStreamersTranspiler)} has been patched successfully.");
69+
}
70+
else
71+
{
72+
Mod.LogError($"{nameof(CreateStreamersTranspiler)} failed patching.");
73+
}
74+
}
75+
76+
private static LargeWorldStreamer.Settings ParseStreamingSettings(
77+
LargeWorldStreamer.Settings settings
78+
)
79+
{
80+
var patchedBatches = TerrainRegistry.patchedBatches.Keys;
81+
settings.octreesSettings.centerMin = SmallestBatch(patchedBatches) * 5 - 10;
82+
settings.octreesSettings.centerMax =
83+
BiggestBatch(patchedBatches, settings.octreesSettings.centerMax) * 5 + 15;
84+
return settings;
85+
}
86+
87+
private static Int3 MinimumBoundary()
88+
{
89+
return SmallestBatch(TerrainRegistry.patchedBatches.Keys) * 5 - 10;
90+
}
91+
92+
private static Int3 SmallestBatch(IEnumerable<Int3> batches)
93+
{
94+
var result = Int3.zero;
95+
96+
foreach (var batch in batches)
97+
{
98+
result = Int3.Min(result, batch);
99+
}
100+
101+
var horizontalMin = Mathf.Min(result.x, result.z);
102+
return new Int3(horizontalMin, result.y, horizontalMin);
103+
}
104+
105+
private static Int3 BiggestBatch(IEnumerable<Int3> batches, Int3 minimumBatch)
106+
{
107+
var result = minimumBatch;
108+
109+
foreach (var batch in batches)
110+
{
111+
var batchSize = batch;
112+
result = Int3.Max(result, batchSize);
113+
}
114+
115+
var horizontalMax = Mathf.Max(result.x, result.z);
116+
return new Int3(horizontalMax, result.y, horizontalMax);
117+
}
118+
}
119+
}

0 commit comments

Comments
 (0)