using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using BepInEx;
using BepInEx.Bootstrap;
using BepInEx.Configuration;
using BepInEx.Logging;
using HarmonyLib;
using Microsoft.CodeAnalysis;
using StraftatModding;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.SceneManagement;
[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.Default | DebuggableAttribute.DebuggingModes.DisableOptimizations | DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints | DebuggableAttribute.DebuggingModes.EnableEditAndContinue)]
[assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")]
[assembly: AssemblyCompany("StraftShuffle")]
[assembly: AssemblyConfiguration("Debug")]
[assembly: AssemblyFileVersion("0.3.1.0")]
[assembly: AssemblyInformationalVersion("0.3.1+2d9a4faaa1ab2a143f6d2c726cfb78d7f9ac2453")]
[assembly: AssemblyProduct("StraftShuffle")]
[assembly: AssemblyTitle("StraftShuffle")]
[assembly: AssemblyVersion("0.3.1.0")]
[module: RefSafetyRules(11)]
namespace Microsoft.CodeAnalysis
{
[CompilerGenerated]
[Embedded]
internal sealed class EmbeddedAttribute : Attribute
{
}
}
namespace System.Runtime.CompilerServices
{
[CompilerGenerated]
[Embedded]
[AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)]
internal sealed class RefSafetyRulesAttribute : Attribute
{
public readonly int Version;
public RefSafetyRulesAttribute(int P_0)
{
Version = P_0;
}
}
}
namespace StraftatModding
{
internal static class ModPresence
{
public const string MoreStraftsGuid = "com.nitrogenia.morestrafts";
public const string UiSpawnAddonGuid = "com.morestrafts.uispawn.addon";
public static bool MoreStrafts => IsLoaded("com.nitrogenia.morestrafts");
public static bool UiSpawnAddon => IsLoaded("com.morestrafts.uispawn.addon");
public static bool IsLoaded(string guid)
{
try
{
return Chainloader.PluginInfos != null && Chainloader.PluginInfos.ContainsKey(guid);
}
catch
{
return false;
}
}
public static string VersionOf(string guid)
{
try
{
if (Chainloader.PluginInfos != null && Chainloader.PluginInfos.TryGetValue(guid, out var value))
{
object result;
if (value == null)
{
result = null;
}
else
{
BepInPlugin metadata = value.Metadata;
result = ((metadata == null) ? null : metadata.Version?.ToString());
}
return (string)result;
}
}
catch
{
}
return null;
}
public static string Summary()
{
string text = VersionOf("com.nitrogenia.morestrafts");
string text2 = VersionOf("com.morestrafts.uispawn.addon");
return "moreStrafts=" + (text ?? "absent") + ", UISpawnAddon=" + (text2 ?? "absent");
}
}
internal static class StaticAccess
{
public static Func<object> Getter(Type owner, string name)
{
if (owner == null || string.IsNullOrEmpty(name))
{
return null;
}
Type type = owner;
while (type != null)
{
FieldInfo field = type.GetField(name, BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.FlattenHierarchy);
if (field != null && field.IsStatic)
{
return () => field.GetValue(null);
}
PropertyInfo property = type.GetProperty(name, BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.FlattenHierarchy);
MethodInfo methodInfo = property?.GetGetMethod(nonPublic: true);
if (methodInfo != null && methodInfo.IsStatic)
{
return () => property.GetValue(null);
}
type = type.BaseType;
}
return null;
}
public static object Read(Type owner, string name)
{
return Getter(owner, name)?.Invoke();
}
}
}
namespace StraftShuffle
{
internal static class GameAccess
{
private static bool _resolved;
private static bool _usable;
private static FieldInfo _currentSpawnPoints;
private static FieldInfo _clientScript;
private static FieldInfo _playerId;
private static FieldInfo _playerInstances;
private static Func<object> _scoreInstance;
private static FieldInfo _takeIndex;
private static MethodInfo _setActiveSpawnPoints;
private static MethodInfo _spawnAt;
public static bool Usable
{
get
{
if (!_resolved)
{
Resolve();
}
return _usable;
}
}
private static void Resolve()
{
_resolved = true;
try
{
Type type = AccessTools.TypeByName("PlayerManager");
Type type2 = AccessTools.TypeByName("ClientInstance");
Type type3 = AccessTools.TypeByName("ScoreManager");
if (type == null || type2 == null || type3 == null)
{
Log.Warn("could not find PlayerManager/ClientInstance/ScoreManager; standing down.");
return;
}
_currentSpawnPoints = AccessTools.Field(type, "CurrentSpawnPoints");
_clientScript = AccessTools.Field(type, "ClientScript");
_playerId = AccessTools.Field(type2, "PlayerId");
_playerInstances = AccessTools.Field(type2, "playerInstances");
_scoreInstance = StaticAccess.Getter(type3, "Instance");
_takeIndex = AccessTools.Field(type3, "TakeIndex");
_setActiveSpawnPoints = AccessTools.Method(type, "SetActiveSpawnPoints", (Type[])null, (Type[])null);
_spawnAt = AccessTools.Method(type, "SpawnPlayer", new Type[4]
{
typeof(int),
typeof(int),
typeof(Vector3),
typeof(Quaternion)
}, (Type[])null);
_usable = _currentSpawnPoints != null && _clientScript != null && _playerId != null && _playerInstances != null && _scoreInstance != null && _takeIndex != null && _setActiveSpawnPoints != null && _spawnAt != null;
if (!_usable)
{
Log.Warn("game layout changed (" + Missing() + "); standing down, spawns stay vanilla.");
}
else
{
Log.Info("game members resolved.");
}
}
catch (Exception arg)
{
Log.Error($"resolving game members failed: {arg}");
_usable = false;
}
}
public static string MissingMembers()
{
if (!_resolved)
{
Resolve();
}
return Missing();
}
private static string Missing()
{
List<string> list = new List<string>();
if (_currentSpawnPoints == null)
{
list.Add("PlayerManager.CurrentSpawnPoints");
}
if (_clientScript == null)
{
list.Add("PlayerManager.ClientScript");
}
if (_playerId == null)
{
list.Add("ClientInstance.PlayerId");
}
if (_playerInstances == null)
{
list.Add("ClientInstance.playerInstances");
}
if (_scoreInstance == null)
{
list.Add("ScoreManager.Instance");
}
if (_takeIndex == null)
{
list.Add("ScoreManager.TakeIndex");
}
if (_setActiveSpawnPoints == null)
{
list.Add("PlayerManager.SetActiveSpawnPoints");
}
if (_spawnAt == null)
{
list.Add("PlayerManager.SpawnPlayer(int,int,Vector3,Quaternion)");
}
return string.Join(", ", list);
}
public static void RefreshSpawnPoints(object playerManager)
{
_setActiveSpawnPoints.Invoke(playerManager, null);
}
public static IReadOnlyList<Transform> SpawnPoints(object playerManager)
{
if (!(_currentSpawnPoints.GetValue(playerManager) is Array array))
{
return Array.Empty<Transform>();
}
List<Transform> list = new List<Transform>(array.Length);
foreach (object item in array)
{
object obj = ((item is Component) ? item : null);
list.Add((obj != null) ? ((Component)obj).transform : null);
}
return list;
}
public static int PlayerIdOf(object playerManager)
{
object value = _clientScript.GetValue(playerManager);
return (value == null) ? (-1) : ((int)_playerId.GetValue(value));
}
public static object AnyPlayerManager()
{
Type type = AccessTools.TypeByName("PlayerManager");
return (type == null) ? null : Object.FindObjectOfType(type);
}
public static List<int> ConnectedPlayerIds()
{
List<int> list = new List<int>();
if (_playerInstances.GetValue(null) is IDictionary dictionary)
{
foreach (object key in dictionary.Keys)
{
list.Add((int)key);
}
}
return list;
}
public static int RoundIndex()
{
object obj = _scoreInstance();
return (obj != null) ? ((int)_takeIndex.GetValue(obj)) : 0;
}
public static void SpawnAt(object playerManager, int suitIndex, int cigIndex, Vector3 position, Quaternion rotation)
{
//IL_0020: Unknown result type (might be due to invalid IL or missing references)
//IL_0029: Unknown result type (might be due to invalid IL or missing references)
_spawnAt.Invoke(playerManager, new object[4] { suitIndex, cigIndex, position, rotation });
}
}
internal static class Log
{
private static ManualLogSource _source;
public static void Init(ManualLogSource source)
{
_source = source;
}
public static void Info(string message)
{
ManualLogSource source = _source;
if (source != null)
{
source.LogInfo((object)message);
}
}
public static void Warn(string message)
{
ManualLogSource source = _source;
if (source != null)
{
source.LogWarning((object)message);
}
}
public static void Error(string message)
{
ManualLogSource source = _source;
if (source != null)
{
source.LogError((object)message);
}
}
}
[BepInPlugin("straftshuffle", "Straft Shuffle", "0.1.0")]
[BepInDependency(/*Could not decode attribute arguments.*/)]
[BepInDependency(/*Could not decode attribute arguments.*/)]
public class Plugin : BaseUnityPlugin
{
public const string Guid = "straftshuffle";
public const string Name = "Straft Shuffle";
public const string Version = "0.1.0";
private int _lastTickFrame = -1;
internal static StraftShuffleConfig Settings { get; private set; }
internal static bool ShouldOffsetSharedPoints { get; private set; } = true;
internal static int MatchSeed { get; private set; } = NewMatchSeed();
internal static int EffectiveSalt
{
get
{
int valueOrDefault = (Settings?.Salt?.Value).GetValueOrDefault();
StraftShuffleConfig settings = Settings;
return valueOrDefault ^ ((settings == null || settings.RandomizeEachMatch?.Value != false) ? MatchSeed : 0);
}
}
private static int NewMatchSeed()
{
long ticks = DateTime.UtcNow.Ticks;
return (int)(ticks ^ (ticks >> 32) ^ (Environment.TickCount * 2654435761u));
}
private void Awake()
{
//IL_002e: Unknown result type (might be due to invalid IL or missing references)
//IL_004a: Unknown result type (might be due to invalid IL or missing references)
//IL_0054: Expected O, but got Unknown
Log.Init(((BaseUnityPlugin)this).Logger);
Settings = new StraftShuffleConfig(((BaseUnityPlugin)this).Config);
ShouldOffsetSharedPoints = ResolveOffsetBehaviour();
new Harmony("straftshuffle").PatchAll(typeof(RoundSpawnPatch));
Application.onBeforeRender += new UnityAction(OnBeforeRender);
SceneManager.sceneLoaded += OnSceneLoaded;
Log.Info("detected mods: " + ModPresence.Summary());
Log.Info(ShouldOffsetSharedPoints ? "separating players who share a spawn point ourselves." : "leaving shared-point separation to MoreStrafts_UISpawnAddon.");
Log.Info(Settings.Enabled.Value ? ("Straft Shuffle v0.1.0 loaded — spawns re-dealt each round " + $"(min {Settings.MinimumPlayers.Value} players, " + (Settings.RandomizeEachMatch.Value ? "randomised per map" : $"fixed salt {Settings.Salt.Value}") + ").") : "Straft Shuffle v0.1.0 loaded — disabled in config, spawns stay vanilla.");
}
private static bool ResolveOffsetBehaviour()
{
return Settings.OffsetMode.Value switch
{
OffsetBehaviour.Always => true,
OffsetBehaviour.Never => false,
_ => !ModPresence.UiSpawnAddon,
};
}
private void OnSceneLoaded(Scene scene, LoadSceneMode mode)
{
try
{
if (Settings != null && Settings.RandomizeEachMatch.Value && !(((Scene)(ref scene)).name == "MainMenu"))
{
MatchSeed = NewMatchSeed();
Log.Info($"map '{((Scene)(ref scene)).name}' loaded; new spawn arrangement (match seed {MatchSeed}).");
}
}
catch (Exception arg)
{
Log.Error($"scene-load hook failed: {arg}");
}
}
private void Update()
{
Tick();
}
private void OnBeforeRender()
{
Tick();
}
private void Tick()
{
//IL_0038: Unknown result type (might be due to invalid IL or missing references)
//IL_003d: Unknown result type (might be due to invalid IL or missing references)
try
{
if (_lastTickFrame == Time.frameCount)
{
return;
}
_lastTickFrame = Time.frameCount;
if (Settings != null)
{
KeyboardShortcut value = Settings.ReportKey.Value;
if (((KeyboardShortcut)(ref value)).IsDown() || Input.GetKeyDown((KeyCode)291))
{
Report();
}
}
}
catch (Exception arg)
{
Log.Error($"tick failed: {arg}");
}
}
private static void Report()
{
try
{
Log.Info("---- spawn shuffle report ----");
Log.Info($"enabled : {Settings.Enabled.Value}");
Log.Info("detected mods : " + ModPresence.Summary());
Log.Info($"we offset shared points : {ShouldOffsetSharedPoints}" + (ShouldOffsetSharedPoints ? "" : " (left to UISpawnAddon)"));
Log.Info($"spawns re-dealt so far : {RoundSpawnPatch.AppliedCount}");
Log.Info("last spawn outcome : " + RoundSpawnPatch.LastOutcome);
if (!GameAccess.Usable)
{
Log.Warn("NOT running. Could not find: " + GameAccess.MissingMembers());
Log.Info("------------------------------");
return;
}
List<int> list = GameAccess.ConnectedPlayerIds();
int num = GameAccess.RoundIndex();
Log.Info($"round index : {num}");
Log.Info($"match seed : {MatchSeed}" + (Settings.RandomizeEachMatch.Value ? " (new each map)" : " (randomisation off)"));
Log.Info(string.Format("players present : {0} [{1}]", list.Count, string.Join(",", list)));
Log.Info($"minimum to act : {Settings.MinimumPlayers.Value}");
object obj = GameAccess.AnyPlayerManager();
if (obj == null)
{
Log.Info("spawn points : no PlayerManager yet (start a match)");
Log.Info("------------------------------");
return;
}
GameAccess.RefreshSpawnPoints(obj);
IReadOnlyList<Transform> readOnlyList = GameAccess.SpawnPoints(obj);
Log.Info($"spawn points on this map: {readOnlyList.Count}");
if (readOnlyList.Count == 0)
{
Log.Info("------------------------------");
return;
}
int num2 = Math.Max(list.Count, Settings.MinimumPlayers.Value);
List<int> list2 = new List<int>();
for (int i = 0; i < num2; i++)
{
list2.Add(i);
}
Log.Info($"dry run for {num2} players (round {num}):");
for (int j = 0; j < num2; j++)
{
SpawnSlot spawnSlot = SpawnAssignment.Assign(j, list2, readOnlyList.Count, num, EffectiveSalt);
Log.Info($" player {j} -> point {spawnSlot.SpawnPointIndex}" + ((spawnSlot.Occupants > 1) ? $" (shared by {spawnSlot.Occupants}, ring {spawnSlot.Ring})" : " (alone)"));
}
Log.Info("------------------------------");
}
catch (Exception arg)
{
Log.Error($"report failed: {arg}");
}
}
}
[HarmonyPatch]
internal static class RoundSpawnPatch
{
private static bool _loggedFailure;
internal static int AppliedCount { get; private set; }
internal static string LastOutcome { get; private set; } = "no spawn seen yet";
private static MethodBase TargetMethod()
{
return AccessTools.Method(AccessTools.TypeByName("PlayerManager"), "SpawnPlayer", new Type[2]
{
typeof(int),
typeof(int)
}, (Type[])null);
}
private static bool Skip(string reason)
{
LastOutcome = reason;
return true;
}
private static bool Prefix(object __instance, int suitIndex, int cigIndex)
{
//IL_018f: Unknown result type (might be due to invalid IL or missing references)
//IL_0194: Unknown result type (might be due to invalid IL or missing references)
//IL_01fc: Unknown result type (might be due to invalid IL or missing references)
//IL_020b: Unknown result type (might be due to invalid IL or missing references)
//IL_0210: Unknown result type (might be due to invalid IL or missing references)
//IL_0217: Unknown result type (might be due to invalid IL or missing references)
//IL_0219: Unknown result type (might be due to invalid IL or missing references)
//IL_01cb: Unknown result type (might be due to invalid IL or missing references)
//IL_01e8: Unknown result type (might be due to invalid IL or missing references)
//IL_01ed: Unknown result type (might be due to invalid IL or missing references)
//IL_01f2: Unknown result type (might be due to invalid IL or missing references)
StraftShuffleConfig settings = Plugin.Settings;
if (settings == null || !settings.Enabled.Value)
{
return Skip("disabled in config");
}
bool flag = false;
try
{
if (!GameAccess.Usable)
{
return Skip("game members unresolved (" + GameAccess.MissingMembers() + ")");
}
List<int> list = GameAccess.ConnectedPlayerIds();
if (list.Count < settings.MinimumPlayers.Value)
{
return Skip($"{list.Count} player(s), minimum is {settings.MinimumPlayers.Value}");
}
if (!settings.OverrideTeamModes.Value && TeamModeActive())
{
return Skip("team mode, and OverrideTeamModes is off");
}
int num = GameAccess.PlayerIdOf(__instance);
if (num < 0)
{
return Skip("player id unknown");
}
GameAccess.RefreshSpawnPoints(__instance);
IReadOnlyList<Transform> readOnlyList = GameAccess.SpawnPoints(__instance);
if (readOnlyList.Count == 0)
{
return Skip("the map reports no spawn points");
}
int num2 = GameAccess.RoundIndex();
SpawnSlot slot = SpawnAssignment.Assign(num, list, readOnlyList.Count, num2, Plugin.EffectiveSalt);
Transform val = readOnlyList[slot.SpawnPointIndex];
if ((Object)(object)val == (Object)null)
{
return Skip($"spawn point {slot.SpawnPointIndex} is null");
}
Vector3 val2 = val.position;
if (SpawnAssignment.NeedsOffset(slot) && Plugin.ShouldOffsetSharedPoints)
{
double num3 = SpawnAssignment.AngleFor(slot, num2, Plugin.EffectiveSalt);
float value = settings.ClusterRadius.Value;
val2 += new Vector3((float)Math.Cos(num3) * value, 0f, (float)Math.Sin(num3) * value);
}
Quaternion rotation = Quaternion.Euler(0f, val.eulerAngles.y, 0f);
flag = true;
GameAccess.SpawnAt(__instance, suitIndex, cigIndex, val2, rotation);
AppliedCount++;
LastOutcome = $"round {num2}: player {num} -> point {slot.SpawnPointIndex}" + ((slot.Occupants > 1) ? $" (sharing with {slot.Occupants - 1}, ring {slot.Ring})" : " (alone)");
Log.Info(LastOutcome);
return false;
}
catch (Exception ex)
{
if (!_loggedFailure)
{
_loggedFailure = true;
Log.Error($"spawn assignment failed: {ex}");
}
if (flag)
{
LastOutcome = "threw after spawning (" + ex.GetType().Name + "); suppressed vanilla to avoid a double spawn";
return false;
}
return Skip("threw before spawning: " + ex.GetType().Name);
}
}
private static bool TeamModeActive()
{
try
{
Type type = AccessTools.TypeByName("GameManager");
object obj = StaticAccess.Read(type, "Instance");
if (obj == null)
{
return false;
}
FieldInfo fieldInfo = AccessTools.Field(type, "playingTeams");
return fieldInfo != null && (bool)fieldInfo.GetValue(obj);
}
catch
{
return true;
}
}
}
public readonly struct SpawnSlot
{
public readonly int SpawnPointIndex;
public readonly int Ring;
public readonly int Occupants;
public SpawnSlot(int spawnPointIndex, int ring, int occupants)
{
SpawnPointIndex = spawnPointIndex;
Ring = ring;
Occupants = occupants;
}
}
public static class SpawnAssignment
{
public static SpawnSlot Assign(int playerId, IReadOnlyList<int> playerIds, int spawnPointCount, int roundIndex, int salt)
{
if (spawnPointCount <= 0)
{
return new SpawnSlot(0, 0, 1);
}
List<int> list = Sorted(playerIds);
int count = list.Count;
int num = list.IndexOf(playerId);
if (num < 0 || count == 0)
{
return new SpawnSlot(Mod(roundIndex + playerId, spawnPointCount), 0, 1);
}
int num2 = Deal(num, count, Seed(roundIndex, salt));
int num3 = num2 % spawnPointCount;
int spawnPointIndex = Permutation(spawnPointCount, Seed(roundIndex, salt ^ 0x2545F491))[num3];
return new SpawnSlot(spawnPointIndex, num2 / spawnPointCount, OccupantsOf(num3, count, spawnPointCount));
}
public static double AngleFor(SpawnSlot slot, int roundIndex, int salt)
{
if (slot.Occupants <= 1)
{
return 0.0;
}
double num = (double)(Seed(roundIndex, salt ^ 0x5F37) & 0xFFFF) / 65536.0;
return Math.PI * 2.0 * (((double)slot.Ring + num) / (double)slot.Occupants);
}
public static bool NeedsOffset(SpawnSlot slot)
{
return slot.Occupants > 1;
}
private static int Deal(int index, int seatCount, uint seed)
{
return Permutation(seatCount, seed)[index];
}
private static int[] Permutation(int count, uint seed)
{
int[] array = new int[count];
for (int i = 0; i < count; i++)
{
array[i] = i;
}
uint state = seed;
for (int num = count - 1; num > 0; num--)
{
int num2 = (int)NextBelow(ref state, (uint)(num + 1));
ref int reference = ref array[num];
ref int reference2 = ref array[num2];
int num3 = array[num2];
int num4 = array[num];
reference = num3;
reference2 = num4;
}
return array;
}
private static int OccupantsOf(int spawnPointIndex, int seatCount, int spawnPointCount)
{
if (spawnPointIndex >= seatCount)
{
return 0;
}
return (seatCount - 1 - spawnPointIndex) / spawnPointCount + 1;
}
private static List<int> Sorted(IReadOnlyList<int> playerIds)
{
List<int> list = new List<int>(playerIds?.Count ?? 0);
if (playerIds != null)
{
list.AddRange(playerIds);
}
list.Sort();
return list;
}
private static uint Seed(int roundIndex, int salt)
{
uint num = (uint)((roundIndex * -1640531527) ^ (salt * -2048144789));
num ^= num >> 16;
num *= 2146121005;
num ^= num >> 15;
num *= 2221713035u;
num ^= num >> 16;
return (num == 0) ? 2654435769u : num;
}
private static uint NextBelow(ref uint state, uint bound)
{
if (bound <= 1)
{
return 0u;
}
uint num = uint.MaxValue - uint.MaxValue % bound;
uint num2;
do
{
num2 = Next(ref state);
}
while (num2 >= num);
return num2 % bound;
}
private static uint Next(ref uint state)
{
state ^= state << 13;
state ^= state >> 17;
state ^= state << 5;
return state;
}
private static int Mod(int value, int modulus)
{
int num = value % modulus;
return (num < 0) ? (num + modulus) : num;
}
}
internal enum OffsetBehaviour
{
Auto,
Always,
Never
}
internal sealed class StraftShuffleConfig
{
public readonly ConfigEntry<bool> Enabled;
public readonly ConfigEntry<int> MinimumPlayers;
public readonly ConfigEntry<bool> OverrideTeamModes;
public readonly ConfigEntry<float> ClusterRadius;
public readonly ConfigEntry<OffsetBehaviour> OffsetMode;
public readonly ConfigEntry<int> Salt;
public readonly ConfigEntry<bool> RandomizeEachMatch;
public readonly ConfigEntry<KeyboardShortcut> ReportKey;
public StraftShuffleConfig(ConfigFile config)
{
//IL_0043: Unknown result type (might be due to invalid IL or missing references)
//IL_004d: Expected O, but got Unknown
//IL_00b4: Unknown result type (might be due to invalid IL or missing references)
//IL_00be: Expected O, but got Unknown
//IL_00d9: Unknown result type (might be due to invalid IL or missing references)
Enabled = config.Bind<bool>("Shuffle", "Enabled", true, "Re-deal spawn points every round so you don't start next to the same players all match. Turn off to restore vanilla spawn order.");
MinimumPlayers = config.Bind<int>("Shuffle", "MinimumPlayers", 3, new ConfigDescription("Leave matches smaller than this alone. Vanilla 1v1 spawn handling is already correct (the two players are simply placed apart), so the default skips it.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(2, 10), Array.Empty<object>()));
OverrideTeamModes = config.Bind<bool>("Shuffle", "OverrideTeamModes", false, "Also re-deal in team modes. Off by default: the game has hand-tuned 2v2 spawn placement that keeps teammates together, and shuffling would break that on purpose-built maps.");
OffsetMode = config.Bind<OffsetBehaviour>("Shuffle", "SharedPointOffsets", OffsetBehaviour.Auto, "How players sharing one spawn point get separated.\nAuto: leave it to MoreStrafts_UISpawnAddon when that is installed, since it already offsets every spawn, and do it ourselves otherwise. This is the one you want.\nAlways: always apply our own offset. Stacks with the addon's, so players end up further apart than ClusterRadius suggests.\nNever: never offset. Players sharing a point spawn on top of each other unless something else separates them.");
ClusterRadius = config.Bind<float>("Shuffle", "ClusterRadius", 0.6f, new ConfigDescription("Metres apart when several players share one spawn point, which happens above 4 players because maps only ship 1v1 and 4-player spawn sets. Too small and players spawn inside each other; too large and they land in walls.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 3f), Array.Empty<object>()));
ReportKey = config.Bind<KeyboardShortcut>("Hotkeys", "Report", new KeyboardShortcut((KeyCode)291, Array.Empty<KeyCode>()), "Log what this plugin is doing, including the deal it would make right now. Works in a one-player lobby, where the shuffle itself does not run.");
RandomizeEachMatch = config.Bind<bool>("Shuffle", "RandomizeEachMatch", true, "Pick a fresh arrangement for every map. Without this the shuffle is keyed only to the round number, and since that resets at the start of each match, round 1 deals exactly the same spawns every time. Turn it off only if you want reproducible results for testing.");
Salt = config.Bind<int>("Shuffle", "Salt", 0, "Shifts the whole sequence of deals. With RandomizeEachMatch off, the same salt and round always produce the same arrangement, which makes bugs reproducible.");
}
}
}