diff --git a/EXILED/Exiled.Events/EventArgs/Map/GeneratingEventArgs.cs b/EXILED/Exiled.Events/EventArgs/Map/GeneratingEventArgs.cs new file mode 100644 index 000000000..7e29c9394 --- /dev/null +++ b/EXILED/Exiled.Events/EventArgs/Map/GeneratingEventArgs.cs @@ -0,0 +1,85 @@ +// ----------------------------------------------------------------------- +// +// 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 + { + /// + /// 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 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. + /// + /// The default value, , indicates any layout is valid. + public LczFacilityLayout TargetLczLayout { get; set; } + + /// + /// Gets or sets the hcz layout Exiled will attempt to generate a seed for. + /// + /// The default value, , indicates any layout is valid. + public HczFacilityLayout TargetHczLayout { get; set; } + + /// + /// Gets or sets the ez layout Exiled will attempt to generate a seed for. + /// + /// 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 . + public int Seed { get; set; } + + /// + /// 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 4db303b36..d8890bfa5 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 000000000..443dfc0db --- /dev/null +++ b/EXILED/Exiled.Events/Patches/Events/Map/Generating.cs @@ -0,0 +1,438 @@ +// ----------------------------------------------------------------------- +// +// 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.Diagnostics; + 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 MapGeneration.Holidays; + 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 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); + + 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)); + + LocalBuilder newSeed = generator.DeclareLocal(typeof(int)); + + int offset = -2; + int index = newInstructions.FindIndex(x => x.opcode == OpCodes.Stloc_1) + offset; + + /* + * To summarize this transpiler: + * + * if (!TryDetermineLayouts(SeedSynchronizer.Seed, out LczFacilityLayout lcz, out HczFacilityLayout hcz, out EzFacilityLayout ez) + * goto skipEvent; + * + * 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 (SeedSynchronizer.Seed != ev.Seed) + * { + * SeedSynchronizer.Seed = ev.Seed; + * goto skipEvent; + * } + * + * int newSeed = GenerateSeed(ev.TargetLczLayout, ev.TargetHczLayout, ev.TargetEzLayout); + * if (newSeed == -1) + * goto skipEvent; + * SeedSynchronizer.Seed = newSeed; + */ + + newInstructions.InsertRange(index, new[] + { + // 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), + 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), + + // SeedSynchronizer.Seed again + new(OpCodes.Call, PropertyGetter(typeof(SeedSynchronizer), nameof(SeedSynchronizer.Seed))), + + // 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), + 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 (SeedSynchronizer.Seed != ev.Seed) + new(OpCodes.Call, PropertyGetter(typeof(SeedSynchronizer), nameof(SeedSynchronizer.Seed))), + new(OpCodes.Ldloc_S, ev), + new(OpCodes.Callvirt, PropertyGetter(typeof(GeneratingEventArgs), nameof(GeneratingEventArgs.Seed))), + new(OpCodes.Beq_S, continueEventLabel), + + // { + // SeedSynchronizer.Seed = ev.Seed + new(OpCodes.Ldloc_S, ev), + new(OpCodes.Callvirt, PropertyGetter(typeof(GeneratingEventArgs), nameof(GeneratingEventArgs.Seed))), + new(OpCodes.Call, PropertySetter(typeof(SeedSynchronizer), nameof(SeedSynchronizer.Seed))), + + // skip other event code; + new(OpCodes.Br_S, skipLabel), + + // } + new CodeInstruction(OpCodes.Ldloc_S, ev).WithLabels(continueEventLabel), + new(OpCodes.Callvirt, PropertyGetter(typeof(GeneratingEventArgs), nameof(GeneratingEventArgs.TargetLczLayout))), + + new(OpCodes.Ldloc_S, ev), + new(OpCodes.Callvirt, PropertyGetter(typeof(GeneratingEventArgs), nameof(GeneratingEventArgs.TargetHczLayout))), + + new(OpCodes.Ldloc_S, ev), + new(OpCodes.Callvirt, PropertyGetter(typeof(GeneratingEventArgs), nameof(GeneratingEventArgs.TargetEzLayout))), + + // int newSeed = GenerateSeed(ev.TargetLczLayout, ev.TargetHczLayout, ev.TargetEzLayout); + new(OpCodes.Call, Method(typeof(Generating), nameof(GenerateSeed))), + 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), + + // SeedSynchronizer.Seed = newSeed; + new(OpCodes.Ldloc_S, newSeed), + new(OpCodes.Call, PropertySetter(typeof(SeedSynchronizer), nameof(SeedSynchronizer.Seed))), + }); + + index = newInstructions.FindLastIndex(x => x.opcode == OpCodes.Ldloc_1); + + newInstructions[index].WithLabels(skipLabel); + + 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; + + Stopwatch debug = new(); + debug.Start(); + + int i = 0; + + try + { + // TODO: optimize, increase max iterations, and calculate probability of failure. + for (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) + { + 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); + } + + debug.Stop(); + Log.Debug($"Attempted {i} seeds in {debug.Elapsed.TotalSeconds}"); + + return best; + } + + /// + /// 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; + } + } + + 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 gen.CompatibleRooms) + { + SpawnableRoom spawnableRoom = room; + if (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) + 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; + } + } +} \ No newline at end of file