Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions src/Extensions/ListExt.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
using RemoveMultiplayerPlayerLimit.src;
using System.Collections.Generic;
using System.IO;

namespace RemoveMultiplayerPlayetLimit.src.Extensions
{
public static class ListExt
{
public static bool TryGetNext<T>(this List<T> list, T value, out T next)
{
return TryGetAfter(list, value, 1, out next);
}

public static bool TryGetAfter<T>(this List<T> list, T value, int num, out T after)
{
after = default;

var index = list.IndexOf(value);

if (index != -1 && list.Count > index + num)
{
after = list[index + num];

return true;
}

return false;
}

public static bool TryGetLast<T>(this List<T> list, T value, out T last)
{
return TryGetBefore(list, value, 1, out last);
}

public static bool TryGetBefore<T>(this List<T> list, T value, int num, out T before)
{
before = default;

var index = list.IndexOf(value);

if (index != -1 && index - num >= 0)
{
before = list[index - num];

return true;
}

return false;
}
}
}
195 changes: 82 additions & 113 deletions src/ModEntry.cs
Original file line number Diff line number Diff line change
@@ -1,186 +1,155 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text.Json;
using Godot;
using HarmonyLib;
using MegaCrit.Sts2.Core.Context;
using MegaCrit.Sts2.Core.Entities.RestSite;
using MegaCrit.Sts2.Core.Logging;
using MegaCrit.Sts2.Core.Modding;
using MegaCrit.Sts2.Core.Nodes.Rooms;
using MegaCrit.Sts2.Core.Nodes.RestSite;
using MegaCrit.Sts2.Core.Nodes.Rooms;
using MegaCrit.Sts2.Core.Runs;
using RemoveMultiplayerPlayerLimit.src;
using RemoveMultiplayerPlayetLimit.src;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text.Json;

namespace RemoveMultiplayerPlayerLimit;

[ModInitializer("Initialize")]
public static partial class ModEntry
{
private const int DefaultPlayerLimit = 8;
public static Option Option { get; set; }

private const int MinSupportedPlayerLimit = 4;
public static Harmony Harmony { get; set; } = new("Rain156.RemoveMultiplayerPlayerLimit");

private const int MaxSupportedPlayerLimit = 16;
internal const int DefaultPlayerLimit = 8;

private const int VanillaSlotIdBits = 2;
internal const int MinSupportedPlayerLimit = 4;

private const int VanillaLobbyListLengthBits = 3;
internal const int MaxSupportedPlayerLimit = 16;

private const string ModFolderName = "RemoveMultiplayerPlayerLimit";
internal const int VanillaSlotIdBits = 2;

private const string ConfigFileName = "config.json";
internal const int VanillaLobbyListLengthBits = 3;

private static int TargetPlayerLimit { get; set; } = DefaultPlayerLimit;
internal const string ModFolderName = "RemoveMultiplayerPlayerLimit";

private static int SlotIdBits { get; set; } = RequiredBitsForExclusiveUpperBound(DefaultPlayerLimit);
internal const string ConfigFileName = "config.json";

private static int LobbyListLengthBits { get; set; } = RequiredBitsForExclusiveUpperBound(DefaultPlayerLimit + 1);
private static int SlotIdBits { get; set; }

private static int SlotIdCapacity { get; set; } = 1 << RequiredBitsForExclusiveUpperBound(DefaultPlayerLimit);
private static int LobbyListLengthBits { get; set; }

private static int LobbyListLengthCapacity { get; set; } = 1 << RequiredBitsForExclusiveUpperBound(DefaultPlayerLimit + 1);
private static int SlotIdCapacity { get; set; }

private static readonly FieldInfo? MaxPlayersField = AccessTools.Field(typeof(MegaCrit.Sts2.Core.Multiplayer.Game.Lobby.StartRunLobby), "<MaxPlayers>k__BackingField");
private static int LobbyListLengthCapacity { get; set; }

public static void Initialize()
{
TargetPlayerLimit = LoadOrCreatePlayerLimit();
SlotIdBits = RequiredBitsForExclusiveUpperBound(TargetPlayerLimit);
LobbyListLengthBits = RequiredBitsForExclusiveUpperBound(TargetPlayerLimit + 1);
SlotIdCapacity = 1 << SlotIdBits;
LobbyListLengthCapacity = 1 << LobbyListLengthBits;
if (TargetPlayerLimit > SlotIdCapacity)
try
{
throw new InvalidOperationException($"TargetPlayerLimit {TargetPlayerLimit} exceeds slot id capacity {SlotIdCapacity}.");
}
if (TargetPlayerLimit > LobbyListLengthCapacity)
LoadOptions();

SlotIdBits = RequiredBitsForExclusiveUpperBound(Option.PlayerLimit);

LobbyListLengthBits = RequiredBitsForExclusiveUpperBound(Option.PlayerLimit + 1);

SlotIdCapacity = 1 << SlotIdBits;

LobbyListLengthCapacity = 1 << LobbyListLengthBits;

Harmony.PatchAll();

Log.Info($"RemoveMultiplayerPlayerLimit loaded. Target limit: {Option.PlayerLimit}, slot capacity: {SlotIdCapacity}, lobby list capacity: {LobbyListLengthCapacity}");
}
catch (Exception e)
{
throw new InvalidOperationException($"TargetPlayerLimit {TargetPlayerLimit} exceeds lobby list capacity {LobbyListLengthCapacity}.");
}
new Harmony("cn.remove.multiplayer.playerlimit").PatchAll();
Log.Info($"RemoveMultiplayerPlayerLimit loaded. Target limit: {TargetPlayerLimit}, slot capacity: {SlotIdCapacity}, lobby list capacity: {LobbyListLengthCapacity}");
File.AppendAllText(Path.Combine(Pathes.RootPath, "logs.txt"), e.Message + e.StackTrace);
}
}

private static int LoadOrCreatePlayerLimit()
private static void LoadOptions()
{
string modDirectory = ResolveModDirectory();
Directory.CreateDirectory(modDirectory);
string configPath = Path.Combine(modDirectory, ConfigFileName);
string configPath = Pathes.ConfigPath;

if (!File.Exists(configPath))
{
WriteDefaultConfig(configPath, DefaultPlayerLimit);
return DefaultPlayerLimit;
}

try
{
using JsonDocument jsonDocument = JsonDocument.Parse(File.ReadAllText(configPath));
if (jsonDocument.RootElement.TryGetProperty("max_player_limit", out JsonElement value) && value.ValueKind == JsonValueKind.Number && value.TryGetInt32(out int rawLimit))
Option = JsonSerializer.Deserialize<Option>(File.ReadAllText(configPath));

var clampedLimit = Math.Clamp(Option.PlayerLimit, MinSupportedPlayerLimit, MaxSupportedPlayerLimit);

if (clampedLimit != Option.PlayerLimit)
{
int clampedLimit = Math.Clamp(rawLimit, MinSupportedPlayerLimit, MaxSupportedPlayerLimit);
if (clampedLimit != rawLimit)
{
WriteDefaultConfig(configPath, clampedLimit);
}
return clampedLimit;
}
WriteDefaultConfig(configPath, DefaultPlayerLimit);
return DefaultPlayerLimit;
Option.PlayerLimit = clampedLimit;

WriteDefaultConfig(configPath, clampedLimit);
}
}
catch (Exception ex)
{
Log.Warn($"Failed to parse config at {configPath}: {ex.Message}");

BackupCorruptedConfig(configPath);
}
WriteDefaultConfig(configPath, DefaultPlayerLimit);
return DefaultPlayerLimit;
}

private static string ResolveModDirectory()
{
string? assemblyLocation = Assembly.GetExecutingAssembly().Location;
string? assemblyDirectory = string.IsNullOrWhiteSpace(assemblyLocation) ? null : Path.GetDirectoryName(assemblyLocation);
if (!string.IsNullOrWhiteSpace(assemblyDirectory) && Directory.Exists(assemblyDirectory))
{
return assemblyDirectory;
}
string fallbackModDirectory = Path.Combine(AppContext.BaseDirectory, "mods", ModFolderName);
if (Directory.Exists(fallbackModDirectory))
{
return fallbackModDirectory;
}
return AppContext.BaseDirectory;
}
private static JsonSerializerOptions _defaultOption = new() { WriteIndented = true };

private static void WriteDefaultConfig(string configPath, int playerLimit)
{
// min_supported / max_supported are informational fields for users and are not parsed.
string contents = JsonSerializer.Serialize(new Dictionary<string, int>
{
["max_player_limit"] = playerLimit,
["min_supported"] = MinSupportedPlayerLimit,
["max_supported"] = MaxSupportedPlayerLimit
}, new JsonSerializerOptions
// min_player / max_player are informational fields for users and are not parsed.
string contents = JsonSerializer.Serialize(new Dictionary<string, int>
{
WriteIndented = true
});
["player_limit"] = playerLimit,
["min_player"] = MinSupportedPlayerLimit,
["max_player"] = MaxSupportedPlayerLimit
}, _defaultOption);

File.WriteAllText(configPath, contents);
}

private static void BackupCorruptedConfig(string configPath)
{
if (!File.Exists(configPath))
{
return;
}

string backupPath = $"{configPath}.bak";

if (File.Exists(backupPath))
{
backupPath = $"{configPath}.{DateTime.Now:yyyyMMddHHmmss}.bak";
}
File.Move(configPath, backupPath);
}

private static int RequiredBitsForExclusiveUpperBound(int upperBound)
{
int normalizedBound = Math.Max(1, upperBound);
int bitCount = 0;
int capacity = 1;
while (capacity < normalizedBound)
{
bitCount++;
capacity <<= 1;
}
return Math.Max(1, bitCount);
File.Move(configPath, backupPath);
}

private static int EnsureMin(int value, int min) => Math.Max(value, min);
private static int RequiredBitsForExclusiveUpperBound(int upperBound) => Mathf.CeilToInt(Math.Log2(upperBound));

private static bool TryGetCharacter(NRestSiteRoom room, ulong playerId, out NRestSiteCharacter character)
private static bool TryGetCharacter(NRestSiteRoom room, ulong playerId, out NRestSiteCharacter character)
{
NRestSiteCharacter? nRestSiteCharacter = room.Characters.FirstOrDefault((NRestSiteCharacter c) => c.Player.NetId == playerId);
if (nRestSiteCharacter == null)
{
character = null!;
return false;
}
character = nRestSiteCharacter;
return true;
character = room.Characters.FirstOrDefault(c => c.Player.NetId == playerId, null);

return character != null;
}

private static RestSiteOption? TryGetHoveredOption(ulong playerId)
private static RestSiteOption GetHoveredOption(ulong playerId)
{
int? hoveredOptionIndex = MegaCrit.Sts2.Core.Runs.RunManager.Instance.RestSiteSynchronizer.GetHoveredOptionIndex(playerId);
if (!hoveredOptionIndex.HasValue)
{
var hoveredOptionIndex = RunManager.Instance.RestSiteSynchronizer.GetHoveredOptionIndex(playerId);

if (!hoveredOptionIndex.HasValue)
return null;
}
IReadOnlyList<RestSiteOption> optionsForPlayer = MegaCrit.Sts2.Core.Runs.RunManager.Instance.RestSiteSynchronizer.GetOptionsForPlayer(playerId);

var optionsForPlayer = RunManager.Instance.RestSiteSynchronizer.GetOptionsForPlayer(playerId);

int value = hoveredOptionIndex.Value;
if ((uint)value >= (uint)optionsForPlayer.Count)
{

if (value >= optionsForPlayer.Count)
return null;
}

return optionsForPlayer[value];
}

Expand Down
23 changes: 23 additions & 0 deletions src/Option.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using RemoveMultiplayerPlayerLimit;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Text.Json.Serialization;
using System.Threading.Tasks;

namespace RemoveMultiplayerPlayetLimit.src
{
public class Option
{
[JsonPropertyName("player_limit")]
public int PlayerLimit { get; set; } = ModEntry.DefaultPlayerLimit;

[JsonPropertyName("min_player")]
public int MinPlayerLimit { get; set; } = ModEntry.MinSupportedPlayerLimit;

[JsonPropertyName("max_player")]
public int MaxPlayerLimit { get; set; } = ModEntry.MaxSupportedPlayerLimit;
}
}
11 changes: 9 additions & 2 deletions src/Patches.Merchant.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,21 +31,28 @@ private static void Postfix(NMerchantRoom __instance)
private static void RepositionMerchantVisuals(IReadOnlyList<NMerchantCharacter> visuals)
{
if (visuals.Count <= VanillaMultiplayerHolderCount)
{
return;
}

int rowCount = visuals.Count <= VanillaMultiplayerHolderCount * 2 ? 2 : Mathf.CeilToInt((float)visuals.Count / VanillaMultiplayerHolderCount);

int columnCount = Mathf.CeilToInt((float)visuals.Count / rowCount);

int visualIndex = 0;

for (int row = 0; row < rowCount; row++)
{
float x = MerchantForwardShiftX + MerchantRowStartOffsetX * row;

float y = MerchantForwardShiftY + MerchantRowStepY * row;

for (int column = 0; column < columnCount && visualIndex < visuals.Count; column++)
{
NMerchantCharacter nMerchantCharacter = visuals[visualIndex];

nMerchantCharacter.Position = new Vector2(x, y);

x += MerchantColumnStepX;

visualIndex++;
}
}
Expand Down
Loading