Decompiled source of CatLibTests v0.6.2
plugins/CatLib.Tests.dll
Decompiled a day ago
The result has been truncated due to the large size, download it to view full contents!
using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; using System.IO.Compression; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Versioning; using System.Security; using System.Security.Permissions; using System.Text; using System.Threading; using System.Threading.Tasks; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using BepInEx.Unity.IL2CPP; using BetterRepair; using BetterRepair.Logic; using BetterRepair.Settings; using BoatTweaks; using BoatTweaks.Logic; using BoatTweaks.Settings; using CatLib.Config; using CatLib.Core; using CatLib.DevTools; using CatLib.Diagnostics; using CatLib.Events; using CatLib.Game; using CatLib.Game.Bridge; using CatLib.Game.Events; using CatLib.Il2Cpp; using CatLib.Localization; using CatLib.Logging; using CatLib.Net; using CatLib.Saves; using CatLib.Tests.Diagnostics; using CatLib.Tests.Diagnostics.Inspection; using CatLib.Tests.Framework; using CatLib.Tests.Suites.Localization; using CatLib.Tests.Suites.Network; using CatLib.Tests.Suites.Presentation; using CatLib.Tests.Suites.Saves; using CatLib.Tests.Suites.Settings; using CatLib.Tests.Timeline; using CatLib.Threading; using CatLib.UI; using HarmonyLib; using I2.Loc; using Il2CppInterop.Runtime; using Il2CppInterop.Runtime.InteropTypes; using Il2CppInterop.Runtime.InteropTypes.Arrays; using Il2CppSystem; using Il2CppSystem.Collections.Generic; using Il2CppSystem.Reflection; using Microsoft.CodeAnalysis; using SemanticVersioning; using ShelfLabels; using ShelfLabels.Data; using ShelfLabels.Logic; using ShelfLabels.Settings; using ShelfLabels.Sync; using Steamworks; using TMPro; using UnityEngine; using UnityEngine.Diagnostics; using UnityEngine.EventSystems; using UnityEngine.Events; using UnityEngine.SceneManagement; using UnityEngine.UI; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETCoreApp,Version=v6.0", FrameworkDisplayName = ".NET 6.0")] [assembly: AssemblyCompany("CatLib contributors")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("0.6.2.0")] [assembly: AssemblyInformationalVersion("0.6.2+4162013e7c932bfa1cf0340558d83186f1fd5817")] [assembly: AssemblyProduct("CatLib")] [assembly: AssemblyTitle("CatLib.Tests")] [assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)] [assembly: AssemblyVersion("0.6.2.0")] [module: UnverifiableCode] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)] internal sealed class NullableAttribute : Attribute { public readonly byte[] NullableFlags; public NullableAttribute(byte P_0) { NullableFlags = new byte[1] { P_0 }; } public NullableAttribute(byte[] P_0) { NullableFlags = P_0; } } [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Interface | AttributeTargets.Delegate, AllowMultiple = false, Inherited = false)] internal sealed class NullableContextAttribute : Attribute { public readonly byte Flag; public NullableContextAttribute(byte P_0) { Flag = P_0; } } [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 CatLib.Tests { [BepInPlugin("catlib.tests", "CatLib.Tests", "0.6.2")] [BepInDependency(/*Could not decode attribute arguments.*/)] public sealed class CatLibTestsPlugin : BasePlugin { private ConfigEntry<bool> _runOnMainMenu; private ConfigEntry<float> _fallbackDelaySeconds; private ConfigEntry<string> _entityDumpTypes; private ConfigEntry<string> _textureNames; private EntityInspector _entityInspector; private LabelCloneExperiment _labelClone; private LocalJoin _localJoin; private CrashTrigger _crashes; private MessageProbe _probes; private SaveProbe _saveProbe; private UiHierarchyDumper _uiDumper; private SpriteExporter _sprites; private StressMods _stressMods; private CatLogger _log; private TestRunner _runner; private TestReportWriter _reportWriter; private TimelineRecorder _timeline; private bool _autoRunHandled; public override void Load() { _log = CatLogger.From(((BasePlugin)this).Log); _runOnMainMenu = ((BasePlugin)this).Config.Bind<bool>("Run", "OnMainMenu", true, "Run all tests automatically the first time the main menu is loaded."); _fallbackDelaySeconds = ((BasePlugin)this).Config.Bind<float>("Run", "FallbackDelaySeconds", 45f, "Run all tests after this many seconds if the main menu event was never observed. Set to 0 to disable."); _textureNames = ((BasePlugin)this).Config.Bind<string>("Diagnostics", "TextureExportNames", "t_RepairWorkstation_Elements,t_DamagedParcel,t_Tools_BC,t_StorageTags,t_ParcelsConstraint_Icon,t_PackageConstraint_Heavy,t_Boat_Props_BC,t_Boat_Hull_BC,t_WorldsAndStampsMaps_BC,t_Progression_Objetcs_BC,t_Dock_Props_BC,t_Workstations_BC,t_CatStuff_BC", "Comma separated texture names the Texture export command writes as PNG."); _entityDumpTypes = ((BasePlugin)this).Config.Bind<string>("Diagnostics", "EntityDumpTypes", "EntityInteractableStore,EntityRepairWorkstation,EntityStorageLabel,ParcelDeliverer,ParcelDeliveryManager", "Comma separated game types to list in every entity dump."); string path = Path.Combine(Paths.BepInExRootPath, "CatLib.Tests"); ConfigSandbox.RootDirectory = Path.Combine(path, "Sandbox"); ResetSandbox(); _timeline = new TimelineRecorder(Path.Combine(path, "Timelines"), _log.Scope("Timeline")); _timeline.Start(); _uiDumper = new UiHierarchyDumper(Path.Combine(path, "Dumps"), _log.Scope("UiDump")); _sprites = new SpriteExporter(Path.Combine(path, "Dumps"), _log.Scope("Sprites")); _stressMods = new StressMods(Path.Combine(path, "Stress"), _log.Scope("Stress")); _probes = new MessageProbe(_log.Scope("Probe")); _saveProbe = new SaveProbe("0.6.2", _log.Scope("SaveProbe")); _labelClone = new LabelCloneExperiment(_log.Scope("LabelClone")); _localJoin = new LocalJoin(_log.Scope("LocalJoin")); _crashes = new CrashTrigger(_log.Scope("Crash")); _entityInspector = new EntityInspector(Path.Combine(path, "Dumps"), () => _entityDumpTypes.Value, _log.Scope("Inspect")); _reportWriter = new TestReportWriter(Path.Combine(path, "Reports")); _runner = new TestRunner(TestRegistry.Discover(typeof(CatLibTestsPlugin).Assembly), _log.Scope("Runner")); _runner.Completed += OnRunCompleted; DeclareDemoSettings(); RegisterDevCommands(); BootstrapEvents.MainMenuLoaded += OnMainMenuLoaded; FrameLoop.Update += OnUpdate; _log.Info($"CatLib.Tests {"0.6.2"} loaded with {_runner.TestCount} tests. Run them again from the developer menu."); } private void RegisterDevCommands() { DevMenu.Command("Tests", "Run all tests", (Func<string>)delegate { _runner.Start("DevMenu"); return $"{_runner.TestCount} test(s) started, the report is written when they finish"; }, "Runs every test again. They also run by themselves when the main menu loads."); DevMenu.Toggle("Tests", "Run on main menu", (Func<bool>)(() => _runOnMainMenu.Value), (Action<bool>)delegate(bool value) { _runOnMainMenu.Value = value; }, "Runs all tests the first time the main menu loads. Saved as OnMainMenu in section [Run] of catlib.tests.cfg."); DevMenu.Command("Inspect", "Entity dump", (Func<string>)delegate { _entityInspector.Dump(); return "written to BepInEx/CatLib.Tests/Dumps"; }, "Writes what the camera looks at, with components, fields and object tree, and the nearest objects of the focus types."); DevMenu.Command("Inspect", "Settings menu dump", (Func<string>)delegate { _uiDumper.DumpSettingsMenus(); return "written to BepInEx/CatLib.Tests/Dumps"; }, "Writes the hierarchy of the open settings menu."); DevMenu.Command("Inspect", "Lobby dump", (Func<string>)delegate { _uiDumper.DumpLobby(); return "written to BepInEx/CatLib.Tests/Dumps"; }, "Writes the hierarchy of the open multiplayer lobby with sizes, fonts, colors and sprites."); DevMenu.Command("Inspect", "Sprite export", (Func<string>)(() => "written to " + _sprites.Export()), "Writes every texture that holds loaded UI sprites as PNG, with index.txt naming each sprite and its place. Run it in the menu and in a level."); DevMenu.Command("Inspect", "Texture export", (Func<string>)(() => "written to " + _sprites.ExportTextures(_textureNames.Value.Split(','))), "Writes the textures named in TextureExportNames of catlib.tests.cfg as PNG. Run it in a level, where they are loaded."); DevMenu.Command("Network", "Session roster", (Func<string>)delegate { //IL_015b: Unknown result type (might be due to invalid IL or missing references) //IL_00a5: Unknown result type (might be due to invalid IL or missing references) SessionRoster roster = CatNetwork.Roster; if (!(roster == (SessionRoster)null)) { foreach (RosterEntry player in roster.Players) { _log.Info($"Roster: {player.Name} ({player.Id}){(player.IsHost ? " host" : string.Empty)} {player.Status}, CatLib {player.CatLibVersion}, " + string.Join(", ", player.Mods.Select((RosterMod mod) => $"{mod.Name} {mod.Version} {mod.Mark}"))); } IReadOnlyList<string> readOnlyList = roster.PausedMods((IEnumerable<ModInfo>)CatNetwork.DeclaredMods); _log.Info($"Roster: policy {roster.Policy}, active {string.Join(", ", roster.ActiveMods)}, paused here {((readOnlyList.Count == 0) ? "none" : string.Join(", ", readOnlyList))}"); return $"{roster.Players.Count} player(s), {readOnlyList.Count} paused, see the log"; } return "no session"; }, "Writes every player of the session with their mods and marks, and which mods are paused."); DevMenu.Command("Inspect", "Label clone experiment", (Func<string>)delegate { _labelClone.Toggle(); return "see the log"; }, "Copies the shelf label the camera looks at, press again to remove the copies. Superseded by the Shelf Labels mod."); DevMenu.Command("Network", "Mod message probe", (Func<string>)delegate { _probes.SendPing(); return "see the log"; }, "Sends a mod message to the host, which answers every player. Works alone too."); DevMenu.Command("Network", "Visible players", (Func<string>)_localJoin.ListPlayers, "Lists the players Steam sees, with their rich presence. On one computer these are the other game copies."); DevMenu.Command("Network", "Join the first host", (Func<string>)_localJoin.JoinFirst, "Joins the first visible player that offers a game, the same way accepting a Steam invite does."); DevMenu.Command("Network", "Steam self check", (Func<string>)delegate { SessionNetwork.RunSelfCheck(); return "see the log"; }, "Sends a message to yourself through the CatLib Steam channel."); DevMenu.Command("UI", "Notification preview", (Func<string>)delegate { string languageCode = UiText.LanguageCode; PlayerMessages.Post(UiText.Format("NetPlayerDisconnected", languageCode, new object[2] { "RENTAI", "CatLib Demo Network Mod 1.0.0 / 2.0.0" }), UiText.Format("NetPlayerDisconnectedBrief", languageCode, new object[2] { "RENTAI", "CatLib Demo Network Mod" })); return "posted, it shows as a game notification in a level"; }, "Posts a sample network message."); DevMenu.Command("UI", "Version badge preview", (Func<string>)delegate { //IL_0014: Unknown result type (might be due to invalid IL or missing references) //IL_002a: Unknown result type (might be due to invalid IL or missing references) //IL_0030: Invalid comparison between Unknown and I4 //IL_007b: Unknown result type (might be due to invalid IL or missing references) //IL_0080: Unknown result type (might be due to invalid IL or missing references) GameBuildCheck previewCheck = MenuVersionBadge.PreviewCheck; GameBuildStatus? val = ((previewCheck != null) ? new GameBuildStatus?(previewCheck.Status) : ((GameBuildStatus?)null)); string text = ((!val.HasValue) ? "CMC 1.02.00.1800.9800.100" : (((int)val.GetValueOrDefault() != 3) ? null : "CMC 1.01.00.1700.9722.30000")); string text2 = text; MenuVersionBadge.PreviewCheck = ((text2 == null) ? null : GameCompatibility.Compare(GameBuild.Parse(text2, 0), GameCompatibility.Supported)); return (!(MenuVersionBadge.PreviewCheck == (GameBuildCheck)null)) ? ("pretends the game is " + ((object)MenuVersionBadge.PreviewCheck.Status/*cast due to .constrained prefix*/).ToString()) : "the real check"; }, "Shows the main menu version badge as if the game were newer, then older, then the real check again."); DevMenu.Command("UI", "Stress mods on or off", (Func<string>)delegate { _stressMods.Toggle(); return "see the Mods tab"; }, "Creates or removes a set of mods with many settings to check the Mods tab layout."); DevMenu.Command("Crash", "Native crash, game thread", (Func<string>)_crashes.NativeOnGameThread, "Crashes the game with a native access violation on the game thread to check the CatLib crash window. Press twice."); DevMenu.Command("Crash", "Native crash, worker thread", (Func<string>)_crashes.NativeOnWorkerThread, "Crashes the game from a new native thread, like the Steam networking crash. Press twice."); DevMenu.Command("Crash", "Managed crash", (Func<string>)_crashes.Managed, "Crashes the game with an unhandled .NET exception on a new thread. Press twice."); DevMenu.Command("Crash", "Hang on quit", (Func<string>)_crashes.ToggleHangOnQuit, "Turns on or off a 90 s hang of the game thread while Unity shuts down after a quit, to check that the crash watcher reports a hang and keeps a dump."); } private void DeclareDemoSettings() { CatSettings obj = CatSettings.For((BasePlugin)(object)this); obj.Local<string>("Demo", "Message", "Hello from CatLib", "Edit while the game is running to check live reload.", (AcceptableValueBase)null).Apply((Action<string>)delegate(string value) { _log.Message("Demo.Message is now \"" + value + "\""); }); obj.Local<int>("Demo", "Volume", 50, "Edit while the game is running. Values outside 0-100 are adjusted.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(0, 100)).Apply((Action<int>)delegate(int value) { _log.Message($"Demo.Volume is now {value}"); }); obj.Local<bool>("Demo", "FastMode", false, "Changes to this value only apply after a restart.", (AcceptableValueBase)null).RequiresRestart().Apply((Action<bool>)delegate(bool value) { _log.Message($"Demo.FastMode is {value}"); }); obj.Local<string>("Demo", "NetworkModVersion", "1.0.0", "Version of the demo network mod that every player needs. Change it on one side to test a mismatch.", (AcceptableValueBase)null).Apply((Action<string>)delegate(string value) { CatNetwork.Declare("catlib.tests.demo-network", "CatLib Demo Network Mod", value, (SessionPolicy)1, (VersionRule)2); _log.Message("Demo network mod declared as version " + value); }); obj.Session<int>("Demo", "SharedLimit", 3, "A session setting. In multiplayer the host's value applies to everyone.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(1, 10)).Apply((Action<int>)delegate(int value) { _log.Message($"Demo.SharedLimit is now {value}"); }); } private void ResetSandbox() { try { if (Directory.Exists(ConfigSandbox.RootDirectory)) { Directory.Delete(ConfigSandbox.RootDirectory, recursive: true); } } catch (Exception ex) { _log.Warning("Could not clean " + ConfigSandbox.RootDirectory + ": " + ex.Message); } } private void OnMainMenuLoaded() { if (!_autoRunHandled && _runOnMainMenu.Value) { _autoRunHandled = true; _runner.Start("MainMenuLoaded"); } } private void OnUpdate() { if (!_autoRunHandled && _runOnMainMenu.Value && _fallbackDelaySeconds.Value > 0f && FrameLoop.Realtime >= (double)_fallbackDelaySeconds.Value) { _autoRunHandled = true; _log.Warning($"Main menu event was not observed within {_fallbackDelaySeconds.Value:0} s, running tests anyway"); _runner.Start("FallbackTimeout"); } _labelClone.Update(); _runner.Update(); } private void OnRunCompleted(TestRunSummary summary) { try { string text = _reportWriter.Write(summary); _log.Message("Test report written to " + text); } catch (Exception ex) { _log.Error("Failed to write test report", ex); } } } public static class PluginMeta { public const string Guid = "catlib.tests"; public const string Name = "CatLib.Tests"; public const string Version = "0.6.2"; } } namespace CatLib.Tests.Timeline { public sealed class TimelineRecorder { private readonly string _directory; private readonly CatLogger _log; private StreamWriter _writer; private long _pendingTicks; private string _lastTickArguments; public string FilePath { get; private set; } public TimelineRecorder(string directory, CatLogger log) { _directory = directory; _log = log; } public void Start() { if (_writer == null) { Directory.CreateDirectory(_directory); FilePath = Path.Combine(_directory, "timeline_" + InvariantFormat.FileStamp(DateTime.Now) + ".log"); _writer = new StreamWriter(FilePath, append: false) { AutoFlush = true }; _writer.WriteLine("CatLib timeline started " + InvariantFormat.Timestamp(DateTime.Now)); _writer.WriteLine($"CatLib {"0.6.2"}, BepInEx {Paths.BepInExVersion}"); _writer.WriteLine(" realtime frame event"); GameEventStream.Raised += OnRaised; _log.Info("Recording game event timeline to " + FilePath); } } private void OnRaised(GameEventRecord record) { if (record.Name == "Network.NetworkTick") { _pendingTicks++; _lastTickArguments = record.Arguments; } else { FlushTicks(record); Write(record.Realtime, record.Frame, record.Name, record.Arguments); } } private void FlushTicks(GameEventRecord next) { if (_pendingTicks != 0L) { Write(next.Realtime, next.Frame, "Network.NetworkTick", $"x{_pendingTicks} last {_lastTickArguments}"); _pendingTicks = 0L; } } public static string FormatLine(double realtime, long frame, string name, string arguments) { return realtime.ToString("0.000", CultureInfo.InvariantCulture).PadLeft(13) + " " + frame.ToString(CultureInfo.InvariantCulture).PadLeft(8) + " " + name + (string.IsNullOrEmpty(arguments) ? string.Empty : (" " + arguments)); } private void Write(double realtime, long frame, string name, string arguments) { string text = FormatLine(realtime, frame, name, arguments); try { _writer.WriteLine(text); } catch (Exception ex) { _log.Warning("Failed to write timeline entry: " + ex.Message); } _log.Info(text.TrimStart()); } } } namespace CatLib.Tests.Suites.Ui { public sealed class ContextStripTest : TestCase { public override string Suite => "Ui"; public override int Order => 12; public override IEnumerable<TestStep> Run(TestContext context) { using UiRowsSandbox sandbox = new UiRowsSandbox("UiContext"); foreach (TestStep item in SettingsMenuFixture.SelectMod(context, sandbox.Settings)) { yield return item; } Vector3 pointer = new Vector3(-10000f, -10000f, 0f); ModsPanel.PointerOverride = () => pointer; try { ModsPanel controller = SettingsMenuFixture.Tab.Controller; EventSystem eventSystem = EventSystem.current; Assert.NotNull(eventSystem, "EventSystem.current"); string language = UiText.LanguageCode; SliderRow count = SettingsMenuFixture.Row<SliderRow>("Count"); eventSystem.SetSelectedGameObject(((Component)((SettingRow)count).Control).gameObject); yield return Wait.Frames(2); context.Note("Context: " + controller.ContextLabel.text.Replace("\n", " | ")); Assert.True((object)count == controller.ContextRow, "The selected row must drive the context strip"); Assert.Equal(ContextText.For((ISetting)(object)sandbox.Count, language), controller.ContextLabel.text, "Context strip text"); DropdownRow val = SettingsMenuFixture.Row<DropdownRow>("Color"); eventSystem.SetSelectedGameObject(((Component)((SettingRow)val).Control).gameObject); yield return Wait.Frames(2); Assert.Equal(ContextText.For((ISetting)(object)sandbox.Color, language), controller.ContextLabel.text, "Context strip follows the selection"); eventSystem.SetSelectedGameObject((GameObject)null); yield return Wait.Frames(2); Assert.Equal(UiText.Get("HoverHint", language), controller.ContextLabel.text, "Hint without a focused row and with the pointer away"); RectTransform val2 = ((Il2CppObjectBase)((SettingRow)SettingsMenuFixture.Row<ToggleRow>("Enabled")).Root.transform).TryCast<RectTransform>(); Rect rect = val2.rect; Vector2 screen = RectTransformUtility.WorldToScreenPoint((Camera)null, ((Transform)val2).TransformPoint(Vector2.op_Implicit(((Rect)(ref rect)).center))); RectTransform viewport = ModsTabBuilder.ViewportOf(SettingsMenuFixture.Tab.ContentScroll); Assert.True(RectTransformUtility.RectangleContainsScreenPoint(viewport, screen, (Camera)null), $"Test setup: the Enabled row at {screen.x:0},{screen.y:0} must be inside the visible part of the settings pane"); eventSystem.SetSelectedGameObject(((Component)((SettingRow)count).Control).gameObject); yield return Wait.Frames(2); pointer = new Vector3(screen.x, screen.y, 0f); eventSystem.SetSelectedGameObject((GameObject)null); yield return Wait.Frames(2); context.Note($"Pointer at {screen.x:0},{screen.y:0}: " + controller.ContextLabel.text.Replace("\n", " | ")); Assert.Equal(ContextText.For((ISetting)(object)sandbox.Enabled, language), controller.ContextLabel.text, "After a click clears the selection, the row under a still pointer must drive the context strip"); RectTransform val3 = ((Il2CppObjectBase)((SettingRow)SettingsMenuFixture.Row<TextRow>("Seed")).Root.transform).TryCast<RectTransform>(); rect = val3.rect; Vector2 val4 = RectTransformUtility.WorldToScreenPoint((Camera)null, ((Transform)val3).TransformPoint(Vector2.op_Implicit(((Rect)(ref rect)).center))); if (!RectTransformUtility.RectangleContainsScreenPoint(viewport, val4, (Camera)null)) { pointer = new Vector3(val4.x, val4.y, 0f); yield return Wait.Frames(2); Assert.Equal(UiText.Get("HoverHint", language), controller.ContextLabel.text, "A row scrolled out of the visible area must not react to the pointer"); context.Note("Checked that a row outside the visible area is ignored"); } } finally { ModsPanel.PointerOverride = null; EventSystem current = EventSystem.current; if (current != null) { current.SetSelectedGameObject((GameObject)null); } } } } public sealed class ControlsRegisteredTest : TestCase { public override string Suite => "Ui"; public override int Order => 9; public override IEnumerable<TestStep> Run(TestContext context) { using UiRowsSandbox sandbox = new UiRowsSandbox("UiControls"); foreach (TestStep item in SettingsMenuFixture.SelectMod(context, sandbox.Settings)) { yield return item; } OptionsInterface options = SettingsMenuFixture.Tab.Options; TextRow val = SettingsMenuFixture.Row<TextRow>("Name"); TMP_InputField input = val.Input; TMP_Dropdown dropdown = SettingsMenuFixture.Row<DropdownRow>("Color").Dropdown; Il2CppReferenceArray<SelectableInputField> inputFields = options._inputFields; Il2CppReferenceArray<SelectableDropdown> dropdowns = options._dropdowns; bool flag = false; bool flag2 = false; int num = 0; for (int i = 0; i < ((Il2CppArrayBase<SelectableInputField>)(object)inputFields).Length; i++) { flag |= (Object)(object)((Il2CppArrayBase<SelectableInputField>)(object)inputFields)[i] != (Object)null && ((Il2CppObjectBase)((Il2CppArrayBase<SelectableInputField>)(object)inputFields)[i]).Pointer == ((Il2CppObjectBase)input).Pointer; num += ((!UiClone.IsAlive((Object)(object)((Il2CppArrayBase<SelectableInputField>)(object)inputFields)[i])) ? 1 : 0); } for (int j = 0; j < ((Il2CppArrayBase<SelectableDropdown>)(object)dropdowns).Length; j++) { flag2 |= (Object)(object)((Il2CppArrayBase<SelectableDropdown>)(object)dropdowns)[j] != (Object)null && ((Il2CppObjectBase)((Il2CppArrayBase<SelectableDropdown>)(object)dropdowns)[j]).Pointer == ((Il2CppObjectBase)dropdown).Pointer; num += ((!UiClone.IsAlive((Object)(object)((Il2CppArrayBase<SelectableDropdown>)(object)dropdowns)[j])) ? 1 : 0); } context.Note($"Registered input fields: {((Il2CppArrayBase<SelectableInputField>)(object)inputFields).Length}, dropdowns: {((Il2CppArrayBase<SelectableDropdown>)(object)dropdowns).Length}"); SelectableInputField obj = ((Il2CppObjectBase)input).TryCast<SelectableInputField>(); string text = ((obj != null) ? obj.BigPictureDescription : null); context.Note("Virtual keyboard title of Name: \"" + text + "\""); Assert.True(flag, "Mod input fields must be registered in OptionsInterface._inputFields"); Assert.True(flag2, "Mod dropdowns must be registered in OptionsInterface._dropdowns"); Assert.Equal(0, num, "Destroyed controls left in the OptionsInterface arrays"); Assert.Equal(((SettingRow)val).Label.text, text, "The virtual keyboard of a text setting must be titled with the setting's name"); } } public sealed class GamepadNavigationTest : TestCase { private sealed class Mover { public bool Simulated { get; private set; } = true; public string Failure { get; private set; } public GameObject Move(Selectable from, MoveDirection direction) { //IL_0064: Unknown result type (might be due to invalid IL or missing references) //IL_007a: Expected I4, but got Unknown //IL_001b: Unknown result type (might be due to invalid IL or missing references) //IL_0021: Expected O, but got Unknown //IL_0022: Unknown result type (might be due to invalid IL or missing references) EventSystem current = EventSystem.current; current.SetSelectedGameObject(((Component)from).gameObject); if (Simulated) { try { AxisEventData val = new AxisEventData(current); val.moveDir = direction; from.OnMove(val); return current.currentSelectedGameObject; } catch (Exception ex) { Simulated = false; Failure = ex.GetType().Name; current.SetSelectedGameObject(((Component)from).gameObject); } } Selectable val2 = (Selectable)((int)direction switch { 1 => from.FindSelectableOnUp(), 3 => from.FindSelectableOnDown(), 0 => from.FindSelectableOnLeft(), _ => from.FindSelectableOnRight(), }); if ((Object)(object)val2 != (Object)null && ((UIBehaviour)val2).IsActive()) { current.SetSelectedGameObject(((Component)val2).gameObject); } return current.currentSelectedGameObject; } } public override string Suite => "Ui"; public override int Order => 20; public override IEnumerable<TestStep> Run(TestContext context) { using UiRowsSandbox sandbox = new UiRowsSandbox("UiNavigation"); foreach (TestStep item2 in SettingsMenuFixture.SelectMod(context, sandbox.Settings)) { yield return item2; } Vector3 pointer = new Vector3(-10000f, -10000f, 0f); ModsPanel.PointerOverride = () => pointer; Mover mover = new Mover(); try { EventSystem eventSystem = EventSystem.current; Assert.NotNull(eventSystem, "EventSystem.current"); ModsPanel controller = SettingsMenuFixture.Tab.Controller; string language = UiText.LanguageCode; string values = Values(sandbox); List<Selectable> list = controller.Items.Select((ModListItem val5) => (Selectable)(object)val5.Toggle).ToList(); int num = controller.Items.ToList().FindIndex((ModListItem val5) => val5.IsSelected); Assert.True(num >= 0, "The sandbox mod is selected in the list"); Selectable item = list[num]; Assert.True((Object)(object)SettingsMenuFixture.Tab.Tab.FirstSelected != (Object)null && ((Il2CppObjectBase)SettingsMenuFixture.Tab.Tab.FirstSelected).Pointer == ((Il2CppObjectBase)item).Pointer, "Opening the tab with a gamepad starts on the selected mod"); List<SettingRow> rows = controller.Rows.Where((SettingRow row) => (Object)(object)row.Control != (Object)null).ToList(); Assert.True(rows.Count >= 3, $"Test setup: the sandbox mod has {rows.Count} rows with controls"); Button reset = controller.ResetButton; Assert.NotNull(reset, "The Mods tab has a reset button"); if (num < list.Count - 1) { Expect(mover.Move(item, (MoveDirection)3), list[num + 1], "Down from the selected mod goes to the next mod"); Expect(mover.Move(list[num + 1], (MoveDirection)1), item, "Up goes back to the selected mod"); } else if (num > 0) { Expect(mover.Move(item, (MoveDirection)1), list[num - 1], "Up from the selected mod goes to the previous mod"); Expect(mover.Move(list[num - 1], (MoveDirection)3), item, "Down goes back to the selected mod"); } Expect(mover.Move(item, (MoveDirection)2), rows[0].Control, "Right from the selected mod goes to the first setting"); yield return Wait.Frames(2); ExpectContext(controller, rows[0], language); for (int index = 0; index < rows.Count; index++) { Selectable expected = (Selectable)((index < rows.Count - 1) ? ((object)rows[index + 1].Control) : ((object)reset)); Expect(mover.Move(rows[index].Control, (MoveDirection)3), expected, "Down from " + rows[index].Setting.Key + " goes to " + ((index < rows.Count - 1) ? rows[index + 1].Setting.Key : "the reset button")); if (index < rows.Count - 1) { yield return Wait.Frames(2); ExpectContext(controller, rows[index + 1], language); SettingRow obj = rows[index + 1]; TextRow val = (TextRow)(object)((obj is TextRow) ? obj : null); if (val != null) { Assert.False(val.Input.isFocused, "Selecting " + ((SettingRow)val).Setting.Key + " with a gamepad does not start typing, so the gamepad can move on"); } } } yield return Wait.Frames(2); RectTransform obj2 = ModsTabBuilder.ViewportOf(SettingsMenuFixture.Tab.ContentScroll); RectTransform val2 = ((Il2CppObjectBase)rows[rows.Count - 1].Root.transform).TryCast<RectTransform>(); Rect rect = val2.rect; Vector2 val3 = RectTransformUtility.WorldToScreenPoint((Camera)null, ((Transform)val2).TransformPoint(Vector2.op_Implicit(((Rect)(ref rect)).center))); Assert.True(RectTransformUtility.RectangleContainsScreenPoint(obj2, val3, (Camera)null), "The pane scrolls with the selection, so the last setting is in view after moving down"); eventSystem.SetSelectedGameObject(((Component)rows[0].Control).gameObject); yield return Wait.Frames(2); Assert.True(SettingsMenuFixture.Tab.ContentScroll.verticalNormalizedPosition > 0.99f, "Going back to the first setting scrolls the pane to the top, so the mod card is in view"); Expect(mover.Move((Selectable)(object)reset, (MoveDirection)1), rows[rows.Count - 1].Control, "Up from the reset button goes to the last setting"); Expect(mover.Move((Selectable)(object)reset, (MoveDirection)0), item, "Left from the reset button goes back to the selected mod"); SettingRow val4 = rows.First((SettingRow row) => row is ToggleRow); Expect(mover.Move(val4.Control, (MoveDirection)0), item, "Left from " + val4.Setting.Key + " goes back to the selected mod"); foreach (SettingRow item3 in rows.Where((SettingRow row) => row is SliderRow)) { Selectable selectOnLeft = item3.Control.navigation.selectOnLeft; Assert.True((Object)(object)selectOnLeft != (Object)null && ((Il2CppObjectBase)selectOnLeft).Pointer == ((Il2CppObjectBase)item).Pointer, item3.Setting.Key + " links left to the selected mod"); } TextRow textRow = rows.OfType<TextRow>().First(); eventSystem.SetSelectedGameObject(((Component)((SettingRow)textRow).Control).gameObject); bool flag = true; try { textRow.Input.OnSubmit(new BaseEventData(eventSystem)); } catch (Exception ex) { flag = false; context.Note("The text field has no submit handler to call (" + ex.GetType().Name + ")"); } if (flag) { yield return Wait.Frames(2); Assert.True(textRow.Input.isFocused, "Pressing submit on " + ((SettingRow)textRow).Setting.Key + " starts typing"); textRow.Input.DeactivateInputField(false); yield return Wait.Frames(2); } context.Note("Sliders change their value with left and right, like the game's own sliders"); context.Note(mover.Simulated ? "Moves were sent to the controls the way the game's input sends them" : ("The game has no move handler to call (" + mover.Failure + "), the links were followed directly")); Assert.Equal(values, Values(sandbox), "Moving through the settings changes no values"); } finally { ModsPanel.PointerOverride = null; EventSystem current2 = EventSystem.current; if (current2 != null) { current2.SetSelectedGameObject((GameObject)null); } } } private static string Values(UiRowsSandbox sandbox) { return $"{sandbox.Enabled.Value}|{sandbox.Count.Value}|{sandbox.Speed.Value}|{sandbox.Mode.Value}|{sandbox.Color.Value}|{sandbox.Name.Value}|{sandbox.Seed.Value}|{sandbox.FastMode.Value}|{sandbox.Difficulty.Value}"; } private static void Expect(GameObject actual, Selectable expected, string message) { Assert.True((Object)(object)expected != (Object)null && UiClone.IsAlive((Object)(object)expected) && ((UIBehaviour)expected).IsActive(), message + ": the target must be active"); Assert.True((Object)(object)actual != (Object)null && ((Il2CppObjectBase)actual).Pointer == ((Il2CppObjectBase)((Component)expected).gameObject).Pointer, $"{message}, but the selection is {Describe(actual)} instead of {Describe(((Component)expected).gameObject)}"); } private static string Describe(GameObject gameObject) { if ((Object)(object)gameObject == (Object)null) { return "empty"; } string text = ((Object)gameObject).name; Transform parent = gameObject.transform.parent; int num = 0; while (num < 2 && (Object)(object)parent != (Object)null) { text = ((Object)parent).name + "/" + text; num++; parent = parent.parent; } return text; } private static void ExpectContext(ModsPanel controller, SettingRow row, string language) { Assert.True(row == controller.ContextRow, "The selected " + row.Setting.Key + " drives the description under the settings"); Assert.Equal(ContextText.For(row.Setting, language), controller.ContextLabel.text, "Description of " + row.Setting.Key); } } public sealed class MenuNoticesTest : TestCase { public const string Notice = "CatLib notice test"; public override string Suite => "Ui"; public override int Order => 19; public override IEnumerable<TestStep> Run(TestContext context) { if ((int)SettingsMenuFixture.Context != 0) { context.Note("Main menu notices can only be checked in the main menu"); yield break; } MultiplayerLobbyInterface obj = MenuNotices.Lobby(); Assert.NotNull(obj, "The multiplayer lobby interface must be found"); Assert.NotNull(obj.BackButton, "The lobby must have its back button"); Assert.False(((InterfaceBase)obj).IsShown, "The lobby is not shown in the main menu, so leaving must do nothing"); Assert.False(MenuNotices.LeaveLobby(), "Leaving is refused while the lobby is hidden"); TMP_Text val = MenuNotices.LobbyLabel(); Assert.NotNull(val, "The lobby waiting label must be found"); string text = val.text; MenuNotices.Apply(val, "CatLib notice test"); string text2 = val.text; MenuNotices.Apply(val, "CatLib notice test"); string text3 = val.text; MenuNotices.Apply(val, (string)null); context.Note("Lobby label: \"" + text + "\""); Assert.True(text2.StartsWith(text) && text2.Contains("CatLib notice test"), "The notice is added under the lobby label"); Assert.Equal(text2, text3, "Applying the same notice twice changes nothing"); Assert.Equal(text, val.text, "Clearing restores the lobby label"); List<ConnectedPlayerHelper> playerSlots = obj._playerSlots; context.Note($"Lobby player cards: {playerSlots?.Count ?? 0}"); if (playerSlots != null && playerSlots.Count > 0) { TMP_Text playerNameText = playerSlots[0].PlayerNameText; Assert.NotNull(playerNameText, "A player card must have a name label"); string text4 = playerNameText.text; MenuNotices.Apply(playerNameText, "CatLib notice test"); string text5 = playerNameText.text; MenuNotices.Apply(playerNameText, (string)null); Assert.True(text5.StartsWith(text4) && text5.Contains("CatLib notice test"), "The notice is added under the player name"); Assert.Equal(text4, playerNameText.text, "Clearing restores the player name"); } MenuNotices.SetPlayerNotice(76561190000000002uL, "CatLib notice test"); MenuNotices.ApplyToPlayerCards(obj); int num = 0; int num2 = 0; while (playerSlots != null && num2 < playerSlots.Count) { num += (((Object)(object)playerSlots[num2].PlayerNameText != (Object)null && playerSlots[num2].PlayerNameText.text.Contains("CatLib notice test")) ? 1 : 0); num2++; } MenuNotices.ClearPlayerNotice(76561190000000002uL); Assert.Equal(0, num, "A notice for a player who is not in the lobby must not appear on any card"); } } public sealed class MenuToSettingTest : TestCase { public override string Suite => "Ui"; public override int Order => 7; public override IEnumerable<TestStep> Run(TestContext context) { using UiRowsSandbox sandbox = new UiRowsSandbox("UiWrite"); foreach (TestStep item in SettingsMenuFixture.SelectMod(context, sandbox.Settings)) { yield return item; } SettingsMenuFixture.Row<ToggleRow>("Enabled").Toggle.isOn = false; SettingsMenuFixture.Row<DropdownRow>("Mode").Dropdown.value = 1; SettingsMenuFixture.Row<DropdownRow>("Color").Dropdown.value = 2; ((UnityEvent<string>)(object)SettingsMenuFixture.Row<TextRow>("Name").Input.onEndEdit).Invoke("Tabby"); ((UnityEvent<string>)(object)SettingsMenuFixture.Row<TextRow>("Seed").Input.onEndEdit).Invoke("7"); ((UnityEvent<string>)(object)SettingsMenuFixture.Row<TextRow>("Seed").Input.onEndEdit).Invoke("not a number"); SliderRow val = SettingsMenuFixture.Row<SliderRow>("Count"); SliderRow speed = SettingsMenuFixture.Row<SliderRow>("Speed"); val.Slider.value = 8f; speed.Slider.value = 2.3456f; Assert.False(sandbox.Enabled.Value, "Toggle must apply immediately"); Assert.Equal(SampleMode.SlowMode, sandbox.Mode.Value, "Enum dropdown must apply immediately"); Assert.Equal("Blue", sandbox.Color.Value, "Listed dropdown must apply immediately"); Assert.Equal("Tabby", sandbox.Name.Value, "Text must apply on end edit"); Assert.Equal(7, sandbox.Seed.Value, "Number text must apply on end edit"); Assert.Equal("7", SettingsMenuFixture.Row<TextRow>("Seed").Input.text, "Invalid text must be reverted"); Assert.Equal(5, sandbox.Count.Value, "Slider must not apply while it is being moved"); Assert.True(val.HasPendingValue, "Slider must hold a pending value"); Assert.Equal("8", val.DisplayedText, "Slider text must follow the handle immediately"); yield return Wait.Seconds(0.45); Assert.Equal(8, sandbox.Count.Value, "Slider must apply after the commit delay"); Assert.Equal(2.35f, sandbox.Speed.Value, "Fractional slider must apply rounded to two digits"); Assert.Equal("2.35", speed.DisplayedText, "Fractional slider text"); string text = File.ReadAllText(sandbox.Config.FilePath); Assert.True(text.Contains("Enabled = false"), "The toggle change must be saved to the file"); Assert.True(text.Contains("Count = 8"), "The slider change must be saved to the file"); Assert.True(text.Contains("Name = Tabby"), "The text change must be saved to the file"); context.Note("One warning log line about \"not a number\" is expected"); } } public sealed class MenuVersionBadgeTest : TestCase { public override string Suite => "Ui"; public override int Order => 100; public override IEnumerable<TestStep> Run(TestContext context) { if ((int)SettingsMenuFixture.Context != 0 || !Singleton<MainMenuInterfacesManager>.HasInstance()) { context.Note("The version badge can only be checked in the main menu"); yield break; } MainMenuInterfacesManager manager = Singleton<MainMenuInterfacesManager>.Instance; InterfaceBase menu = manager.MainMenuInterface; if (!menu.IsShown) { manager.ShowMainMenu(); } yield return Wait.Until(() => menu.IsShown && (Object)(object)MenuVersionBadge.Root != (Object)null, 5.0, "the main menu with the version badge"); yield return Wait.Seconds(1.0); GameBuildCheck check = GameCompatibility.Current; Assert.NotNull(check, "The game build is checked in the main menu"); context.Note("Game: " + check.Running.Describe()); context.Note("Made for: " + check.Target.Describe()); context.Note($"Status: {check.Status}"); Assert.NotEqual<GameBuildStatus>((GameBuildStatus)0, check.Status, "The game build is recognised"); GameObject root = MenuVersionBadge.Root; RectTransform rect = ((Il2CppObjectBase)root.transform).TryCast<RectTransform>(); Canvas rootCanvas = ((Component)menu).GetComponentInParent<Canvas>().rootCanvas; Assert.True(((Il2CppObjectBase)root.transform.parent).Pointer == ((Il2CppObjectBase)((Component)rootCanvas).transform).Pointer, "The badge sits on the main menu canvas"); Assert.True(root.GetComponent<CanvasGroup>().alpha > 0.99f, "The badge is visible while the main menu is shown"); Assert.True(MenuVersionBadge.Label.text.Contains(VersionBadgeText.Title), "The badge shows the CatLib version"); Assert.True(MenuVersionBadge.IsPlateShown, "The badge has a plate to stay readable over any background"); Assert.Equal(check.IsWarning, MenuVersionBadge.IsPlateWarning, "The plate is amber only with a warning"); RectTransform canvasRect = ((Il2CppObjectBase)((Component)rootCanvas).transform).TryCast<RectTransform>(); Rect area = canvasRect.rect; Rect val = CanvasRect(rect, canvasRect); context.Note($"Badge at {((Rect)(ref val)).xMin:0},{((Rect)(ref val)).yMin:0} size {((Rect)(ref val)).width:0}x{((Rect)(ref val)).height:0} on a {((Rect)(ref area)).width:0}x{((Rect)(ref area)).height:0} canvas, lifted by {MenuVersionBadge.Lift:0}"); Assert.True(((Rect)(ref val)).xMax <= ((Rect)(ref area)).xMax + 0.5f && ((Rect)(ref val)).yMin >= ((Rect)(ref area)).yMin - 0.5f && ((Rect)(ref val)).xMin >= ((Rect)(ref area)).center.x && ((Rect)(ref val)).yMax <= ((Rect)(ref area)).center.y, "The badge is inside the bottom right quarter of the screen"); foreach (Selectable componentsInChild in ((Component)menu).GetComponentsInChildren<Selectable>(false)) { Rect val2 = CanvasRect(((Il2CppObjectBase)((Component)componentsInChild).transform).TryCast<RectTransform>(), canvasRect); if (((Rect)(ref val2)).width < ((Rect)(ref area)).width * 0.5f && ((Rect)(ref val2)).height < ((Rect)(ref area)).height * 0.5f) { Assert.False(((Rect)(ref val2)).Overlaps(val), "The badge does not cover the " + ((Object)componentsInChild).name + " button"); } } string calmText = MenuVersionBadge.Label.text; Vector2 calmSize = rect.sizeDelta; Camera val3 = (((int)rootCanvas.renderMode == 0) ? null : rootCanvas.worldCamera); Rect rect2 = rect.rect; Vector2 center = RectTransformUtility.WorldToScreenPoint(val3, ((Transform)rect).TransformPoint(Vector2.op_Implicit(((Rect)(ref rect2)).center))); try { MenuVersionBadge.PointerOverride = () => new Vector3(center.x, center.y, 0f); yield return Wait.Frames(3); Assert.True(MenuVersionBadge.IsHovered, "Pointing at the badge is noticed"); Assert.True(MenuVersionBadge.Label.text.Split('\n').Length > calmText.Split('\n').Length, "Pointing at the badge shows the detail line"); context.Note("Hover: " + MenuVersionBadge.Label.text.Replace("\n", " | ")); MenuVersionBadge.PointerOverride = () => new Vector3(-10000f, -10000f, 0f); yield return Wait.Frames(3); Assert.Equal(calmText, MenuVersionBadge.Label.text, "Moving the pointer away hides the detail line"); string[] array = new string[2] { "CMC 1.02.00.1800.9800.100", "CMC 1.01.00.1700.9722.30000" }; for (int num = 0; num < array.Length; num++) { MenuVersionBadge.PreviewCheck = GameCompatibility.Compare(GameBuild.Parse(array[num], 0), GameCompatibility.Supported); yield return Wait.Frames(3); string value = VersionBadgeText.Warning(MenuVersionBadge.PreviewCheck, UiText.LanguageCode); Assert.True(MenuVersionBadge.Label.text.Contains(value), $"The {MenuVersionBadge.PreviewCheck.Status} warning is shown"); Assert.True(MenuVersionBadge.IsPlateWarning, "A warning has an amber plate"); Assert.True(rect.sizeDelta.y > calmSize.y || check.IsWarning, "A warning makes the badge taller"); Rect val4 = CanvasRect(rect, canvasRect); Assert.True(((Rect)(ref val4)).xMin >= ((Rect)(ref area)).xMin && ((Rect)(ref val4)).xMax <= ((Rect)(ref area)).xMax + 0.5f, "The warning fits on the screen"); context.Note($"{MenuVersionBadge.PreviewCheck.Status}: {MenuVersionBadge.Label.text.Replace("\n", " | ")}, {((Rect)(ref val4)).width:0}x{((Rect)(ref val4)).height:0}"); yield return Wait.Seconds(1.5); } } finally { MenuVersionBadge.PointerOverride = null; MenuVersionBadge.PreviewCheck = null; } yield return Wait.Frames(3); Assert.Equal(calmText, MenuVersionBadge.Label.text, "The badge returns to the real check"); manager.ShowSettingsMenu(); yield return Wait.Seconds(1.0); Assert.True(root.GetComponent<CanvasGroup>().alpha < 0.01f, "The badge hides with the main menu"); manager.GoBackToLastInterface(); yield return Wait.Seconds(1.0); Assert.True(root.GetComponent<CanvasGroup>().alpha > 0.99f, "The badge comes back with the main menu"); } private static Rect CanvasRect(RectTransform rect, RectTransform canvasRect) { //IL_0012: Unknown result type (might be due to invalid IL or missing references) //IL_0017: Unknown result type (might be due to invalid IL or missing references) //IL_001c: Unknown result type (might be due to invalid IL or missing references) //IL_0020: Unknown result type (might be due to invalid IL or missing references) //IL_0025: Unknown result type (might be due to invalid IL or missing references) //IL_002a: Unknown result type (might be due to invalid IL or missing references) //IL_002b: Unknown result type (might be due to invalid IL or missing references) //IL_0031: Unknown result type (might be due to invalid IL or missing references) //IL_003c: Unknown result type (might be due to invalid IL or missing references) //IL_0042: Unknown result type (might be due to invalid IL or missing references) //IL_004d: Unknown result type (might be due to invalid IL or missing references) //IL_0053: Unknown result type (might be due to invalid IL or missing references) //IL_005e: Unknown result type (might be due to invalid IL or missing references) //IL_0064: Unknown result type (might be due to invalid IL or missing references) //IL_006f: Unknown result type (might be due to invalid IL or missing references) Il2CppStructArray<Vector3> val = new Il2CppStructArray<Vector3>(4L); rect.GetWorldCorners(val); Vector3 val2 = ((Transform)canvasRect).InverseTransformPoint(((Il2CppArrayBase<Vector3>)(object)val)[0]); Vector3 val3 = ((Transform)canvasRect).InverseTransformPoint(((Il2CppArrayBase<Vector3>)(object)val)[2]); return Rect.MinMaxRect(Mathf.Min(val2.x, val3.x), Mathf.Min(val2.y, val3.y), Mathf.Max(val2.x, val3.x), Mathf.Max(val2.y, val3.y)); } } public sealed class ModCardTest : TestCase { public override string Suite => "Ui"; public override int Order => 11; public override IEnumerable<TestStep> Run(TestContext context) { using UiRowsSandbox sandbox = new UiRowsSandbox("UiCard"); foreach (TestStep item in SettingsMenuFixture.SelectMod(context, sandbox.Settings)) { yield return item; } ModCard card = SettingsMenuFixture.Tab.Controller.Card; string language = UiText.LanguageCode; Assert.Equal(sandbox.Settings.DisplayName, card.Title.text, "Card title"); Assert.Equal<string>(string.Empty, card.Version.text, "A mod without a version shows no version"); Assert.Equal(UiText.Plural("SettingsCount", 9, language), card.Status.text, "Card status"); Assert.Equal(0, card.Root.transform.GetSiblingIndex(), "The card must be the first element of the settings pane"); sandbox.FastMode.Entry.Value = true; yield return Wait.Frames(17); string expected = UiText.Plural("SettingsCount", 9, language) + " · " + UiText.Plural("PendingRestart", 1, language); context.Note("Status with a pending restart: " + card.Status.text); Assert.Equal<string>(expected, card.Status.text, "Card status with a pending restart"); } } public sealed class ModIconTest : TestCase { public override string Suite => "Ui"; public override int Order => 17; public override IEnumerable<TestStep> Run(TestContext context) { foreach (TestStep item in SettingsMenuFixture.EnsureModsTab(context)) { yield return item; } CatSettings catLib = CatLibRuntime.Settings; context.Note("CatLib folder: " + (catLib.PluginDirectory ?? "unknown") + ", icon: " + (catLib.IconPath ?? "none")); Assert.NotNull(catLib.IconPath, "The build copies icon.png next to CatLib.dll and CatLib finds it"); ModListItem val = ((IEnumerable<ModListItem>)SettingsMenuFixture.Tab.Controller.Items).FirstOrDefault((Func<ModListItem, bool>)((ModListItem entry) => entry.Settings == catLib)); Assert.NotNull(val, "CatLib is in the mods list"); Assert.True((Object)(object)val.Icon != (Object)null && ((Component)val.Icon).gameObject.activeSelf && (Object)(object)val.Icon.sprite != (Object)null, "CatLib shows its icon in the mods list"); foreach (ModListItem item2 in SettingsMenuFixture.Tab.Controller.Items) { Sprite val2 = (((Object)(object)item2.Icon == (Object)null) ? null : item2.Icon.sprite); context.Note(item2.Settings.DisplayName + ": " + (((Object)(object)val2 != (Object)null) ? $"icon {((Texture)val2.texture).width}x{((Texture)val2.texture).height}" : "no icon")); } } } public sealed class ModsListTest : TestCase { public override string Suite => "Ui"; public override int Order => 5; public override IEnumerable<TestStep> Run(TestContext context) { foreach (TestStep item in SettingsMenuFixture.EnsureModsTab(context)) { yield return item; } ModsPanel controller = SettingsMenuFixture.Tab.Controller; UiRowsSandbox sandbox = new UiRowsSandbox("UiList"); try { yield return Wait.Until(() => SettingsMenuFixture.HasItem(controller, sandbox.Settings), 3.0, "the new mod to appear in the list"); context.Note($"Mods listed with the sandbox: {controller.Items.Count}"); } finally { sandbox.Dispose(); } yield return Wait.Until(() => !SettingsMenuFixture.HasItem(controller, sandbox.Settings), 3.0, "the disposed mod to leave the list"); Assert.True(controller.Selected != null, "A mod must stay selected after the selected one disappears"); } } public sealed class ModsTabHygieneTest : TestCase { public override string Suite => "Ui"; public override int Order => 1; public override IEnumerable<TestStep> Run(TestContext context) { foreach (TestStep item in SettingsMenuFixture.EnsureModsTab(context)) { yield return item; } ModsTab tab = SettingsMenuFixture.Tab; int length = ((Component)tab.Tab).GetComponentsInChildren<Localize>(true).Length; int num = 0; int num2 = 0; foreach (Localize componentsInChild in tab.Panel.GetComponentsInChildren<Localize>(true)) { if ((Object)(object)((Component)componentsInChild).GetComponentInParent<SelectableButton>(true) != (Object)null) { num2++; } else { num++; } } int length2 = tab.Panel.GetComponentsInChildren<LocalizeDropdown>(true).Length; int actual = tab.Panel.GetComponentsInChildren<AudioSettingsInterface>(true).Length + tab.Panel.GetComponentsInChildren<GameplaySettingsInterface>(true).Length + tab.Panel.GetComponentsInChildren<ControlsSettingsInterface>(true).Length + tab.Panel.GetComponentsInChildren<InputsSettingsInterface>(true).Length; ButtonClickedEvent onClick = ((Component)tab.Tab).GetComponent<Button>().onClick; int persistentEventCount = ((UnityEventBase)onClick).GetPersistentEventCount(); int num3 = 0; for (int i = 0; i < persistentEventCount; i++) { if ((int)((UnityEventBase)onClick).GetPersistentListenerState(i) != 0) { num3++; } } context.Note($"Tab button persistent listeners: {persistentEventCount}, still enabled: {num3}"); context.Note($"Localization kept on the revert button: {num2}"); Assert.Equal(0, length, "Localize components left on the Mods tab"); Assert.Equal(0, num, "Localize components left in the Mods panel outside the revert button"); Assert.Equal(0, length2, "LocalizeDropdown components left in the Mods panel"); Assert.Equal(0, actual, "Game settings interfaces left in the Mods panel"); Assert.Equal(0, num3, "Enabled persistent listeners on the Mods tab button"); } } public sealed class ModsTabInjectedTest : TestCase { public override string Suite => "Ui"; public override int Order => 0; public override IEnumerable<TestStep> Run(TestContext context) { foreach (TestStep item in SettingsMenuFixture.EnsureModsTab(context)) { yield return item; } ModsTab tab = SettingsMenuFixture.Tab; List<TabInterface> tabs = tab.Options._tabs; int num = tabs.IndexOf(tab.Tab); TabInterface val = tabs[0]; context.Note($"Context: {tab.Context}"); context.Note($"Registered tabs: {tabs.Count}, Mods tab index: {num}"); Assert.True(num >= 0, "The Mods tab must be registered in OptionsInterface._tabs"); Assert.Equal(tabs.Count - 1, num, "The Mods tab must be the last registered tab"); Assert.True(tab.IsVisible, "The Mods tab must be visible while mods declare settings"); Assert.True(((Il2CppObjectBase)((Component)tab.Tab).transform.parent).Pointer == ((Il2CppObjectBase)((Component)val).transform.parent).Pointer, "The Mods tab must live in the tab bar"); Assert.True(((Il2CppObjectBase)tab.Panel.transform.parent).Pointer == ((Il2CppObjectBase)val.AssociatedPanel.transform.parent).Pointer, "The Mods panel must live next to the game panels"); Assert.True(((Il2CppObjectBase)tab.Tab.AssociatedPanel).Pointer == ((Il2CppObjectBase)tab.Panel).Pointer, "The tab must point to the Mods panel"); } } public sealed class ModsTabLayoutTest : TestCase { public override string Suite => "Ui"; public override int Order => 2; public override IEnumerable<TestStep> Run(TestContext context) { foreach (TestStep item in SettingsMenuFixture.EnsureModsTab(context)) { yield return item; } ModsTab tab = SettingsMenuFixture.Tab; RectTransform val = ((Il2CppObjectBase)((Component)tab.ListScroll).transform).TryCast<RectTransform>(); RectTransform val2 = ((Il2CppObjectBase)((Component)tab.ContentScroll).transform).TryCast<RectTransform>(); float num = val.anchoredPosition.x + val.sizeDelta.x / 2f; float num2 = val2.anchoredPosition.x - val2.sizeDelta.x / 2f; context.Note($"List pane width {val.sizeDelta.x}, content pane width {val2.sizeDelta.x}, gap {num2 - num}"); string languageCode = UiText.LanguageCode; string text = ((Component)tab.Tab).GetComponentInChildren<TMP_Text>(true).text; string text2 = tab.ListHeader.GetComponentInChildren<TMP_Text>(true).text; string text3 = tab.Controller.Card.Title.text; context.Note($"Language: {languageCode}, tab \"{text}\", list header \"{text2}\", card \"{text3}\""); Assert.True(((Il2CppObjectBase)((Transform)val).parent).Pointer == ((Il2CppObjectBase)tab.Panel.transform).Pointer, "The list pane must be a direct child of the Mods panel"); Assert.True(((Il2CppObjectBase)((Transform)val2).parent).Pointer == ((Il2CppObjectBase)tab.Panel.transform).Pointer, "The content pane must be a direct child of the Mods panel"); Assert.True(num2 - num >= 29f, "The panes must not overlap"); Assert.True(val2.sizeDelta.x > val.sizeDelta.x, "The content pane must be wider than the list pane"); Assert.Equal(UiText.Get("ModsTab", languageCode), text, "Tab title"); Assert.Equal(UiText.Get("ModsList", languageCode), text2, "List header"); CatSettings selected = tab.Controller.Selected; Assert.Equal((selected == null) ? UiText.Get("SelectMod", languageCode) : SettingTexts.ModName(selected, languageCode), text3, "Card title"); Assert.Equal(1 + tab.Controller.Items.Count, ((Transform)tab.ListScroll.content).childCount, "Children of the list pane content"); Assert.True(tab.Controller.Items.Count >= 1, "The Mods list must contain at least the CatLib.Tests demo mod"); } } public sealed class ModsTabSwitchTest : TestCase { public override string Suite => "Ui"; public override int Order => 3; public override IEnumerable<TestStep> Run(TestContext context) { foreach (TestStep item in SettingsMenuFixture.EnsureModsTab(context)) { yield return item; } ModsTab modsTab = SettingsMenuFixture.Tab; OptionsInterface options = modsTab.Options; TabInterface previous = options._selectedTab ?? options._tabs[0]; modsTab.Tab.SetTabActive(true, true); yield return Wait.NextFrame(); int otherActivePanels = 0; List<TabInterface> tabs = options._tabs; for (int i = 0; i < tabs.Count; i++) { TabInterface val = tabs[i]; if (((Il2CppObjectBase)val).Pointer != ((Il2CppObjectBase)modsTab.Tab).Pointer && (Object)(object)val.AssociatedPanel != (Object)null && val.AssociatedPanel.activeSelf) { otherActivePanels++; } } bool selectedIsMods = (Object)(object)options._selectedTab != (Object)null && ((Il2CppObjectBase)options._selectedTab).Pointer == ((Il2CppObjectBase)modsTab.Tab).Pointer; bool modsPanelActive = modsTab.Panel.activeSelf; previous.SetTabActive(true, true); yield return Wait.NextFrame(); bool condition = (Object)(object)options._selectedTab != (Object)null && ((Il2CppObjectBase)options._selectedTab).Pointer == ((Il2CppObjectBase)previous).Pointer; bool condition2 = !modsTab.Panel.activeSelf; Assert.True(modsPanelActive, "Selecting the Mods tab must show the Mods panel"); Assert.True(selectedIsMods, "OptionsInterface must treat the Mods tab as selected"); Assert.Equal(0, otherActivePanels, "Game panels left visible while the Mods tab is selected"); Assert.True(condition, "Selecting the previous tab again must work"); Assert.True(condition2, "Leaving the Mods tab must hide the Mods panel"); } } public sealed class PlayerMessagesTest : TestCase { public override string Suite => "Ui"; public override int Order => 14; public override IEnumerable<TestStep> Run(TestContext context) { UiRowsSandbox sandbox = new UiRowsSandbox("UiMessages"); try { foreach (TestStep item in SettingsMenuFixture.SelectMod(context, sandbox.Settings)) { yield return item; } ModsPanel controller = SettingsMenuFixture.Tab.Controller; string language = UiText.LanguageCode; sandbox.Config.WriteValue("General", "Count", "50"); yield return Wait.Until(() => sandbox.Config.Adjusted.Count >= 1, 5.0, "the out of range value to be adjusted"); yield return Wait.Frames(2); string expected = UiText.Format("MessageAdjusted", language, new object[4] { sandbox.Settings.DisplayName, "Count", "50", "10" }); Assert.Equal(expected, PlayerMessages.Last, "Message for an adjusted value"); Assert.Equal(expected, controller.StatusLabel.text, "The status line must show the latest message"); sandbox.Config.WriteValue("General", "Count", "many"); yield return Wait.Until(() => sandbox.Config.Rejected.Count >= 1, 5.0, "the invalid value to be rejected"); Assert.Equal(UiText.Format("MessageRejected", language, new object[4] { sandbox.Settings.DisplayName, "Count", "many", "10" }), PlayerMessages.Last, "Message for a rejected value"); sandbox.FastMode.Entry.Value = true; yield return Wait.Frames(2); Assert.Equal(UiText.Format("MessageRestart", language, new object[2] { sandbox.Settings.DisplayName, "Fast Mode" }), PlayerMessages.Last, "Message for a restart-only change"); context.Note("Warning log lines about \"50\" and \"many\" are expected"); } finally { if (sandbox != null) { ((IDisposable)sandbox).Dispose(); } } } } internal readonly struct RectExtent { public float MinX { get; } public float MaxX { get; } public float MinY { get; } public float MaxY { get; } public float Width => MaxX - MinX; public float Height => MaxY - MinY; private RectExtent(float minX, float maxX, float minY, float maxY) { MinX = minX; MaxX = maxX; MinY = minY; MaxY = maxY; } public static RectExtent Of(RectTransform target, RectTransform space) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_003b: Unknown result type (might be due to invalid IL or missing references) //IL_0040: Unknown result type (might be due to invalid IL or missing references) //IL_005a: Unknown result type (might be due to invalid IL or missing references) //IL_005f: Unknown result type (might be due to invalid IL or missing references) //IL_0079: Unknown result type (might be due to invalid IL or missing references) //IL_007e: Unknown result type (might be due to invalid IL or missing references) //IL_0098: Unknown result type (might be due to invalid IL or missing references) //IL_009d: Unknown result type (might be due to invalid IL or missing references) //IL_00ad: Unknown result type (might be due to invalid IL or missing references) //IL_00b2: Unknown result type (might be due to invalid IL or missing references) //IL_00b6: Unknown result type (might be due to invalid IL or missing references) //IL_00b8: Unknown result type (might be due to invalid IL or missing references) //IL_00bd: Unknown result type (might be due to invalid IL or missing references) //IL_00c2: Unknown result type (might be due to invalid IL or missing references) //IL_00c5: Unknown result type (might be due to invalid IL or missing references) //IL_00d3: Unknown result type (might be due to invalid IL or missing references) //IL_00e1: Unknown result type (might be due to invalid IL or missing references) //IL_00f0: Unknown result type (might be due to invalid IL or missing references) Rect rect = target.rect; float num = float.MaxValue; float num2 = float.MinValue; float num3 = float.MaxValue; float num4 = float.MinValue; Vector3[] array = (Vector3[])(object)new Vector3[4] { new Vector3(((Rect)(ref rect)).xMin, ((Rect)(ref rect)).yMin, 0f), new Vector3(((Rect)(ref rect)).xMin, ((Rect)(ref rect)).yMax, 0f), new Vector3(((Rect)(ref rect)).xMax, ((Rect)(ref rect)).yMin, 0f), new Vector3(((Rect)(ref rect)).xMax, ((Rect)(ref rect)).yMax, 0f) }; foreach (Vector3 val in array) { Vector3 val2 = ((Transform)space).InverseTransformPoint(((Transform)target).TransformPoint(val)); num = Mathf.Min(num, val2.x); num2 = Mathf.Max(num2, val2.x); num3 = Mathf.Min(num3, val2.y); num4 = Mathf.Max(num4, val2.y); } return new RectExtent(num, num2, num3, num4); } public override string ToString() { return $"x {MinX:0}..{MaxX:0}, y {MinY:0}..{MaxY:0}"; } } public sealed class ResetButtonTest : TestCase { public override string Suite => "Ui"; public override int Order => 13; public override IEnumerable<TestStep> Run(TestContext context) { using UiRowsSandbox sandbox = new UiRowsSandbox("UiReset"); foreach (TestStep item in SettingsMenuFixture.SelectMod(context, sandbox.Settings)) { yield return item; } ModsPanel controller = SettingsMenuFixture.Tab.Controller; OptionsInterface options = SettingsMenuFixture.Tab.Options; sandbox.Enabled.Entry.Value = false; sandbox.Count.Entry.Value = 9; sandbox.Mode.Entry.Value = SampleMode.HTTPMode; sandbox.Name.Entry.Value = "Tabby"; sandbox.Secret.Entry.Value = "changed"; int messages = PlayerMessages.Count; Assert.NotNull(controller.ResetButton, "The Mods panel must have a reset button"); EventSystem current = EventSystem.current; if (current != null) { current.SetSelectedGameObject((GameObject)null); } ((UnityEvent)controller.ResetButton.onClick).Invoke(); Assert.NotNull(options._actionOnRevertConfirmed, "The reset button must open the game's confirmation popup even when nothing is selected"); Assert.True(sandbox.Count.Value == 9, "Nothing may change before the reset is confirmed"); ((UnityEvent)options.ConfirmRevertYesButton.onClick).Invoke(); yield return Wait.Frames(2); Assert.True(sandbox.Enabled.Value, "Enabled after the reset"); Assert.Equal(5, sandbox.Count.Value, "Count after the reset"); Assert.Equal(SampleMode.FastMode, sandbox.Mode.Value, "Mode after the reset"); Assert.Equal("Cat", sandbox.Name.Value, "Name after the reset"); Assert.Equal("changed", sandbox.Secret.Value, "Hidden settings must keep their value"); Assert.Equal(5f, SettingsMenuFixture.Row<SliderRow>("Count").Slider.value, "The slider must show the default"); Assert.True(PlayerMessages.Count > messages, "The reset must be reported to the player"); Assert.Equal(UiText.Format("MessageReset", UiText.LanguageCode, new object[1] { sandbox.Settings.DisplayName }), PlayerMessages.Last, "Reset message"); } } public sealed class RestartSuspendTest : TestCase { public override string Suite => "Ui"; public override int Order => 17; public override IEnumerable<TestStep> Run(TestContext context) { foreach (TestStep item in SettingsMenuFixture.EnsureModsTab(context)) { yield return item; } MenuContext menu = SettingsMenuFixture.Context; foreach (TestStep item2 in SettingsMenuFixture.ShowModsTab(context)) { yield return item2; } ModsTab before = ModsMenu.Get(menu); OptionsInterface options = before.Options; ModsMenu.SuspendForRestart(); yield return Wait.Frames(2); Assert.True(ModsMenu.IsSuspended, "The Mods tab must be suspended after a restart starts"); Assert.Null(ModsMenu.Get(menu), "The Mods tab must be released during a restart"); yield return Wait.Frames(5); Assert.Null(ModsMenu.Get(menu), "Nothing may be injected while the game is restarting"); ModsMenu.Resume(); yield return Wait.Until(() => ModsMenu.Get(menu) != null, 5.0, "the Mods tab to be injected again"); ModsTab val = ModsMenu.Get(menu); int num = 0; for (int num2 = 0; num2 < options.TabsParent.childCount; num2++) { num += ((((Object)((Component)options.TabsParent.GetChild(num2)).gameObject).name == "btn_TopTab CatLibMods") ? 1 : 0); } int num3 = 0; for (int num4 = 0; num4 < options._tabs.Count; num4++) { num3 += ((((Object)((Component)options._tabs[num4]).gameObject).name == "btn_TopTab CatLibMods") ? 1 : 0); } Transform parent = val.Panel.transform.parent; int num5 = 0; for (int num6 = 0; num6 < parent.childCount; num6++) { num5 += ((((Object)((Component)parent.GetChild(num6)).gameObject).name == "panel_CatLibMods") ? 1 : 0); } int num7 = 0; for (int num8 = 0; num8 < ((Il2CppArrayBase<SelectableInputField>)(object)options._inputFields).Length; num8++) { num7 += ((!UiClone.IsAlive((Object)(object)((Il2CppArrayBase<SelectableInputField>)(object)options._inputFields)[num8])) ? 1 : 0); } for (int num9 = 0; num9 < ((Il2CppArrayBase<SelectableDropdown>)(object)options._dropdowns).Length; num9++) { num7 += ((!UiClone.IsAlive((Object)(object)((Il2CppArrayBase<SelectableDropdown>)(object)options._dropdowns)[num9])) ? 1 : 0); } bool condition = (Object)(object)options._selectedTab == (Object)null || UiClone.IsAlive((Object)(object)options._selectedTab); context.Note($"Tabs in the bar: {num}, registered: {num3}, panels: {num5}, dead controls: {num7}"); Assert.True(condition, "The selected tab of the game menu must not be a destroyed Mods tab"); Assert.False(before == val, "A new Mods tab must be built after resuming"); Assert.Equal(1, num, "Mods tabs in the tab bar"); Assert.Equal(1, num3, "Mods tabs registered in OptionsInterface._tabs"); Assert.Equal(1, num5, "Mods panels next to the game panels"); Assert.Equal(0, num7, "Destroyed controls left in the OptionsInterface arrays"); } } public sealed class RowGeometryTest : TestCase { public const float Tolerance = 12f; public const float MaxTrackThickness = 60f; public override string Suite => "Ui"; public override int Order => 10; public override IEnumerable<TestStep> Run(TestContext context) { using UiRowsSandbox sandbox = new UiRowsSandbox("UiGeometry"); foreach (TestStep item in SettingsMenuFixture.SelectMod(context, sandbox.Settings)) { yield return item; } ModsPanel controller = SettingsMenuFixture.Tab.Controller; foreach (SettingRow row in controller.Rows) { Transform obj = row.Root.transform.Find("panel_Label"); RectTransform val = ((obj != null) ? ((Il2CppObjectBase)obj).TryCast<RectTransform>() : null); Transform obj2 = row.Root.transform.Find("panel_Label/img_DashLine"); RectTransform val2 = ((obj2 != null) ? ((Il2CppObjectBase)obj2).TryCast<RectTransform>() : null); if (!((Object)(object)val == (Object)null) && !((Object)(object)val2 == (Object)null)) { RectExtent value = RectExtent.Of(val2, val); Rect rect = val.rect; Assert.True(value.MinX >= ((Rect)(ref rect)).xMin - 1f && value.MaxX <= ((Rect)(ref rect)).xMax + 1f, $"Dash line of {row.Setting.Key} ({value}) must stay inside its label panel (x {((Rect)(ref rect)).xMin:0}..{((Rect)(ref rect)).xMax:0})"); } } string[] array = new string[2] { "Count", "Speed" }; Rect rect2; foreach (string text in array) { Slider slider = SettingsMenuFixture.Row<SliderRow>(text).Slider; RectTransform val3 = ((Il2CppObjectBase)((Component)slider).transform).TryCast<RectTransform>(); RectExtent value2 = RectExtent.Of(((Il2CppObjectBase)((Component)slider).transform.Find("img_Bkg")).TryCast<RectTransform>(), val3); DefaultInterpolatedStringHandler defaultInterpolatedStringHandler = new DefaultInterpolatedStringHandler(23, 3); defaultInterpolatedStringHandler.AppendFormatted(text); defaultInterpolatedStringHandler.AppendLiteral(": slider width "); rect2 = val3.rect; defaultInterpolatedStringHandler.AppendFormatted(((Rect)(ref rect2)).width, "0"); defaultInterpolatedStringHandler.AppendLiteral(", track "); defaultInterpolatedStringHandler.AppendFormatted(value2); context.Note(defaultInterpolatedStringHandler.ToStringAndClear()); float width = value2.Width; rect2 = val3.rect; bool condition = Mathf.Abs(width - ((Rect)(ref rect2)).width) <= 12f; DefaultInterpolatedStringHandler defaultInterpolatedStringHandler2 = new DefaultInterpolatedStringHandler(47, 3); defaultInterpolatedStringHandler2.AppendLiteral("Track of "); defaultInterpolatedStringHandler2.AppendFormatted(text); defaultInterpolatedStringHandler2.AppendLiteral(" must span the slider: track "); defaultInterpolatedStringHandler2.AppendFormatted(value2.Width, "0"); defaultInterpolatedStringHandler2.AppendLiteral(", slider "); rect2 = val3.rect; defaultInterpolatedStringHandler2.AppendFormatted(((Rect)(ref rect2)).width, "0"); Assert.True(condition, defaultInterpolatedStringHandler2.ToStringAndClear()); Assert.True(value2.Height > 5f && value2.Height <= 60f, $"Track of {text} must have a visible thickness, got {value2.Height:0}"); } foreach (GameObject item2 in new List<GameObject>(controller.SectionHeaders) { SettingsMenuFixture.Tab.ListHeader }) { TMP_Text componentInChildren = item2.GetComponentInChildren<TMP_Text>(true); RectTransform val4 = ((Il2CppObjectBase)componentInChildren.transform.parent).TryCast<RectTransform>(); float x = componentInChildren.GetPreferredValues(componentInChildren.text).x; rect2 = val4.rect; bool condition2 = x <= ((Rect)(ref rect2)).width; DefaultInterpolatedStringHandler defaultInterpolatedStringHandler3 = new DefaultInterpolatedStringHandler(39, 3); defaultInterpolatedStringHandler3.AppendLiteral("Tape \""); defaultInterpolatedStringHandler3.AppendFormatted(componentInChildren.text); defaultInterpolatedStringHandler3.AppendLiteral("\" must fit its text: text "); defaultInterpolatedStringHandler3.AppendFormatted(x, "0"); defaultInterpolatedStringHandler3.AppendLiteral(", tape "); rect2 = val4.rect; defaultInterpolatedStringHandler3.AppendFormatted(((Rect)(ref rect2)).width, "0"); Assert.True(condition2, defaultInterpolatedStringHandler3.ToStringAndClear()); } } } public sealed class SceneOwnershipTest : TestCase { public const string LegacyStagingName = "CatLib.UiStaging"; public override string Suite => "Ui"; public override int Order => 18; public override IEnumerable<TestStep> Run(TestContext context) { foreach (TestStep item in SettingsMenuFixture.EnsureModsTab(context)) { yield return item; } ModsTab tab = SettingsMenuFixture.Tab; Scene scene = ((Component)tab.Options).gameObject.scene; context.Note("Settings menu scene: " + ((Scene)(ref scene)).name); Assert.True(object.Equals(scene, ((Component)tab.Tab).gameObject.scene), "The Mods tab must belong to the scene of its settings menu"); Assert.True(object.Equals(scene, tab.Panel.scene), "The Mods panel must belong to the scene of its settings menu"); foreach (SettingRow row in tab.Controller.Rows) { Assert.True(object.Equals(scene, row.Root.scene), "The row of " + row.Setting.Id + " must belong to the scene of its settings menu"); } int num = 0; foreach (GameObject item2 in Resources.FindObjectsOfTypeAll<GameObject>()) { if (((Object)item2).name == "CatLib.Staging" || ((Object)item2).name == "CatLib.UiStaging") { num++; } } Assert.Equal(0, num, "No staging objects may outlive an injection, they would keep a reference to a scene that gets unloaded"); } } public sealed class ScrollbarLayoutTest : TestCase { public override string Suite => "Ui"; public override int Order => 16; public override IEnumerable<TestStep> Run(TestContext context) { foreach (TestStep item2 in SettingsMenuFixture.EnsureModsTab(context)) { yield return item2; } ModsTab tab = SettingsMenuFixture.Tab; OptionsInterface val = ModsMenu.FindOptions(SettingsMenuFixture.Context); List<TabInterface> tabs = val._tabs; for (int i = 0; i < tabs.Count; i++) { GameObject associatedPanel = tabs[i].AssociatedPanel; ScrollRect val2 = (((Object)(object)associatedPanel == (Object)null) ? null : ModsTabBuilder.FindDirectScroll(associatedPanel.transform)); context.Note($"game tab {i}: {ModsTabBuilder.SafeDescribe(((Object)(object)val2 == (Object)null) ? null : val2.verticalScrollbar)}"); } Scrollbar val3 = ModsTabBuilder.FindLaidOutScrollbar(val); context.Note("reference: " + ModsTabBuilder.SafeDescribe(val3)); (string, ScrollRect)[] array = new(string, ScrollRect)[2] { ("list", tab.ListScroll), ("settings", tab.ContentScroll) }; for (int j = 0; j < array.Length; j++) { (string, ScrollRect) tuple = array[j]; string item = tuple.Item1; Scrollbar verticalScrollbar = tuple.Item2.verticalScrollbar; if ((Object)(object)verticalScrollbar == (Object)null) { context.Note(item + ": no vertical scrollbar"); continue; } RectTransform val4 = ((Il2CppObjectBase)((Component)verticalScrollbar).transform).TryCast<RectTransform>(); context.Note(item + ": " + ModsTabBuilder.SafeDescribe(verticalScrollbar)); context.Note($"{item}: scrollbar anchors {val4.anchorMin}-{val4.anchorMax}, size {val4.sizeDelta}"); Assert.True(val4.anchorMax.y > val4.anchorMin.y, "The " + item + " scrollbar must stretch along the pane, not sit in a corner"); if ((Object)(object)val3 != (Object)null) { Image component = ((Component)verticalScrollbar).GetComponent<Image>(); Image component2 = ((Component)val3).GetComponent<Image>(); Image val5 = (((Object)(object)verticalScrollbar.handleRect == (Object)null) ? null : ((Component)verticalScrollbar.handleRect).GetComponent<Image>()); Image val6 = (((Object)(object)val3.handleRect == (Object)null) ? null : ((Component)val3.handleRect).GetComponent<Image>()); context.Note($"{item}: track {SpriteName(component)} like {SpriteName(component2)}, handle {SpriteName(val5)} like {SpriteName(val6)}"); Assert.Equal(SpriteName(component2), SpriteName(component), "The " + item + " scrollbar track looks like the game's"); Assert.Equal(SpriteName(val6), SpriteName(val5), "The " + item + " scrollbar handle looks like the game's"); Assert.Equal<Color>((Color)(((Object)(object)val6 == (Object)null) ? default(Color) : ((Graphic)val6).color), (Color)(((Object)(object)val5 == (Object)null) ? default(Color) : ((Graphic)val5).color), "The " + item + " scrollbar handle has the game's color"); } } } private static string SpriteName(Image image) { if (!((Object)(object)image == (Object)null)) { if (!((Object)(object)image.sprite == (Object)null)) { return ((Object)image.sprite).name; } return "no sprite"; } return "none"; } } public sealed class SessionOverrideRowTest : TestCase { public override string Suite => "Ui"; public override int Order => 15; public override IEnumerable<TestStep> Run(TestContext context) { using UiRowsSandbox sandbox = new UiRowsSandbox("UiOverride"); foreach (TestStep item in SettingsMenuFixture.SelectMod(context, sandbox.Settings)) { yield return item; } string language = UiText.LanguageCode; SliderRow row = SettingsMenuFixture.Row<SliderRow>("Difficulty"); try { SessionApplyResult val = SessionSettings.Instance.Apply((IReadOnlyList<SessionSettingValue>)(object)new SessionSettingValue[1] { new SessionSettingValue(sandbox.Settings.OwnerId, "Session", "Difficulty", "4") }); Assert.Equal(1, val.Applied, "Override applied"); EventSystem current = EventSystem.current; if (current != null) { current.SetSelectedGameObject(((Component)((SettingRow)row).Control).gameObject); } yield return Wait.Frames(3); ModsPanel controller = SettingsMenuFixture.Tab.Controller; EventSystem current2 = EventSystem.current; GameObject val2 = ((current2 != null) ? current2.currentSelectedGameObject : null); context.Note("Label: " + ((SettingRow)row).Label.text + ", context: " + controller.ContextLabel.text.Replace("\n", " | ")); Assert.Equal(4f, row.Slider.value, "The row must show the host value"); Assert.True((Object)(object)val2 != (Object)null && ((Il2CppObjectBase)val2).Pointer == ((Il2CppObjectBase)((Component)((SettingRow)row).Control).gameObject).Pointer, "An overridden row must stay selectable for gamepad players"); Assert.True(((SettingRow)row).Control.interactable, "An overridden row must stay in the navigation"); Assert.Equal(0.5f, ((SettingRow)row).ValueGroup.alpha, "An overridden value must be dimmed"); Assert.False(((SettingRow)row).ValueGroup.blocksRaycasts, "An overridden value must ignore the mouse"); Assert.Equal<string>("Difficulty " + UiText.Get("HostSuffix", language), ((SettingRow)row).Label.text, "Host suffix in the label"); Assert.True(controller.ContextLabel.text.Contains(UiText.Format("HostValue", language, new object[1] { "2" })), "The context must show the player's own value"); row.Slider.value = 5f; Assert.Equal(4f, row.Slider.value, "Moving an overridden slider must snap back to the host value"); yield return Wait.Seconds(0.45); Assert.Equal(2, sandbox.Difficulty.LocalValue, "An overridden row must never write the local value"); Assert.Equal(4, sandbox.Difficulty.Value, "The host value stays in effect"); SessionSettings.Instance.Clear(); yield return Wait.Frames(3); Assert.Equal(2f, row.Slider.value, "The row must show the local value after the session"); Assert.Equal(1f, ((SettingRow)row).ValueGroup.alpha, "The value must not be dimmed after the session"); Assert.True(((SettingRow)row).ValueGroup.blocksRaycasts, "The value must accept the mouse after the session"); Assert.Equal("Difficulty", ((SettingRow)row).Label.text, "Label without the host suffix"); } finally { SessionSettings.Instance.Clear(); EventSystem current3 = EventSystem.current; if (current3 != null) { current3.SetSelectedGameObject((GameObject)null); } } } } public sealed class SettingRowsTest : TestCase { public override string Suite => "Ui"; public override int Order => 6; public override IEnumerable<TestStep> Run(TestContext context) { using UiRowsSandbox sandbox = new UiRowsSandbox("UiRows"); foreach (TestStep item in SettingsMenuFixture.SelectMod(context, sandbox.Settings)) { yield return item; } ModsPanel controller = SettingsMenuFixture.Tab.Controller; IReadOnlyList<SettingRow> rows = controller.Rows; string languageCode = UiText.LanguageCode; context.Note("Rows: " + string.Join(", ", rows.Select((SettingRow row) => row.Setting.Key + "=" + ((object)row.Kind/*cast due to .constrained prefix*/).ToString()))); Assert.True(sandbox.Settings == controller.Selected, "The sandbox mod must be selected"); Assert.Equal(9, rows.Count, "Rows for visible settings"); Assert.Equal(5, controller.SectionHeaders.Count, "Section headers"); Assert.False(rows.Any((SettingRow row) => row.Setting.Key == "Secret"), "Hidden settings must not get a row"); Assert.Equal<ControlKind>((ControlKind)0, ((SettingRow)SettingsMenuFixture.Row<ToggleRow>("Enabled")).Kind, "Enabled"); Assert.Equal<ControlKind>((ControlKind)1, ((SettingRow)SettingsMenuFixture.Row<SliderRow>("Count")).Kind, "Count"); Assert.Equal<ControlKind>((ControlKind)1, ((SettingRow)SettingsMenuFixture.Row<SliderRow>("Speed")).Kind, "Speed"); Assert.Equal<ControlKind>((ControlKind)2, ((SettingRow)SettingsMenuFixture.Row<DropdownRow>("Mode")).Kind, "Mode"); Assert.Equal<ControlKind>((ControlKind)2, ((SettingRow)SettingsMenuFixture.Row<DropdownRow>("Color")).Kind, "Color"); Assert.Equal<ControlKind>((ControlKind)3, ((SettingRow)SettingsMenuFixture.Row<TextRow>("Name")).Kind, "Name"); Assert.Equal<ControlKind>((ControlKind)3, ((SettingRow)SettingsMenuFixture.Row<TextRow>("Seed")).Kind, "Seed"); Assert.Equal("Custom label", ((SettingRow)SettingsMenuFixture.Row<TextRow>("Name")).Label.text, "Custom label"); Assert.Equal("Seed", ((SettingRow)SettingsMenuFixture.Row<TextRow>("Seed")).Label.text, "Prettified label"); Assert.Equal<string>("Fast Mode " + UiText.Get("RestartSuffix", languageCode), ((SettingRow)SettingsMenuFixture.Row<ToggleRow>("FastMode")).Label.text, "Restart suffix"); Assert.Equal(3, SettingsMenuFixture.Row<DropdownRow>("Color").Dropdown.options.Count, "Color options"); Assert.Equal("5", SettingsMenuFixture.Row<SliderRow>("Count").DisplayedText, "Count display"); Assert.Equal("1.5", SettingsMenuFixture.Row<SliderRow>("Speed").DisplayedText, "Speed display"); Assert.Equal("Cat", SettingsMenuFixture.Row<TextRow>("Name").Input.text, "Name text"); } } public sealed class SettingsMenuCleanupTest : TestCase { public override string Suite => "Ui"; public override int Order => 99; public override IEnumerable<TestStep> Run(TestContext context) { SettingsMenuFixture.RestoreTab(); SettingsMenuFixture.CloseIfOpened(context); yield break; } } internal static class SettingsMenuFixture { public static bool OpenedByTests { get; private set; } public static TabInterface PreviousTab { get; private set; } public static MenuContext Context { get { if (Singleton<InterfaceManager>.HasInstance()) { return (MenuContext)1; } return (MenuContext)0; } } public static ModsTab Tab => ModsMenu.Get(Context); public static IEnumerable<TestStep> EnsureModsTab(TestContext context) { if (Tab == null) { if ((int)Context == 0 && Singleton<MainMenuInterfacesManager>.HasInstance()) { context.Note("The settings menu was not initialized yet, opening it"); Singleton<MainMenuInterfacesManager>.Instance.ShowSettingsMenu(); OpenedByTests = true; } else { context.Note("Open the settings menu from the pause menu before running UI tests in a level"); } yield return Wait.Until(() => Tab != null, 10.0, "the Mods tab to be injected into the " + ((object)Context/*cast due to .constrained prefix*/).ToString() + " settings menu"); } } public static IEnumerable<TestStep> ShowModsTab(TestContext context) { foreach (TestStep item in EnsureModsTab(context)) { yield return item; } OptionsInterface options = Tab.Options; if ((Object)(object)options._selectedTab == (Object)null || ((Il2CppObjectBase)options._selectedTab).Pointer != ((Il2CppObjectBase)Tab.Tab).Pointer) { if (PreviousTab == null) { PreviousTab = options._selectedTab ?? options._tabs[0]; } Tab.Tab.SetTabActive(true, true); yield return Wait.NextFrame(); } } public static IEnumerable<TestStep> SelectMod(TestContext context, CatSettings settings) { foreach (TestStep item in ShowModsTab(context)) { yield return item; } ModsPanel controller = Tab.Controller; yield return Wait.Until(() => HasItem(controller, settings), 3.0, "the mod to appear in the Mods list"); controller.Select(settings); yield return Wait.NextFrame(); } public static bool HasItem(ModsPanel controller, CatSettings settings) { foreach (ModListItem item in controller.Items) { if (item.Settings == settings) { return true; } } return false; } public static T Row<T>(string key) where T : SettingRow { foreach (SettingRow row in Tab.Controller.Rows) { if (row.Setting.Key == key) { T val = (T)(object)((row is T) ? row : null); if (val != null) { return val; } } } Assert.Fail($"No {typeof(T).Name} for {key} in the Mods panel"); return default(T); } public static void RestoreTab() { if ((Object)(object)PreviousTab != (Object)null && UiClone.IsAlive((Object)(object)PreviousTab)) { PreviousTab.SetTabActive(true, true); } PreviousTab = null; } public static void CloseIfOpened(TestContext context) { if (OpenedByTests) { OpenedByTests = false; if (Singleton<MainMenuInterfacesManager>.HasInstance()) { Singleton<MainMenuInterfacesManager>.Instance.GoBackToLastInterface(); context.Note("Closed the settings menu opened by the tests"); } } } } public sealed class SettingToMenuTest : TestCase { public override string Suite => "Ui"; public override int Order => 8; public override IEnumerable<TestStep> Run(TestContext context) { UiRowsSandbox sandbox = new UiRowsSandbox("UiRead"); try { foreach (TestStep item in SettingsMenuFixture.SelectMod(context, sandbox.Settings)) { yield return item; } string text = sandbox.Config.ContentWith("General", "Enabled", "false"); text = ConfigSandbox.ReplaceValue(text, "General", "Count", "3"); text = ConfigSandbox.ReplaceValue(text, "Mode", "Color", "Green"); text = ConfigSandbox.ReplaceValue(text, "Text", "Name", "Edited"); File.WriteAllText(sandbox.Config.FilePath, text); yield return Wait.Until(() => sandbox.Config.Reports.Count >= 1, 5.0, "the edited file to be reloaded"); yield return Wait.Frames(2); Assert.False(SettingsMenuFixture.Row<ToggleRow>("Enabled").Toggle.isOn, "Toggle after a file edit"); Assert.Equal(3f, SettingsMenuFixture.Row<SliderRow>("Count").Slider.value, "Slider after a file edit"); Assert.Equal("3", SettingsMenuFixture.Row<SliderRow>("Count").DisplayedText, "Slider text after a file edit"); Assert.Equal(1, SettingsMenuFixture.Row<DropdownRow>("Color").Dropdown.value, "Dropdown after a file edit"); Assert.Equal("Edited", SettingsMenuFixture.Row<TextRow>("Name").Input.text, "Text after a file edit"); sandbox.Count.Entry.Value = 9; yield return Wait.Frames(2); Assert.Equal(9f, SettingsMenuFixture.Row<SliderRow>("Count").Slider.value, "Slider after a change from code"); } finally { if (sandbox != null) { ((IDisposable)sandbox).Dispose(); } } } } public sealed class TabBarFitTest : TestCase { public override string Suite => "Ui"; public override int Order => 4; public override IEnumerable<TestStep> Run(TestContext context) { foreach (TestStep item in SettingsMenuFixture.EnsureModsTab(context)) { yield return item; } ModsTab modsTab = SettingsMenuFixture.Tab; yield return Wait.Until(() => modsTab.Fitter.IsFitted, 5.0, "the tab bar to be fitted"); Rect rect = ((Il2CppObjectBase)((Component)modsTab.Options).transform).TryCast<RectTransform>().rect; float num = default(float); float num2 = default(float); bool condition = modsTab.Fitter.Measure(ref num, ref num2); context.Note($"Screen {Screen.width}x{Screen.height}, canvas {((Rect)(ref rect)).width:0}x{((Rect)(ref rect)).height:0}"); context.Note($"Tab bar spans {num:0}..{num2:0} of {((Rect)(ref rect)).xMin:0}..{((Rect)(ref rect)).xMax:0}, scale {modsTab.Fitter.Scale:0.###}"); Assert.True(condition, "The tab bar must have measurable children"); Assert.True(num >= ((Rect)(ref rect)).xMin, "The left edge of the tab bar must stay on screen"); Assert.True(num2 <= ((Rect)(ref rect)).xMax - 20f + 1f, "The right edge of the tab bar, including the page hint, must stay on screen"); } } internal sealed class UiRowsSandbox : IDisposable { public const int VisibleCount = 9; public const int SectionCount = 5; public ConfigSandbox Config { get; } public CatSettings Settings => Config.Settings; public Setting<bool> Enabled { get; } public Setting<int> Count { get; } public Setting<float> Speed { get; } public Setting<SampleMode> Mode { get; } public Setting<string> Color { get; } public Setting<string> Name { get; } public Setting<int> Seed { get; } public Setting<string> Secret { get; } public Setting<bool> FastMode { get; } public Setting<int> Difficulty { get; } public UiRowsSandbox(string name) { Config = new ConfigSandbox(name); CatSettings settings = Config.Settings; Enabled = settings.Local<bool>("General", "Enabled", true, "Enabled.", (AcceptableValueBase)null); Count = settings.Local<int>("General", "Count", 5, "Count.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(0, 10)); Speed = settings.Local<float>("General", "Speed", 1.5f, "Speed.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 3f)); Mode = settings.Local<SampleMode>("Mode", "Mode", SampleMode.FastMode, "Mode.", (AcceptableValueBase)null); Color = settings.Local<string>("Mode", "Color", "Red", "Color.", (AcceptableValueBase)(object)new AcceptableValueList<string>(new string[3] { "Red", "Green", "Blue" })); Name = settings.Local<string>("Text", "Name", "Cat", "Name.", (AcceptableValueBase)null).Label("Custom label"); Seed = settings.Local<int>("Text", "Seed", 42, "Seed.", (AcceptableValueBase)null); Secret = settings.Local<string>("Text", "Secret", "hidden", "Secret.", (AcceptableValueBase)null).HiddenInMenu(); FastMode = settings.Local<bool>("Advanced", "FastMode", false, "Fast mode.", (AcceptableValueBase)null).RequiresRestart(); Difficulty = settings.Session<int>("Session", "Difficulty", 2, "Difficulty.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(1, 5)); } public void Dispose() { Config.Dispose(); } } public sealed class ViewportMaskTest : TestCase { public override string Suite => "Ui"; public override int Order => 16; public override IEnumerable<TestStep> Run(TestContext context) { foreach (TestStep item3 in SettingsMenuFixture.EnsureModsTab(context)) { yield return item3; } ModsTab tab = SettingsMenuFixture.Tab; (string, ScrollRect)[] array = new(string, ScrollRect)[2] { ("list", tab.ListScroll), ("settings", tab.ContentScroll) }; for (int i = 0; i < array.Length; i++) { (string, ScrollRect) tuple = array[i]; string item = tuple.Item1; ScrollRect item2 = tuple.Item2; RectTransform val = ModsTabBuilder.ViewportOf(item2); Assert.NotNull(val, "Viewport of the " + item + " pane"); Mask component = ((Component)val).GetComponent<Mask>(); Image component2 = ((Component)val).GetComponent<Image>(); context.Note($"{item}: mask {(((Object)(object)component == (Object)null) ? "missing" : (((Behaviour)component).enabled ? "enabled" : "disabled"))}, graphic {(((Object)(object)component2 == (Object)null) ? "missing" : (((Behaviour)component2).enabled ? "enabled" : "disabled"))}"); Assert.True((Object)(object)component != (Object)null && ((Behaviour)component).enabled, "The " + item + " pane must have an enabled mask"); Assert.True((Object)(object)component2 != (Object)null && ((Behaviour)component2).enabled, "The mask graphic of the " + item + " pane must be enabled, otherwise nothing is clipped"); Assert.True(((Transform)item2.content).IsChildOf((Transform)(object)val), "The " + item + " content must be inside the masked viewport"); } } } } namespace CatLib.Tests.Suites.ShelfLabels { public sealed class LabelBoardTest : TestCase { public override string Suite => "ShelfLabels"; public override IEnumerable<TestStep> Run(TestContext context) { LabelSlot val = default(LabelSlot); Assert.True(LabelSlot.TryParse("390/2", ref val) && val == new LabelSlot(390, 2), "Slot text"); LabelSlot val2 = default(LabelSlot); Assert.False(LabelSlot.TryParse("390/4", ref val2), "At most three extra labels"); Assert.False(LabelSlot.TryParse("390/0", ref val2), "Slot 0 is the game's own label and is never addressed"); Assert.False(LabelSlot.TryParse("-1/1", ref val2), "Negative ids are refused"); Assert.False(LabelSlot.TryParse("390/1/2", ref val2), "Extra parts are refused"); Assert.Equal("label/390/2", ((LabelSlot)(ref val)).SaveKey, "Save key"); LabelSlot val3 = default(LabelSlot); Assert.True(LabelSlot.TryParseSaveKey("label/390/2", ref val3) && val3 == val, "Save key round trip"); Assert.False(LabelSlot.TryParseSaveKey("other/390/2", ref val2), "Foreign keys are skipped"); LabelBoard val4 = new LabelBoard(); List<IReadOnlyList<LabelSlot>> list = new List<IReadOnlyList<LabelSlot>>(); val4.Changed += list.Add; Assert.Equal(0, val4.Get(val), "An unknown slot shows the empty picture"); Assert.Equal(1, val4.Step(val, 1, 10), "Forward"); Assert.Equal(0, val4.Step(val, -1, 10), "Back"); Assert.Equal(9, val4.Step(val, -1, 10), "Back from the empty picture wraps to the last one"); Assert.Equal(0, val4.Step(val, 1, 10), "Forward from the last one wraps to the empty picture"); Assert.Equal(-1, val4.Step(val, 1, 0), "Unknown picture count changes nothing"); Assert.Equal(4, list.Count, "Every change is reported"); Assert.False(val4.Set(val, 0), "Setting the same value is not a change"); Assert.False(val4.Set(val, 300), "Out of range values are refused"); Assert.True(val4.Has(val), "A slot set back to empty is still stored"); val4.Set(new LabelSlot(12, 3), 7); val4.Set(new LabelSlot(390, 1), 2); string text = LabelBoard.Format((IEnumerable<KeyValuePair<LabelSlot, int>>)val4.Entries); Assert.Equal("12/3=7;390/1=2;390/2=0", text, "Format is sorted"); List<KeyValuePair<LabelSlot, int>> list2 = LabelBoard.Parse(text + ";garbage;390/9=1;5/1=x;5/1=999"); Assert.Equal(3, list2.Count, "Malformed entries are skipped"); LabelBoard val5 = new LabelBoard(); val5.Apply(Enumerable.Range(1, 900).SelectMany((int id) => from number in Enumerable.Range(1, 3) select new KeyValuePair<LabelSlot, int>(new LabelSlot(id, number), id % 10))); List<string> list3 = val5.Chunks(12288); Assert.True(list3.Count > 1, "A large board is split into several messages"); Assert.True(list3.All((string chunk) => Encoding.UTF8.GetByteCount(chunk) <= 12288), "Every chunk fits"); Assert.Equal(2700, list3.Sum((string chunk) => LabelBoard.Parse(chunk).Count), "No entry is lost"); val4.Clear(); Assert.Equal(0, val4.Count, "Clear"); yield break; } } public sealed class LabelSaveTest : TestCase { public override string Suite => "ShelfLabels"; public override IEnumerable<TestStep> Run(TestContext context) { using (SaveSandbox saveSandbox = new SaveSandbox("ShelfLabels")) { LabelBoard val = new LabelBoard(); PlacementBoard val2 = new PlacementBoard(); StandBoard val3 = new StandBoard(); new LabelStore(saveSandbox.Session.Register("catlib.shelflabels", "0.1.0", 1), val, val2, val3, (CatLogger)null); saveSandbox.Session.Select("GameSave_20260101_120000_Cat-Mail-Co.bin", (bool?)false); val3.Set(390, true); val3.Set(12, false); val2.Set(390, (Placement)4); val2.Set(12, (Placement)3); val.Set(new LabelSlot(390, 1), 2); val.Set(new LabelSlot(390, 2), 5); val.Set(new LabelSlot(390, 3), 8); saveSandbox.Save(); Assert.True(File.ReadAllText(saveSandbox.PathOf("catlib.shelflabels")).Contains("label/390/3"), "Pictures are written with the game save"); LabelBoard val4 = new LabelBoard(); PlacementBoard val5 = new PlacementBoard(); StandBoard val6 = new StandBoard(); new LabelStore(saveSandbox.Restart().Register("catlib.shelflabels", "0.1.0", 1), val4, val5, val6, (CatLogger)null); saveSandbox.Session.Select("GameSave_20260101_120000_Cat-Mail-Co.bin", (bool?)false); Assert.Equal(8, val4.Get(new LabelSlot(390, 3)), "Pictures come back after a restart"); Assert.Equal<Placement>((Placement)4, val5.Get(390, (Placement)0), "A shelf without copies stays without copies"); Assert.Equal<Placement>((Placement)3, val5.Get(12, (Placement)0), "Placements come back after a restart"); Assert.True(val6.IsHidden(390, false), "A hidden stand stays hidden"); Assert.False(val6.IsHidden(12, true), "An explicitly shown stand stays shown even when the default hides stands"); Assert.Equal(8, val4.Get(new LabelSlot(390, 3)), "Pictures of a shelf without copies are still kept"); Assert.Equal(3, val4.Count, "All of them"); val4.Set(new LabelSlot(390, 1), 3); saveSandbox.Save(); LabelBoard val7 = new LabelBoard(); PlacementBoard val8 = new PlacementBoard(); new LabelStore(saveSandbox.Restart().Register("catlib.shelflabels", "0.1.0", 1), val7, val8, new StandBoard(), (CatLogger)null); saveSandbox.Session.Select("GameSave_20260101_120000_Cat-Mail-Co.bin", (bool?)false); Assert.Equal(5, val7.Get(new LabelSlot(390, 2)), "Slots hidden in the meantime keep their pictures across saves"); Assert.Equal(8, val7.Get(new LabelSlot(390, 3)), "and come back when they are shown again"); Assert.Equal(3, val7.Get(new LabelSlot(390, 1)), "while the visible slot has its new picture"); saveSandbox.Session.Select("GameSave_Other.bin", (bool?)false); Assert.Equal(0, val7.Count, "Another save starts without these pictures"); Assert.Equal(0, val8.Count, "and without these placements"); } yield break; } } public sealed class LabelSyncTest : TestCase { public override string Suite => "ShelfLabels"; public override IEnumerable<TestStep> Run(TestContext context) { ModInfo val = NetworkFixture.Mod("catlib.shelflabels", "0.1.0", (SessionPolicy)1, (VersionRule)2); ModMessagingFixture modMessagingFixture = new ModMessagingFixture(NetworkFixture.Identity(val), NetworkFixture.Identity(val)); LabelBoard val2 = new LabelBoard(); LabelBoard val3 = new LabelBoard(); PlacementBoard val4 = new PlacementBoard(); PlacementBoard val5 = new PlacementBoard(); StandBoard val6 = new StandBoard(); StandBoard val7 = new StandBoard(); bool hideStands = false; int slots = 2; Placement defaultPlacement = (Placement)0; LabelSync val8 = new LabelSync(modMessagingFixture.HostMessenger.Channel("catlib.shelflabels"), val2, val4, val6, (Func<int, int>)((int id) => (id == 390 || id == 12) ? 10 : 0), (Func<int>)(() => slots), (Func<Placement>)(() => defaultPlacement), (Func<bool>)(() => hideStands), (CatLogger)null); LabelSync val9 = new LabelSync(modMessagingFix