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);