diff --git a/EXILED/Exiled.Events/EventArgs/Map/GeneratingEventArgs.cs b/EXILED/Exiled.Events/EventArgs/Map/GeneratingEventArgs.cs
index 7e29c9394..15725ac6c 100644
--- a/EXILED/Exiled.Events/EventArgs/Map/GeneratingEventArgs.cs
+++ b/EXILED/Exiled.Events/EventArgs/Map/GeneratingEventArgs.cs
@@ -14,6 +14,7 @@ namespace Exiled.Events.EventArgs.Map
///
/// Contains all information after the server generates a seed, but before the map is generated.
///
+ /// The target layout properties have a miniscule (but non-zero) chance of not working, make sure your event can handle the edge case of failure if you want reliability.
public class GeneratingEventArgs : IDeniableEvent
{
///
diff --git a/EXILED/Exiled.Events/Handlers/Internal/Round.cs b/EXILED/Exiled.Events/Handlers/Internal/Round.cs
index 8cad547ff..cebcffa74 100644
--- a/EXILED/Exiled.Events/Handlers/Internal/Round.cs
+++ b/EXILED/Exiled.Events/Handlers/Internal/Round.cs
@@ -57,6 +57,9 @@ public static void OnWaitingForPlayers()
TranslationManager.Reload();
RoundSummary.RoundLock = false;
+
+ if (Events.Instance.Config.Debug)
+ Patches.Events.Map.Generating.Benchmark();
}
///
diff --git a/EXILED/Exiled.Events/Patches/Events/Map/Generating.cs b/EXILED/Exiled.Events/Patches/Events/Map/Generating.cs
index 443dfc0db..ce68e1588 100644
--- a/EXILED/Exiled.Events/Patches/Events/Map/Generating.cs
+++ b/EXILED/Exiled.Events/Patches/Events/Map/Generating.cs
@@ -34,8 +34,12 @@ namespace Exiled.Events.Patches.Events.Map
[HarmonyPatch(typeof(SeedSynchronizer), nameof(SeedSynchronizer.Awake))]
public class Generating
{
+ // (Surface gen + PD gen)
+ private const int ExcludedZoneGeneratorCount = 2;
+
private static readonly List Candidates = new();
private static readonly List Spawned = new();
+ private static readonly Dictionary SpawnCounts = new();
///
/// Determines what layouts will be generated from a seed, code comes from interpreting and sub-methods.
@@ -45,11 +49,8 @@ public class Generating
/// 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)
+ public 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;
@@ -61,6 +62,7 @@ internal static bool TryDetermineLayouts(int seed, out LczFacilityLayout lcz, ou
for (int i = 0; i < gens.Length - ExcludedZoneGeneratorCount; i++)
{
Spawned.Clear();
+ SpawnCounts.Clear();
ZoneGenerator generator = gens[i];
switch (generator)
@@ -134,6 +136,127 @@ internal static bool TryDetermineLayouts(int seed, out LczFacilityLayout lcz, ou
return !error;
}
+ ///
+ /// Benchmarks 10000 times and prints the average ticks for each important step.
+ ///
+ /// Whether the method executed correctly.
+ internal static bool Benchmark()
+ {
+ bool error = false;
+ long hczInterpretationTicks = 0;
+ long hczFakeSpawnTicks = 0;
+ long lczInterpretationTicks = 0;
+ long lczFakeSpawnTicks = 0;
+
+ LczFacilityLayout lcz = LczFacilityLayout.Unknown;
+ HczFacilityLayout hcz = HczFacilityLayout.Unknown;
+ EzFacilityLayout ez = EzFacilityLayout.Unknown;
+
+ Stopwatch sw = new();
+
+ System.Random seedGenerator = new();
+ int k;
+ for (k = 1; k <= 10000; k++)
+ {
+ System.Random rng = new(seedGenerator.Next(1, int.MaxValue));
+ try
+ {
+ ZoneGenerator[] gens = SeedSynchronizer._singleton._zoneGenerators;
+ for (int i = 0; i < gens.Length - ExcludedZoneGeneratorCount; i++)
+ {
+ Spawned.Clear();
+ SpawnCounts.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:
+ bool isLight = gen is LightContainmentZoneGenerator;
+
+ int layout = rng.Next(gen.Atlases.Length);
+
+ if (isLight)
+ 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];
+
+ sw.Restart();
+ AtlasInterpretation[] interpretations = Interpret(tex, rng);
+ if (isLight)
+ lczInterpretationTicks += sw.ElapsedTicks;
+ else
+ hczInterpretationTicks += sw.ElapsedTicks;
+
+ RandomizeInterpreted(rng, interpretations);
+
+ sw.Restart();
+
+ // this block "generates" them and accounts for duplicates and other things.
+ for (int j = 0; j < interpretations.Length; j++)
+ {
+ FakeSpawn(gen, interpretations[j], rng);
+ }
+
+ if (isLight)
+ lczFakeSpawnTicks += sw.ElapsedTicks;
+ else
+ hczFakeSpawnTicks += sw.ElapsedTicks;
+ 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;
+ }
+
+ if (lcz is LczFacilityLayout.Unknown)
+ {
+ Log.Error($"{typeof(Generating).FullName}.{nameof(TryDetermineLayouts)}: Failed to find layout for LCZ during benchmark!");
+ error = true;
+ }
+
+ if (hcz is HczFacilityLayout.Unknown)
+ {
+ Log.Error($"{typeof(Generating).FullName}.{nameof(TryDetermineLayouts)}: Failed to find layout for HCZ during benchmark!");
+ error = true;
+ }
+
+ if (ez is EzFacilityLayout.Unknown)
+ {
+ Log.Error($"{typeof(Generating).FullName}.{nameof(TryDetermineLayouts)}: Failed to find layout for EZ during benchmark!");
+ error = true;
+ }
+
+ if (error)
+ break;
+ }
+
+ Log.Debug($"Average hcz interpretation ticks: {hczInterpretationTicks / (double)k}");
+ Log.Debug($"Average hcz fake spawn ticks: {hczFakeSpawnTicks / (double)k}");
+ Log.Debug($"Average lcz interpretation ticks: {lczInterpretationTicks / (double)k}");
+ Log.Debug($"Average lcz fake spawn ticks: {lczFakeSpawnTicks / (double)k}");
+ return !error;
+ }
+
private static IEnumerable Transpiler(IEnumerable instructions, ILGenerator generator)
{
List newInstructions = ListPool.Pool.Get(instructions);
@@ -256,9 +379,10 @@ private static IEnumerable Transpiler(IEnumerable x.opcode == OpCodes.Ldloc_1);
+ offset = -1;
+ index = newInstructions.FindIndex(x => x.operand == (object)GetDeclaredConstructors(typeof(MapGeneratingEventArgs))[0]) + offset;
- newInstructions[index].WithLabels(skipLabel);
+ newInstructions[index].labels.Add(skipLabel);
for (int z = 0; z < newInstructions.Count; z++)
yield return newInstructions[z];
@@ -266,7 +390,7 @@ private static IEnumerable Transpiler(IEnumerable.Pool.Return(newInstructions);
}
- // generates a seed for target layouts
+ // generates a seed for target layouts, at 5k attempts, failure is only 2 in a billion.
private static int GenerateSeed(LczFacilityLayout lcz, HczFacilityLayout hcz, EzFacilityLayout ez)
{
if (lcz is LczFacilityLayout.Unknown && hcz is HczFacilityLayout.Unknown && ez is EzFacilityLayout.Unknown)
@@ -277,21 +401,19 @@ private static int GenerateSeed(LczFacilityLayout lcz, HczFacilityLayout hcz, Ez
int best = -1;
int bestMatches = 0;
- Stopwatch debug = new();
- debug.Start();
-
int i = 0;
-
+ LczFacilityLayout currLcz = LczFacilityLayout.Unknown;
+ HczFacilityLayout currHcz = HczFacilityLayout.Unknown;
+ EzFacilityLayout currEz = EzFacilityLayout.Unknown;
try
{
- // TODO: optimize, increase max iterations, and calculate probability of failure.
- for (i = 0; i < 1000; i++)
+ for (i = 0; i < 5000; i++)
{
int matches = 0;
int seed = seedGenerator.Next(1, int.MaxValue);
- if (!TryDetermineLayouts(seed, out LczFacilityLayout currLcz, out HczFacilityLayout currHcz, out EzFacilityLayout currEz))
+ if (!TryDetermineLayouts(seed, out currLcz, out currHcz, out currEz))
{
break;
}
@@ -335,8 +457,10 @@ private static int GenerateSeed(LczFacilityLayout lcz, HczFacilityLayout hcz, Ez
Log.Error(ex);
}
- debug.Stop();
- Log.Debug($"Attempted {i} seeds in {debug.Elapsed.TotalSeconds}");
+ if (i is 5000 && (lcz != currLcz || hcz != currHcz || ez != currEz))
+ {
+ Log.Warn($"{typeof(Generating).FullName}.{nameof(GenerateSeed)}: Failed to generate a desired seed for {lcz}, {hcz}, {ez}.\nAccording to my calculations, this is like 2 in a billion, so maybe go hit the Casino!!!!");
+ }
return best;
}
@@ -366,17 +490,30 @@ private static void FakeSpawn(AtlasZoneGenerator gen, AtlasInterpretation interp
{
Candidates.Clear();
float chanceMultiplier = 0F;
- bool flag = interpretation.SpecificRooms.Length != 0;
+ bool hasSpecificRoom = interpretation.SpecificRooms.Length != 0;
+
+ bool isHoliday = HolidayUtils.IsAnyHolidayActive();
foreach (SpawnableRoom room in gen.CompatibleRooms)
{
SpawnableRoom spawnableRoom = room;
- if (spawnableRoom.HolidayVariants.TryGetResult(out SpawnableRoom result))
+ if (isHoliday && spawnableRoom.HolidayVariants.TryGetResult(out SpawnableRoom result))
{
spawnableRoom = result;
}
- 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)
+ if (!SpawnCounts.TryGetValue(spawnableRoom, out int count))
+ count = SpawnCounts[spawnableRoom] = 0;
+
+ if (hasSpecificRoom != spawnableRoom.SpecialRoom)
+ continue;
+
+ if (spawnableRoom.Room.Shape != interpretation.RoomShape)
+ continue;
+
+ if (count >= spawnableRoom.MaxAmount)
+ continue;
+
+ if (hasSpecificRoom && !interpretation.SpecificRooms.Contains(spawnableRoom.Room.Name))
continue;
if (count < spawnableRoom.MinAmount)
@@ -388,6 +525,7 @@ private static void FakeSpawn(AtlasZoneGenerator gen, AtlasInterpretation interp
Interpretation = interpretation,
});
+ SpawnCounts[spawnableRoom]++;
return;
}
@@ -397,8 +535,9 @@ private static void FakeSpawn(AtlasZoneGenerator gen, AtlasInterpretation interp
double random = rng.NextDouble() * chanceMultiplier;
float chance = 0F;
- foreach (SpawnableRoom room in Candidates)
+ for (int i = 0; i < Candidates.Count; i++)
{
+ SpawnableRoom room = Candidates[i];
chance += GetChanceWeight(interpretation.Coords, room);
if (random > chance)
continue;
@@ -410,29 +549,88 @@ private static void FakeSpawn(AtlasZoneGenerator gen, AtlasInterpretation interp
Interpretation = interpretation,
});
+ SpawnCounts[room]++;
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;
+ if (Math.Abs(candidate.AdjacentChanceMultiplier - 1) < 0.001F)
+ return chance;
+
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)
+ if ((spawnedRoomData.Interpretation.Coords - coords).sqrMagnitude is 1)
chance *= candidate.AdjacentChanceMultiplier;
}
return chance;
}
+
+ private static unsafe AtlasInterpretation[] Interpret(Texture2D atlas, System.Random rng)
+ {
+ MapAtlasInterpreter.ResultsBuffer.Clear();
+
+ int width = atlas.width;
+ int height = atlas.height;
+ int jump = 1;
+ int startX = 0;
+ bool flag = false;
+ byte[] pixels = atlas.GetRawTextureData();
+
+ fixed (byte* ptr = pixels)
+ {
+ int bytesPerRow = width * 3;
+ int stride = (bytesPerRow + 3) & ~3;
+
+ for (int y = 0; y < width; y += jump)
+ {
+ byte* row = ptr + (y * stride);
+
+ for (int x = startX; x < height; x += jump)
+ {
+ byte* pixel = row + (x * 3);
+
+ GlyphShapePair? nullable = ScanPixel(pixel[0], pixel[1], pixel[2]);
+
+ if (!nullable.HasValue)
+ continue;
+
+ if (!flag)
+ {
+ Vector2Int glyphCenterOffset = nullable.Value.GlyphCenterOffset;
+ x += glyphCenterOffset.x;
+ y += glyphCenterOffset.y;
+ jump = 3;
+ startX = x % 3;
+ flag = true;
+
+ row = ptr + (y * stride);
+ }
+
+ MapAtlasInterpreter.ResultsBuffer.Add(new AtlasInterpretation(nullable.Value, rng, x, y));
+ }
+ }
+ }
+
+ return MapAtlasInterpreter.ResultsBuffer.ToArray();
+ }
+
+ private static GlyphShapePair? ScanPixel(byte r, byte g, byte b)
+ {
+ foreach (GlyphShapePair pairDefinition in MapAtlasInterpreter.Singleton.PairDefinitions)
+ {
+ if (Mathf.Abs(pairDefinition.GlyphColor.r - r) <= 5 && Mathf.Abs(pairDefinition.GlyphColor.g - g) <= 5 && Mathf.Abs(pairDefinition.GlyphColor.b - b) <= 5)
+ return pairDefinition;
+ }
+
+ return null;
+ }
}
}
\ No newline at end of file