From 805d26ddea06f91d3962bf48c09ba0c68c8d961a Mon Sep 17 00:00:00 2001
From: "@Someone" <45270312+Someone-193@users.noreply.github.com>
Date: Wed, 7 Jan 2026 23:20:15 -0500
Subject: [PATCH 1/4] Add event boilerplate + all of transpiler (all that's
missing is layout checker)
---
.../EventArgs/Map/GeneratingEventArgs.cs | 69 ++++
EXILED/Exiled.Events/Handlers/Map.cs | 11 +
.../Patches/Events/Map/Generating.cs | 352 ++++++++++++++++++
3 files changed, 432 insertions(+)
create mode 100644 EXILED/Exiled.Events/EventArgs/Map/GeneratingEventArgs.cs
create mode 100644 EXILED/Exiled.Events/Patches/Events/Map/Generating.cs
diff --git a/EXILED/Exiled.Events/EventArgs/Map/GeneratingEventArgs.cs b/EXILED/Exiled.Events/EventArgs/Map/GeneratingEventArgs.cs
new file mode 100644
index 0000000000..f14625995d
--- /dev/null
+++ b/EXILED/Exiled.Events/EventArgs/Map/GeneratingEventArgs.cs
@@ -0,0 +1,69 @@
+// -----------------------------------------------------------------------
+//
+// Copyright (c) ExMod Team. All rights reserved.
+// Licensed under the CC BY-SA 3.0 license.
+//
+// -----------------------------------------------------------------------
+
+namespace Exiled.Events.EventArgs.Map
+{
+ using Exiled.API.Enums;
+ using Exiled.Events.EventArgs.Interfaces;
+ using UnityEngine;
+
+ ///
+ /// Contains all information after the server generates a seed, but before the map is generated.
+ ///
+ public class GeneratingEventArgs : IDeniableEvent
+ {
+ private int seed;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ ///
+ ///
+ ///
+ ///
+ public GeneratingEventArgs(int seed, LczFacilityLayout lcz, HczFacilityLayout hcz, EzFacilityLayout ez)
+ {
+ LczLayout = lcz;
+ HczLayout = hcz;
+ EzLayout = ez;
+
+ Seed = seed;
+ IsAllowed = true;
+ }
+
+ ///
+ /// Gets or sets the layout of the light containment zone.
+ ///
+ public LczFacilityLayout LczLayout { get; set; }
+
+ ///
+ /// Gets or sets the layout of the heavy containment zone.
+ ///
+ public HczFacilityLayout HczLayout { get; set; }
+
+ ///
+ /// Gets or sets the layout of the entrance zone.
+ ///
+ public EzFacilityLayout EzLayout { get; set; }
+
+ ///
+ /// Gets or sets the seed of the map.
+ ///
+ /// This property overrides any changes in , , or .
+ public int Seed
+ {
+ get => seed;
+ set => seed = Mathf.Clamp(value, 0, int.MaxValue);
+ }
+
+ ///
+ /// Gets or sets a value indicating whether the map can be generated.
+ ///
+ /// This property overrides any changes in .
+ public bool IsAllowed { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/EXILED/Exiled.Events/Handlers/Map.cs b/EXILED/Exiled.Events/Handlers/Map.cs
index 4db303b36d..d8890bfa5a 100644
--- a/EXILED/Exiled.Events/Handlers/Map.cs
+++ b/EXILED/Exiled.Events/Handlers/Map.cs
@@ -125,6 +125,11 @@ public static class Map
///
public static Event PlacingPickupIntoPocketDimension { get; set; } = new();
+ ///
+ /// Invoked after a map seed has been chosen, but before it is used.
+ ///
+ public static Event Generating { get; set; } = new();
+
///
/// Called before placing a decal.
///
@@ -249,5 +254,11 @@ public static class Map
///
/// The instnace.
public static void OnPlacingPickupIntoPocketDimension(PlacingPickupIntoPocketDimensionEventArgs ev) => PlacingPickupIntoPocketDimension.InvokeSafely(ev);
+
+ ///
+ /// Called after a map seed has been chosen, but before it is used.
+ ///
+ /// The instnace.
+ public static void OnGenerating(GeneratingEventArgs ev) => Generating.InvokeSafely(ev);
}
}
\ No newline at end of file
diff --git a/EXILED/Exiled.Events/Patches/Events/Map/Generating.cs b/EXILED/Exiled.Events/Patches/Events/Map/Generating.cs
new file mode 100644
index 0000000000..cc64650f4f
--- /dev/null
+++ b/EXILED/Exiled.Events/Patches/Events/Map/Generating.cs
@@ -0,0 +1,352 @@
+// -----------------------------------------------------------------------
+//
+// Copyright (c) ExMod Team. All rights reserved.
+// Licensed under the CC BY-SA 3.0 license.
+//
+// -----------------------------------------------------------------------
+
+namespace Exiled.Events.Patches.Events.Map
+{
+ using System;
+ using System.Collections.Generic;
+ using System.Linq;
+ using System.Reflection.Emit;
+
+ using Exiled.API.Enums;
+ using Exiled.API.Features;
+ using Exiled.API.Features.Pools;
+ using Exiled.Events.Attributes;
+ using Exiled.Events.EventArgs.Map;
+ using HarmonyLib;
+ using LabApi.Events.Arguments.ServerEvents;
+ using MapGeneration;
+ using UnityEngine;
+
+ using static HarmonyLib.AccessTools;
+
+ ///
+ /// Patches .
+ /// Adds the event.
+ ///
+ [EventPatch(typeof(Handlers.Map), nameof(Handlers.Map.Generating))]
+ [HarmonyPatch(typeof(SeedSynchronizer), nameof(SeedSynchronizer.Awake))]
+ public class Generating
+ {
+ private static IEnumerable Transpiler(IEnumerable instructions, ILGenerator generator)
+ {
+ List newInstructions = ListPool.Pool.Get(instructions);
+
+ Label skipLabel = generator.DefineLabel();
+
+ // Label to LabAPI's ev.IsAllowed = false branch
+ Label notAllowedLabel = newInstructions.FindLast(x => x.opcode == OpCodes.Ldstr).labels.First();
+
+ Label continueEventLabel = generator.DefineLabel();
+
+ LocalBuilder lcz = generator.DeclareLocal(typeof(LczFacilityLayout));
+ LocalBuilder hcz = generator.DeclareLocal(typeof(HczFacilityLayout));
+ LocalBuilder ez = generator.DeclareLocal(typeof(EzFacilityLayout));
+
+ LocalBuilder ev = generator.DeclareLocal(typeof(GeneratingEventArgs));
+
+ Label lczLabel1 = generator.DefineLabel();
+ Label lczLabel2 = generator.DefineLabel();
+
+ Label hczLabel1 = generator.DefineLabel();
+ Label hczLabel2 = generator.DefineLabel();
+
+ Label ezLabel1 = generator.DefineLabel();
+ Label ezLabel2 = generator.DefineLabel();
+
+ LocalBuilder newSeed = generator.DeclareLocal(typeof(int));
+
+ int index = newInstructions.FindLastIndex(x => x.opcode == OpCodes.Ldloc_1);
+
+ newInstructions[index].WithLabels(skipLabel);
+
+ /*
+ * To summarize this transpiler:
+ *
+ * if (!TryDetermineLayouts(ev2.Seed, out LczFacilityLayout lcz, out HczFacilityLayout hcz, out EzFacilityLayout ez)
+ * goto skipEvent;
+ *
+ * ev = new GeneratingEventArgs(ev2.Seed, lcz, hcz, ez);
+ * Handlers.Map.OnGenerating(ev);
+ *
+ * if (!ev.IsAllowed)
+ * goto "Map generation cancelled by a plugin." debug statement;
+ *
+ * if (ev2.Seed != ev.Seed)
+ * {
+ * ev2.Seed = ev.Seed;
+ * goto skipEvent;
+ * }
+ *
+ * int newSeed = GenerateSeed(lcz == ev.LczLayout ? LczFacilityLayout.Unknown : ev.LczLayout, hcz == ev.HczLayout ? HczFacilityLayout.Unknown : ev.HczLayout, ez == ev.EzLayout ? EzFacilityLayout.Unknown : ev.EzLayout);
+ * if (newSeed == -1)
+ * goto skipEvent;
+ * ev2.Seed = newSeed;
+ */
+
+ newInstructions.InsertRange(index, new[]
+ {
+ // ev.Seed (from LabAPI event)
+ new CodeInstruction(OpCodes.Ldloc_1).MoveLabelsFrom(newInstructions[index]),
+ new(OpCodes.Callvirt, PropertyGetter(typeof(MapGeneratingEventArgs), nameof(MapGeneratingEventArgs.Seed))),
+
+ // Dup on stack twice
+ new(OpCodes.Dup),
+
+ // TryDetermineLayouts(ev.Seed, out lcz, out hcz, out ez)
+ new(OpCodes.Ldloca_S, lcz),
+ new(OpCodes.Ldloca_S, hcz),
+ new(OpCodes.Ldloca_S, ez),
+ new(OpCodes.Call, Method(typeof(Generating), nameof(TryDetermineLayouts))),
+
+ // if (false) skip our code;
+ new(OpCodes.Brfalse_S, skipLabel),
+
+ // new GeneratingEventArgs(ev.Seed, lcz, hcz, ez);
+ new(OpCodes.Ldloc_S, lcz),
+ new(OpCodes.Ldloc_S, hcz),
+ new(OpCodes.Ldloc_S, ez),
+ new(OpCodes.Newobj, GetDeclaredConstructors(typeof(GeneratingEventArgs))[0]),
+
+ // Dup on stack
+ new(OpCodes.Dup),
+
+ // Call OnGenerating
+ new(OpCodes.Call, Method(typeof(Handlers.Map), nameof(Handlers.Map.OnGenerating))),
+
+ // save ev
+ new(OpCodes.Stloc_S, ev),
+
+ // if (!ev.IsAllowed) goto "Map generation cancelled by a plugin." debug statement;
+ new(OpCodes.Callvirt, PropertyGetter(typeof(GeneratingEventArgs), nameof(GeneratingEventArgs.IsAllowed))),
+ new(OpCodes.Brfalse_S, notAllowedLabel),
+
+ // if (ev.Seed (LabAPI) != ev.Seed (ours))
+ new(OpCodes.Ldloc_1),
+ new(OpCodes.Callvirt, PropertyGetter(typeof(MapGeneratingEventArgs), nameof(MapGeneratingEventArgs.Seed))),
+ new(OpCodes.Ldloc_S, ev),
+ new(OpCodes.Callvirt, PropertyGetter(typeof(GeneratingEventArgs), nameof(GeneratingEventArgs.Seed))),
+ new(OpCodes.Beq_S, continueEventLabel),
+
+ // {
+ // ev.Seed (LabAPI) = ev.Seed (ours)
+ new(OpCodes.Ldloc_S, ev),
+ new(OpCodes.Callvirt, PropertyGetter(typeof(GeneratingEventArgs), nameof(GeneratingEventArgs.Seed))),
+ new(OpCodes.Ldloc_1),
+ new(OpCodes.Callvirt, PropertySetter(typeof(MapGeneratingEventArgs), nameof(MapGeneratingEventArgs.Seed))),
+
+ // skip other event code;
+ new(OpCodes.Br_S, skipLabel),
+
+ // }
+ // load (lcz == ev.LczLayout ? LczFacilityLayout.Unknown : ev.LczLayout) onto the stack
+ new CodeInstruction(OpCodes.Ldloc_S, lcz).WithLabels(continueEventLabel),
+ new(OpCodes.Ldloc_S, ev),
+ new(OpCodes.Callvirt, PropertyGetter(typeof(GeneratingEventArgs), nameof(GeneratingEventArgs.LczLayout))),
+ new(OpCodes.Beq_S, lczLabel1),
+
+ // ev.LczLayout
+ new(OpCodes.Ldloc_S, ev),
+ new(OpCodes.Callvirt, PropertyGetter(typeof(GeneratingEventArgs), nameof(GeneratingEventArgs.LczLayout))),
+ new(OpCodes.Ldind_I4),
+ new(OpCodes.Br_S, lczLabel2),
+
+ // LczFacilityLayout.Unknown
+ new CodeInstruction(OpCodes.Ldc_I4_0).WithLabels(lczLabel1),
+
+ // load (hcz == ev.HczLayout ? HczFacilityLayout.Unknown : ev.HczLayout) onto the stack
+ new CodeInstruction(OpCodes.Ldloc_S, hcz).WithLabels(lczLabel2),
+ new(OpCodes.Ldloc_S, ev),
+ new(OpCodes.Callvirt, PropertyGetter(typeof(GeneratingEventArgs), nameof(GeneratingEventArgs.HczLayout))),
+ new(OpCodes.Beq_S, hczLabel1),
+
+ // ev.HczLayout
+ new(OpCodes.Ldloc_S, ev),
+ new(OpCodes.Callvirt, PropertyGetter(typeof(GeneratingEventArgs), nameof(GeneratingEventArgs.HczLayout))),
+ new(OpCodes.Ldind_I4),
+ new(OpCodes.Br_S, hczLabel2),
+
+ // HczFacilityLayout.Unknown
+ new CodeInstruction(OpCodes.Ldc_I4_0).WithLabels(hczLabel1),
+
+ // load (ez == ev.EzLayout ? EzFacilityLayout.Unknown : ev.EzLayout) onto the stack
+ new CodeInstruction(OpCodes.Ldloc_S, ez).WithLabels(hczLabel2),
+ new(OpCodes.Ldloc_S, ev),
+ new(OpCodes.Callvirt, PropertyGetter(typeof(GeneratingEventArgs), nameof(GeneratingEventArgs.EzLayout))),
+ new(OpCodes.Beq_S, ezLabel1),
+
+ // ev.EzLayout
+ new(OpCodes.Ldloc_S, ev),
+ new(OpCodes.Callvirt, PropertyGetter(typeof(GeneratingEventArgs), nameof(GeneratingEventArgs.EzLayout))),
+ new(OpCodes.Ldind_I4),
+ new(OpCodes.Br_S, ezLabel2),
+
+ // EzFacilityLayout.Unknown
+ new CodeInstruction(OpCodes.Ldc_I4_0).WithLabels(ezLabel1),
+
+ // int newSeed = GenerateSeed(those 3 ternary values);
+ new CodeInstruction(OpCodes.Call, Method(typeof(Generating), nameof(GenerateSeed))).WithLabels(ezLabel2),
+ new(OpCodes.Dup),
+ new(OpCodes.Stloc_S, newSeed),
+
+ // if (newSeed == -1) skip other event code;
+ new(OpCodes.Ldc_I4_M1),
+ new(OpCodes.Beq_S, skipLabel),
+
+ // ev.Seed (LabAPI) = newSeed;
+ new(OpCodes.Ldloc_S, newSeed),
+ new(OpCodes.Ldloc_1),
+ new(OpCodes.Callvirt, PropertySetter(typeof(MapGeneratingEventArgs), nameof(MapGeneratingEventArgs.Seed))),
+ });
+
+ for (int z = 0; z < newInstructions.Count; z++)
+ yield return newInstructions[z];
+
+ ListPool.Pool.Return(newInstructions);
+ }
+
+ // generates a seed for target layouts
+ private static int GenerateSeed(LczFacilityLayout lcz, HczFacilityLayout hcz, EzFacilityLayout ez)
+ {
+ if (lcz is LczFacilityLayout.Unknown && hcz is HczFacilityLayout.Unknown && ez is EzFacilityLayout.Unknown)
+ return -1;
+
+ System.Random seedGenerator = new();
+
+ int best = -1;
+ int bestMatches = 0;
+
+ for (int i = 0; i < 1000; i++)
+ {
+ int matches = 0;
+
+ int seed = seedGenerator.Next(1, int.MaxValue);
+
+ if (!TryDetermineLayouts(seed, out LczFacilityLayout currLcz, out HczFacilityLayout currHcz, out EzFacilityLayout currEz))
+ {
+ break;
+ }
+
+ if (lcz is LczFacilityLayout.Unknown || currLcz == lcz)
+ matches++;
+ if (hcz is HczFacilityLayout.Unknown || currHcz == hcz)
+ matches++;
+ if (ez is EzFacilityLayout.Unknown || currEz == ez)
+ matches++;
+
+ if (matches > bestMatches)
+ {
+ best = seed;
+ bestMatches = matches;
+ }
+
+ if (bestMatches is 3)
+ break;
+ }
+
+ return best;
+ }
+
+ // determines what layouts will be generated from a seed
+ private static bool TryDetermineLayouts(int seed, out LczFacilityLayout lcz, out HczFacilityLayout hcz, out EzFacilityLayout ez)
+ {
+ lcz = LczFacilityLayout.Unknown;
+ hcz = HczFacilityLayout.Unknown;
+ ez = EzFacilityLayout.Unknown;
+
+ System.Random rng = new(seed);
+ try
+ {
+ ZoneGenerator[] gens = SeedSynchronizer._singleton._zoneGenerators;
+ for (int i = 0; i < gens.Length; i++)
+ {
+ ZoneGenerator generator = gens[i];
+
+ switch (generator)
+ {
+ // EntranceZoneGenerator should be the last zone generator
+ case EntranceZoneGenerator ezGen:
+ if (i != gens.Length - 1)
+ {
+ Log.Error($"{typeof(Generating).FullName}.{nameof(TryDetermineLayouts)}: Last zone generator processed for seed {seed} was not an EntranceZoneGenerator!");
+ return false;
+ }
+
+ ez = (EzFacilityLayout)(rng.Next(ezGen.Atlases.Length) + 1);
+ break;
+ case AtlasZoneGenerator gen:
+ int layout = rng.Next(gen.Atlases.Length);
+
+ if (gen is LightContainmentZoneGenerator)
+ lcz = (LczFacilityLayout)(layout + 1);
+ else
+ hcz = (HczFacilityLayout)(layout + 1);
+
+ // rng needs to be called the same amount as during map gen for next zone generator.
+ // this block of code picks what rooms to generate.
+ Texture2D tex = gen.Atlases[layout];
+ AtlasInterpretation[] interpretations = MapAtlasInterpreter.Singleton.Interpret(tex, rng);
+ RandomizeInterpreted(rng, interpretations);
+
+ // this block "generates" them and accounts for duplicates and other things.
+ break;
+ default:
+ Log.Error($"{typeof(Generating).FullName}.{nameof(TryDetermineLayouts)}: Found non AtlasZoneGenerator [{generator}] in SeedSynchronizer._singleton._zoneGenerators!");
+ return false;
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ Log.Error(ex);
+ return false;
+ }
+
+ bool error = false;
+ if (lcz is LczFacilityLayout.Unknown)
+ {
+ Log.Error($"{typeof(Generating).FullName}.{nameof(TryDetermineLayouts)}: Failed to find layout for LCZ for seed {seed}");
+ error = true;
+ }
+
+ if (hcz is HczFacilityLayout.Unknown)
+ {
+ Log.Error($"{typeof(Generating).FullName}.{nameof(TryDetermineLayouts)}: Failed to find layout for HCZ for seed {seed}");
+ error = true;
+ }
+
+ if (ez is EzFacilityLayout.Unknown)
+ {
+ Log.Error($"{typeof(Generating).FullName}.{nameof(TryDetermineLayouts)}: Failed to find layout for EZ for seed {seed}");
+ error = true;
+ }
+
+ return !error;
+ }
+
+ ///
+ /// Copied from , I changed some variable names to better reflect what is actually happening.
+ ///
+ /// The instance.
+ /// The layout rooms to iterate over.
+ private static void RandomizeInterpreted(System.Random rng, AtlasInterpretation[] interpretations)
+ {
+ int length = interpretations.Length;
+ while (length > 1)
+ {
+ --length;
+ int random = rng.Next(length + 1);
+ ref AtlasInterpretation current = ref interpretations[length];
+ ref AtlasInterpretation randomPick = ref interpretations[random];
+ AtlasInterpretation randomPickValue = interpretations[random];
+ AtlasInterpretation currentValue = interpretations[length];
+ current = randomPickValue;
+ randomPick = currentValue;
+ }
+ }
+ }
+}
\ No newline at end of file
From d6c1ffc9e4f3cd7e7020dcffd4382145e781d00f Mon Sep 17 00:00:00 2001
From: "@Someone" <45270312+Someone-193@users.noreply.github.com>
Date: Thu, 8 Jan 2026 00:19:44 -0500
Subject: [PATCH 2/4] Theoretically finished
The transpiler doesnt have any patching errors now and logically speaking everything *should* work, but I doubt that lol. I'll test this more tomorrow (Like seeing what the layout is and seeing if I can set it)
---
.../Patches/Events/Map/Generating.cs | 116 ++++++++++++++++--
1 file changed, 105 insertions(+), 11 deletions(-)
diff --git a/EXILED/Exiled.Events/Patches/Events/Map/Generating.cs b/EXILED/Exiled.Events/Patches/Events/Map/Generating.cs
index cc64650f4f..56f01250e2 100644
--- a/EXILED/Exiled.Events/Patches/Events/Map/Generating.cs
+++ b/EXILED/Exiled.Events/Patches/Events/Map/Generating.cs
@@ -20,6 +20,7 @@ namespace Exiled.Events.Patches.Events.Map
using HarmonyLib;
using LabApi.Events.Arguments.ServerEvents;
using MapGeneration;
+ using MapGeneration.Holidays;
using UnityEngine;
using static HarmonyLib.AccessTools;
@@ -32,6 +33,9 @@ namespace Exiled.Events.Patches.Events.Map
[HarmonyPatch(typeof(SeedSynchronizer), nameof(SeedSynchronizer.Awake))]
public class Generating
{
+ private static readonly List Candidates = new();
+ private static readonly List Spawned = new();
+
private static IEnumerable Transpiler(IEnumerable instructions, ILGenerator generator)
{
List newInstructions = ListPool.Pool.Get(instructions);
@@ -62,8 +66,6 @@ private static IEnumerable Transpiler(IEnumerable x.opcode == OpCodes.Ldloc_1);
- newInstructions[index].WithLabels(skipLabel);
-
/*
* To summarize this transpiler:
*
@@ -94,9 +96,6 @@ private static IEnumerable Transpiler(IEnumerable Transpiler(IEnumerable Transpiler(IEnumerable Transpiler(IEnumerable Transpiler(IEnumerable x.opcode == OpCodes.Ldloc_1);
+
+ newInstructions[index].WithLabels(skipLabel);
+
for (int z = 0; z < newInstructions.Count; z++)
yield return newInstructions[z];
@@ -251,9 +259,19 @@ private static int GenerateSeed(LczFacilityLayout lcz, HczFacilityLayout hcz, Ez
return best;
}
- // determines what layouts will be generated from a seed
+ ///
+ /// Determines what layouts will be generated from a seed, code comes from interpreting and sub-methods.
+ ///
+ /// The seed to find the layouts of.
+ /// The Light Containment Zone layout of the seed.
+ /// The Heavy Containment Zone layout of the seed.
+ /// The Entrance Zone layout of the seed.
+ /// Whether the method executed correctly.
private static bool TryDetermineLayouts(int seed, out LczFacilityLayout lcz, out HczFacilityLayout hcz, out EzFacilityLayout ez)
{
+ // (Surface gen + PD gen)
+ const int ExcludedZoneGeneratorCount = 2;
+
lcz = LczFacilityLayout.Unknown;
hcz = HczFacilityLayout.Unknown;
ez = EzFacilityLayout.Unknown;
@@ -262,7 +280,7 @@ private static bool TryDetermineLayouts(int seed, out LczFacilityLayout lcz, out
try
{
ZoneGenerator[] gens = SeedSynchronizer._singleton._zoneGenerators;
- for (int i = 0; i < gens.Length; i++)
+ for (int i = 0; i < gens.Length - ExcludedZoneGeneratorCount; i++)
{
ZoneGenerator generator = gens[i];
@@ -270,9 +288,8 @@ private static bool TryDetermineLayouts(int seed, out LczFacilityLayout lcz, out
{
// EntranceZoneGenerator should be the last zone generator
case EntranceZoneGenerator ezGen:
- if (i != gens.Length - 1)
+ if (i != gens.Length - 1 - ExcludedZoneGeneratorCount)
{
- Log.Error($"{typeof(Generating).FullName}.{nameof(TryDetermineLayouts)}: Last zone generator processed for seed {seed} was not an EntranceZoneGenerator!");
return false;
}
@@ -291,6 +308,8 @@ private static bool TryDetermineLayouts(int seed, out LczFacilityLayout lcz, out
Texture2D tex = gen.Atlases[layout];
AtlasInterpretation[] interpretations = MapAtlasInterpreter.Singleton.Interpret(tex, rng);
RandomizeInterpreted(rng, interpretations);
+ foreach (AtlasInterpretation interpretation in interpretations)
+ FakeSpawn(gen, interpretation, rng);
// this block "generates" them and accounts for duplicates and other things.
break;
@@ -348,5 +367,80 @@ private static void RandomizeInterpreted(System.Random rng, AtlasInterpretation[
randomPick = currentValue;
}
}
+
+ private static void FakeSpawn(AtlasZoneGenerator generator, AtlasInterpretation interpretation, System.Random rng)
+ {
+ Candidates.Clear();
+ float chanceMultiplier = 0F;
+ bool flag = interpretation.SpecificRooms.Length != 0;
+ foreach (SpawnableRoom room in generator.CompatibleRooms)
+ {
+ SpawnableRoom spawnableRoom = room;
+ if (spawnableRoom.HolidayVariants.TryGetResult(out SpawnableRoom result))
+ {
+ spawnableRoom = result;
+ }
+
+ int count = PreviouslySpawnedCount(spawnableRoom);
+ if (flag != spawnableRoom.SpecialRoom || (flag && !interpretation.SpecificRooms.Contains(spawnableRoom.Room.Name)) || spawnableRoom.Room.Shape != interpretation.RoomShape || count >= spawnableRoom.MaxAmount)
+ continue;
+
+ if (count < spawnableRoom.MinAmount)
+ {
+ Spawned.Add(new AtlasZoneGenerator.SpawnedRoomData
+ {
+ ChosenCandidate = spawnableRoom,
+ Instance = null!,
+ Interpretation = interpretation,
+ });
+
+ return;
+ }
+
+ chanceMultiplier += GetChanceWeight(interpretation.Coords, spawnableRoom);
+ Candidates.Add(spawnableRoom);
+ }
+
+ double random = rng.NextDouble() * chanceMultiplier;
+ float chance = 0F;
+ foreach (SpawnableRoom room in Candidates)
+ {
+ chance += GetChanceWeight(interpretation.Coords, room);
+ if (random > chance)
+ continue;
+
+ Spawned.Add(new AtlasZoneGenerator.SpawnedRoomData
+ {
+ ChosenCandidate = room,
+ Instance = null!,
+ Interpretation = interpretation,
+ });
+
+ return;
+ }
+ }
+
+ private static float GetChanceWeight(Vector2Int coords, SpawnableRoom candidate)
+ {
+ Vector2Int up = coords + Vector2Int.up;
+ Vector2Int down = coords + Vector2Int.down;
+ Vector2Int left = coords + Vector2Int.left;
+ Vector2Int right = coords + Vector2Int.right;
+ float chance = candidate.ChanceMultiplier;
+
+ foreach (AtlasZoneGenerator.SpawnedRoomData spawnedRoomData in Spawned)
+ {
+ if (spawnedRoomData.ChosenCandidate != candidate)
+ continue;
+
+ Vector2Int candidateCoords = spawnedRoomData.Interpretation.Coords;
+ if (candidateCoords == up || candidateCoords == down || candidateCoords == left || candidateCoords == right)
+ chance *= candidate.AdjacentChanceMultiplier;
+ }
+
+ return chance;
+ }
+
+ private static int PreviouslySpawnedCount(SpawnableRoom candidate) => Spawned.Count(spawnedRoomData => spawnedRoomData.ChosenCandidate == candidate);
}
}
\ No newline at end of file
From b03fec3dd3818325a5144d301e4204b702823f3b Mon Sep 17 00:00:00 2001
From: "@Someone" <45270312+Someone-193@users.noreply.github.com>
Date: Thu, 8 Jan 2026 12:29:52 -0500
Subject: [PATCH 3/4] Tidy up
Even actually works now, I just need to review this entire PR as a whole now and try to handle any problems n stuff better
---
.../EventArgs/Map/GeneratingEventArgs.cs | 42 ++-
.../Patches/Events/Map/Generating.cs | 310 +++++++++---------
2 files changed, 185 insertions(+), 167 deletions(-)
diff --git a/EXILED/Exiled.Events/EventArgs/Map/GeneratingEventArgs.cs b/EXILED/Exiled.Events/EventArgs/Map/GeneratingEventArgs.cs
index f14625995d..66149360a0 100644
--- a/EXILED/Exiled.Events/EventArgs/Map/GeneratingEventArgs.cs
+++ b/EXILED/Exiled.Events/EventArgs/Map/GeneratingEventArgs.cs
@@ -22,38 +22,60 @@ public class GeneratingEventArgs : IDeniableEvent
/// Initializes a new instance of the class.
///
///
- ///
- ///
- ///
+ ///
+ ///
+ ///
public GeneratingEventArgs(int seed, LczFacilityLayout lcz, HczFacilityLayout hcz, EzFacilityLayout ez)
{
LczLayout = lcz;
HczLayout = hcz;
EzLayout = ez;
+ TargetLczLayout = LczFacilityLayout.Unknown;
+ TargetHczLayout = HczFacilityLayout.Unknown;
+ TargetEzLayout = EzFacilityLayout.Unknown;
+
Seed = seed;
IsAllowed = true;
}
///
- /// Gets or sets the layout of the light containment zone.
+ /// Gets the lcz layout generated by .
+ ///
+ public LczFacilityLayout LczLayout { get; }
+
+ ///
+ /// Gets the hcz layout generated by .
+ ///
+ public HczFacilityLayout HczLayout { get; }
+
+ ///
+ /// Gets the ez layout generated by .
+ ///
+ public EzFacilityLayout EzLayout { get; }
+
+ ///
+ /// Gets or sets the lcz layout Exiled will attempt to generate a seed for.
///
- public LczFacilityLayout LczLayout { get; set; }
+ /// The default value, , indicates any layout is valid.
+ public LczFacilityLayout TargetLczLayout { get; set; }
///
- /// Gets or sets the layout of the heavy containment zone.
+ /// Gets or sets the hcz layout Exiled will attempt to generate a seed for.
///
- public HczFacilityLayout HczLayout { get; set; }
+ /// The default value, , indicates any layout is valid.
+ public HczFacilityLayout TargetHczLayout { get; set; }
///
- /// Gets or sets the layout of the entrance zone.
+ /// Gets or sets the ez layout Exiled will attempt to generate a seed for.
///
- public EzFacilityLayout EzLayout { get; set; }
+ /// The default value, , indicates any layout is valid.
+ public EzFacilityLayout TargetEzLayout { get; set; }
///
/// Gets or sets the seed of the map.
///
- /// This property overrides any changes in , , or .
+ /// This property overrides any changes in , , or .
public int Seed
{
get => seed;
diff --git a/EXILED/Exiled.Events/Patches/Events/Map/Generating.cs b/EXILED/Exiled.Events/Patches/Events/Map/Generating.cs
index 56f01250e2..ae64d4304e 100644
--- a/EXILED/Exiled.Events/Patches/Events/Map/Generating.cs
+++ b/EXILED/Exiled.Events/Patches/Events/Map/Generating.cs
@@ -9,6 +9,7 @@ namespace Exiled.Events.Patches.Events.Map
{
using System;
using System.Collections.Generic;
+ using System.Diagnostics;
using System.Linq;
using System.Reflection.Emit;
@@ -36,6 +37,103 @@ public class Generating
private static readonly List Candidates = new();
private static readonly List Spawned = new();
+ ///
+ /// Determines what layouts will be generated from a seed, code comes from interpreting and sub-methods.
+ ///
+ /// The seed to find the layouts of.
+ /// The Light Containment Zone layout of the seed.
+ /// The Heavy Containment Zone layout of the seed.
+ /// The Entrance Zone layout of the seed.
+ /// Whether the method executed correctly.
+ internal static bool TryDetermineLayouts(int seed, out LczFacilityLayout lcz, out HczFacilityLayout hcz, out EzFacilityLayout ez)
+ {
+ // (Surface gen + PD gen)
+ const int ExcludedZoneGeneratorCount = 2;
+
+ lcz = LczFacilityLayout.Unknown;
+ hcz = HczFacilityLayout.Unknown;
+ ez = EzFacilityLayout.Unknown;
+
+ System.Random rng = new(seed);
+ try
+ {
+ ZoneGenerator[] gens = SeedSynchronizer._singleton._zoneGenerators;
+ for (int i = 0; i < gens.Length - ExcludedZoneGeneratorCount; i++)
+ {
+ Spawned.Clear();
+ ZoneGenerator generator = gens[i];
+
+ switch (generator)
+ {
+ // EntranceZoneGenerator should be the last zone generator
+ case EntranceZoneGenerator ezGen:
+ if (i != gens.Length - 1 - ExcludedZoneGeneratorCount)
+ {
+ Log.Error("EntranceZoneGenerator was not in expected index!");
+ return false;
+ }
+
+ ez = (EzFacilityLayout)(rng.Next(ezGen.Atlases.Length) + 1);
+ break;
+ case AtlasZoneGenerator gen:
+ int layout = rng.Next(gen.Atlases.Length);
+
+ if (gen is LightContainmentZoneGenerator)
+ lcz = (LczFacilityLayout)(layout + 1);
+ else
+ hcz = (HczFacilityLayout)(layout + 1);
+
+ // rng needs to be called the same amount as during map gen for next zone generator.
+ // this block of code picks what rooms to generate.
+ Texture2D tex = gen.Atlases[layout];
+ AtlasInterpretation[] interpretations = MapAtlasInterpreter.Singleton.Interpret(tex, rng);
+ RandomizeInterpreted(rng, interpretations);
+
+ // debugs here are good for determining if the wrong # of rng.Next();'s are called.
+ Log.Debug(interpretations.Length);
+
+ // this block "generates" them and accounts for duplicates and other things.
+ for (int j = 0; j < interpretations.Length; j++)
+ {
+ Log.Debug(interpretations[j].ToString());
+ FakeSpawn(gen, interpretations[j], rng);
+ }
+
+ break;
+ default:
+ Log.Error($"{typeof(Generating).FullName}.{nameof(TryDetermineLayouts)}: Found non AtlasZoneGenerator [{generator}] in SeedSynchronizer._singleton._zoneGenerators!");
+ return false;
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ Log.Error(ex);
+ return false;
+ }
+
+ bool error = false;
+ if (lcz is LczFacilityLayout.Unknown)
+ {
+ Log.Error($"{typeof(Generating).FullName}.{nameof(TryDetermineLayouts)}: Failed to find layout for LCZ for seed {seed}");
+ error = true;
+ }
+
+ if (hcz is HczFacilityLayout.Unknown)
+ {
+ Log.Error($"{typeof(Generating).FullName}.{nameof(TryDetermineLayouts)}: Failed to find layout for HCZ for seed {seed}");
+ error = true;
+ }
+
+ if (ez is EzFacilityLayout.Unknown)
+ {
+ Log.Error($"{typeof(Generating).FullName}.{nameof(TryDetermineLayouts)}: Failed to find layout for EZ for seed {seed}");
+ error = true;
+ }
+
+ return !error;
+ }
+
private static IEnumerable Transpiler(IEnumerable instructions, ILGenerator generator)
{
List newInstructions = ListPool.Pool.Get(instructions);
@@ -53,15 +151,6 @@ private static IEnumerable Transpiler(IEnumerable x.opcode == OpCodes.Ldloc_1);
@@ -84,11 +173,11 @@ private static IEnumerable Transpiler(IEnumerable Transpiler(IEnumerable bestMatches)
- {
- best = seed;
- bestMatches = matches;
- }
-
- if (bestMatches is 3)
- break;
- }
+ Stopwatch debug = new();
+ debug.Start();
- return best;
- }
+ int i = 0;
- ///
- /// Determines what layouts will be generated from a seed, code comes from interpreting and sub-methods.
- ///
- /// The seed to find the layouts of.
- /// The Light Containment Zone layout of the seed.
- /// The Heavy Containment Zone layout of the seed.
- /// The Entrance Zone layout of the seed.
- /// Whether the method executed correctly.
- private static bool TryDetermineLayouts(int seed, out LczFacilityLayout lcz, out HczFacilityLayout hcz, out EzFacilityLayout ez)
- {
- // (Surface gen + PD gen)
- const int ExcludedZoneGeneratorCount = 2;
-
- lcz = LczFacilityLayout.Unknown;
- hcz = HczFacilityLayout.Unknown;
- ez = EzFacilityLayout.Unknown;
-
- System.Random rng = new(seed);
try
{
- ZoneGenerator[] gens = SeedSynchronizer._singleton._zoneGenerators;
- for (int i = 0; i < gens.Length - ExcludedZoneGeneratorCount; i++)
+ // TODO: optimize, increase max iterations, and calculate probability of failure.
+ for (i = 0; i < 1000; i++)
{
- ZoneGenerator generator = gens[i];
+ int matches = 0;
- switch (generator)
- {
- // EntranceZoneGenerator should be the last zone generator
- case EntranceZoneGenerator ezGen:
- if (i != gens.Length - 1 - ExcludedZoneGeneratorCount)
- {
- return false;
- }
+ int seed = seedGenerator.Next(1, int.MaxValue);
- ez = (EzFacilityLayout)(rng.Next(ezGen.Atlases.Length) + 1);
- break;
- case AtlasZoneGenerator gen:
- int layout = rng.Next(gen.Atlases.Length);
+ if (!TryDetermineLayouts(seed, out LczFacilityLayout currLcz, out HczFacilityLayout currHcz, out EzFacilityLayout currEz))
+ {
+ break;
+ }
- if (gen is LightContainmentZoneGenerator)
- lcz = (LczFacilityLayout)(layout + 1);
- else
- hcz = (HczFacilityLayout)(layout + 1);
+ if (lcz is LczFacilityLayout.Unknown || currLcz == lcz)
+ matches++;
+ if (hcz is HczFacilityLayout.Unknown || currHcz == hcz)
+ matches++;
+ if (ez is EzFacilityLayout.Unknown || currEz == ez)
+ matches++;
- // rng needs to be called the same amount as during map gen for next zone generator.
- // this block of code picks what rooms to generate.
- Texture2D tex = gen.Atlases[layout];
- AtlasInterpretation[] interpretations = MapAtlasInterpreter.Singleton.Interpret(tex, rng);
- RandomizeInterpreted(rng, interpretations);
- foreach (AtlasInterpretation interpretation in interpretations)
- FakeSpawn(gen, interpretation, rng);
+ if (matches > bestMatches)
+ {
+ best = seed;
+ bestMatches = matches;
+ }
- // this block "generates" them and accounts for duplicates and other things.
- break;
- default:
- Log.Error($"{typeof(Generating).FullName}.{nameof(TryDetermineLayouts)}: Found non AtlasZoneGenerator [{generator}] in SeedSynchronizer._singleton._zoneGenerators!");
- return false;
+ if (bestMatches is 3)
+ {
+ if (lcz != currLcz)
+ {
+ Log.Error($"{typeof(Generating).FullName}.{nameof(GenerateSeed)}: A logical error occured processing {seed}. Data: {matches}, {bestMatches}, {currLcz}, {currHcz}, {currEz}");
+ }
+
+ if (hcz != currHcz)
+ {
+ Log.Error($"{typeof(Generating).FullName}.{nameof(GenerateSeed)}: A logical error occured processing {seed}. Data: {matches}, {bestMatches}, {currLcz}, {currHcz}, {currEz}");
+ }
+
+ if (ez != currEz)
+ {
+ Log.Error($"{typeof(Generating).FullName}.{nameof(GenerateSeed)}: A logical error occured processing {seed}. Data: {matches}, {bestMatches}, {currLcz}, {currHcz}, {currEz}");
+ }
+
+ break;
}
}
}
catch (Exception ex)
{
Log.Error(ex);
- return false;
}
- bool error = false;
- if (lcz is LczFacilityLayout.Unknown)
- {
- Log.Error($"{typeof(Generating).FullName}.{nameof(TryDetermineLayouts)}: Failed to find layout for LCZ for seed {seed}");
- error = true;
- }
+ debug.Stop();
+ Log.Debug($"Attempted {i} seeds in {debug.Elapsed.TotalSeconds}");
- if (hcz is HczFacilityLayout.Unknown)
- {
- Log.Error($"{typeof(Generating).FullName}.{nameof(TryDetermineLayouts)}: Failed to find layout for HCZ for seed {seed}");
- error = true;
- }
-
- if (ez is EzFacilityLayout.Unknown)
- {
- Log.Error($"{typeof(Generating).FullName}.{nameof(TryDetermineLayouts)}: Failed to find layout for EZ for seed {seed}");
- error = true;
- }
-
- return !error;
+ return best;
}
///
@@ -368,12 +366,12 @@ private static void RandomizeInterpreted(System.Random rng, AtlasInterpretation[
}
}
- private static void FakeSpawn(AtlasZoneGenerator generator, AtlasInterpretation interpretation, System.Random rng)
+ private static void FakeSpawn(AtlasZoneGenerator gen, AtlasInterpretation interpretation, System.Random rng)
{
Candidates.Clear();
float chanceMultiplier = 0F;
bool flag = interpretation.SpecificRooms.Length != 0;
- foreach (SpawnableRoom room in generator.CompatibleRooms)
+ foreach (SpawnableRoom room in gen.CompatibleRooms)
{
SpawnableRoom spawnableRoom = room;
if (spawnableRoom.HolidayVariants.TryGetResult(out SpawnableRoom result))
@@ -381,7 +379,7 @@ private static void FakeSpawn(AtlasZoneGenerator generator, AtlasInterpretation
spawnableRoom = result;
}
- int count = PreviouslySpawnedCount(spawnableRoom);
+ int count = Spawned.Count(spawned => spawned.ChosenCandidate == spawnableRoom);
if (flag != spawnableRoom.SpecialRoom || (flag && !interpretation.SpecificRooms.Contains(spawnableRoom.Room.Name)) || spawnableRoom.Room.Shape != interpretation.RoomShape || count >= spawnableRoom.MaxAmount)
continue;
@@ -440,7 +438,5 @@ private static float GetChanceWeight(Vector2Int coords, SpawnableRoom candidate)
return chance;
}
-
- private static int PreviouslySpawnedCount(SpawnableRoom candidate) => Spawned.Count(spawnedRoomData => spawnedRoomData.ChosenCandidate == candidate);
}
}
\ No newline at end of file
From 487c52a87f1883f9b68fa4237f4213e7e11a4841 Mon Sep 17 00:00:00 2001
From: "@Someone" <45270312+Someone-193@users.noreply.github.com>
Date: Thu, 8 Jan 2026 20:44:23 -0500
Subject: [PATCH 4/4] =?UTF-8?q?stupid=20baguette=20man=20making=20me=20cha?=
=?UTF-8?q?nge=20transpiler=20=F0=9F=98=AD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../EventArgs/Map/GeneratingEventArgs.cs | 8 +---
.../Patches/Events/Map/Generating.cs | 42 +++++++++----------
2 files changed, 20 insertions(+), 30 deletions(-)
diff --git a/EXILED/Exiled.Events/EventArgs/Map/GeneratingEventArgs.cs b/EXILED/Exiled.Events/EventArgs/Map/GeneratingEventArgs.cs
index 66149360a0..7e29c9394c 100644
--- a/EXILED/Exiled.Events/EventArgs/Map/GeneratingEventArgs.cs
+++ b/EXILED/Exiled.Events/EventArgs/Map/GeneratingEventArgs.cs
@@ -16,8 +16,6 @@ namespace Exiled.Events.EventArgs.Map
///
public class GeneratingEventArgs : IDeniableEvent
{
- private int seed;
-
///
/// Initializes a new instance of the class.
///
@@ -76,11 +74,7 @@ public GeneratingEventArgs(int seed, LczFacilityLayout lcz, HczFacilityLayout hc
/// Gets or sets the seed of the map.
///
/// This property overrides any changes in , , or .
- public int Seed
- {
- get => seed;
- set => seed = Mathf.Clamp(value, 0, int.MaxValue);
- }
+ public int Seed { get; set; }
///
/// Gets or sets a value indicating whether the map can be generated.
diff --git a/EXILED/Exiled.Events/Patches/Events/Map/Generating.cs b/EXILED/Exiled.Events/Patches/Events/Map/Generating.cs
index ae64d4304e..443dfc0db0 100644
--- a/EXILED/Exiled.Events/Patches/Events/Map/Generating.cs
+++ b/EXILED/Exiled.Events/Patches/Events/Map/Generating.cs
@@ -153,37 +153,37 @@ private static IEnumerable Transpiler(IEnumerable x.opcode == OpCodes.Ldloc_1);
+ int offset = -2;
+ int index = newInstructions.FindIndex(x => x.opcode == OpCodes.Stloc_1) + offset;
/*
* To summarize this transpiler:
*
- * if (!TryDetermineLayouts(ev2.Seed, out LczFacilityLayout lcz, out HczFacilityLayout hcz, out EzFacilityLayout ez)
+ * if (!TryDetermineLayouts(SeedSynchronizer.Seed, out LczFacilityLayout lcz, out HczFacilityLayout hcz, out EzFacilityLayout ez)
* goto skipEvent;
*
- * ev = new GeneratingEventArgs(ev2.Seed, lcz, hcz, ez);
+ * ev = new GeneratingEventArgs(SeedSynchronizer.Seed, lcz, hcz, ez);
* Handlers.Map.OnGenerating(ev);
*
* if (!ev.IsAllowed)
* goto "Map generation cancelled by a plugin." debug statement;
*
- * if (ev2.Seed != ev.Seed)
+ * if (SeedSynchronizer.Seed != ev.Seed)
* {
- * ev2.Seed = ev.Seed;
+ * SeedSynchronizer.Seed = ev.Seed;
* goto skipEvent;
* }
*
* int newSeed = GenerateSeed(ev.TargetLczLayout, ev.TargetHczLayout, ev.TargetEzLayout);
* if (newSeed == -1)
* goto skipEvent;
- * ev2.Seed = newSeed;
+ * SeedSynchronizer.Seed = newSeed;
*/
newInstructions.InsertRange(index, new[]
{
- // ev.Seed (from LabAPI event)
- new CodeInstruction(OpCodes.Ldloc_1).MoveLabelsFrom(newInstructions[index]),
- new(OpCodes.Callvirt, PropertyGetter(typeof(MapGeneratingEventArgs), nameof(MapGeneratingEventArgs.Seed))),
+ // SeedSynchronizer.Seed
+ new CodeInstruction(OpCodes.Call, PropertyGetter(typeof(SeedSynchronizer), nameof(SeedSynchronizer.Seed))).MoveLabelsFrom(newInstructions[index]),
// TryDetermineLayouts(ev.Seed, out lcz, out hcz, out ez)
new(OpCodes.Ldloca_S, lcz),
@@ -194,9 +194,8 @@ private static IEnumerable Transpiler(IEnumerable Transpiler(IEnumerable Transpiler(IEnumerable x.opcode == OpCodes.Ldloc_1);