diff --git a/CHANGELOG.md b/CHANGELOG.md index 59292a6..23ffb31 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,22 @@ # Changelog +## 2.10.8 + +**Stabilität – weniger Hänger** +- Senden läuft jetzt in einem eigenen Thread: vorher schrieb das Spiel selbst ins Netzwerk und stand still, sobald die + Leitung kurz voll war (Spielstand hochladen, schwaches WLAN). Kleine Pakete werden gebündelt +- Speichern für den Server (friert das Spiel einige Sekunden ein): bei Routine-Speicherungen wartet das Spiel auf einen + ruhigen Moment (nicht in schneller Fahrt) und zeigt vorher „Saving the world for the server …“. Zusammen mit Server + 2.10.5 wird viel seltener gespeichert (beim Beitreten nur, wenn der letzte Stand älter als 15 Minuten ist, sonst + alle 30 statt 10 Minuten) +- Eigene dynamische Schrift für alle MSCO-Anzeigen (Namensschilder, HUD, Menüs): vorher schrieb Unity hunderte + Warnungen ins Log – mit Rucklern +- Admin-Menü: der Reiter VEHICLE durchsucht das Spiel nur noch alle 30 s bzw. nach einer Aktion (vorher alle 5 s – + spürbarer Ruckler bei offenem Menü); Uhrzeit/Wetter suchen ihre FSMs seltener +- Weniger Speichermüll pro Nachricht (wiederverwendete Puffer) – der Garbage Collector läuft seltener +- Hänger-Meldungen im Log sagen jetzt auch, wenn der Garbage Collector oder das Speichern die Ursache war, und wie + viele Hänger es seit der letzten Meldung gab + ## 2.10.7 **Sicherheit** diff --git a/LobbyUI.cs b/LobbyUI.cs index d6e8366..3e342a2 100644 --- a/LobbyUI.cs +++ b/LobbyUI.cs @@ -1178,6 +1178,7 @@ namespace MSCO static GUIStyle Label(int size, Color color, FontStyle font) { var s = new GUIStyle(GUI.skin.label); + if (UiTheme.DynamicFont != null) s.font = UiTheme.DynamicFont; s.fontSize = size; s.fontStyle = font; s.normal.textColor = color; @@ -1188,6 +1189,7 @@ namespace MSCO static GUIStyle Button(Color normal, Color hover, Color text, int size, FontStyle font, int radius = UiTheme.Radius, Color border = default(Color)) { var s = new GUIStyle(GUI.skin.button); + if (UiTheme.DynamicFont != null) s.font = UiTheme.DynamicFont; bool withBorder = border.a > 0f; int h = radius * 2 + 4; Texture2D n = UiTheme.RoundedEx(normal, normal, border, withBorder, radius, h); diff --git a/MSCO.cs b/MSCO.cs index b8e805a..33c4a58 100644 --- a/MSCO.cs +++ b/MSCO.cs @@ -11,7 +11,7 @@ namespace MSCO /// Version der Mod. Kleine Updates: nur die dritte Stelle erhöhen (2.9.1, 2.9.2 ...) - sie bleiben mit /// Servern und Mitspielern derselben ersten zwei Stellen kompatibel. /// - public const string ModVersion = "2.10.7"; + public const string ModVersion = "2.10.8"; /// /// Netzwerk-Version (die ersten beiden Stellen). Nur erhöhen, wenn sich das Protokoll inkompatibel ändert - dann /// auch GameServer.ModVersion im Server. diff --git a/MultiplayerManager.cs b/MultiplayerManager.cs index ab5ef6d..6b825a8 100644 --- a/MultiplayerManager.cs +++ b/MultiplayerManager.cs @@ -1635,7 +1635,12 @@ namespace MSCO case MsgType.SaveRequest: // Server möchte den aktuellen Spielstand (für neue Spieler / zum Sichern) - StartCoroutine(UploadSave(r.ReadInt32())); + { + int requestId = r.ReadInt32(); + // Neuere Server sagen, ob es eilt (jemand tritt bei) - sonst darf auf einen ruhigen Moment gewartet werden + bool urgent = r.BaseStream.Position >= r.BaseStream.Length || r.ReadBoolean(); + StartCoroutine(UploadSave(requestId, urgent)); + } break; case MsgType.Reconnect: @@ -2290,19 +2295,34 @@ namespace MSCO } /// Zu Beginn jedes Bilds: war das letzte Bild ein Hänger? Dann Ursache melden (höchstens alle 20 s). + int lastGcCount = -1; + int hitchesSinceReport; + void CheckHitch() { float dt = Time.unscaledDeltaTime; - if (dt > HitchSeconds && MenuHelper.InGame && Time.realtimeSinceStartup >= nextHitchReport) + // Lief in diesem Frame der Garbage Collector? (hält bei großem Speicher das ganze Spiel an) + int gc = System.GC.CollectionCount(0); + bool gcRan = lastGcCount >= 0 && gc != lastGcCount; + lastGcCount = gc; + if (dt > HitchSeconds && MenuHelper.InGame) { - nextHitchReport = Time.realtimeSinceStartup + 20f; - int total = Mathf.RoundToInt(dt * 1000f); - if (frameMsco > dt * 1000f * 0.4f) - MscoLog.Warning("[MSCO] Hitch " + total + " ms - caused by MSCO: " + slowestPart + " " + slowestMs.ToString("0") + - " ms (MSCO total " + frameMsco.ToString("0") + " ms)"); - else - MscoLog.Info("[MSCO] Hitch " + total + " ms - not MSCO (MSCO used " + frameMsco.ToString("0") + " ms)" + - (Time.realtimeSinceStartup - lastSaveTriggered < 5f ? " - the game was saving" : " - the game itself (loading, saving, physics)")); + hitchesSinceReport++; + if (Time.realtimeSinceStartup >= nextHitchReport) + { + nextHitchReport = Time.realtimeSinceStartup + 20f; + int total = Mathf.RoundToInt(dt * 1000f); + string more = hitchesSinceReport > 1 ? " [" + hitchesSinceReport + " hitches since the last report]" : ""; + hitchesSinceReport = 0; + // Speichern friert genau den Frame ein, in dem es ausgelöst wurde - also auch so weit zurück schauen + bool saving = Time.realtimeSinceStartup - lastSaveTriggered < dt + 5f; + if (frameMsco > dt * 1000f * 0.4f) + MscoLog.Warning("[MSCO] Hitch " + total + " ms - caused by MSCO: " + slowestPart + " " + slowestMs.ToString("0") + + " ms (MSCO total " + frameMsco.ToString("0") + " ms)" + (gcRan ? ", garbage collection ran" : "") + more); + else + MscoLog.Info("[MSCO] Hitch " + total + " ms - not MSCO (MSCO used " + frameMsco.ToString("0") + " ms)" + + (saving ? " - the game was saving" : gcRan ? " - garbage collection (memory cleanup)" : " - the game itself (loading, physics)") + more); + } } frameMsco = 0; slowestMs = 0; @@ -2490,9 +2510,30 @@ namespace MSCO /// genau die aktuelle Welt - Schrauben, Teile, alles - statt eines alten Stands mit nachgespielten Aktionen. /// Der eigene Spielstand ist davon nicht betroffen: er liegt gesichert im Backup und kommt im Hauptmenü zurück. /// - System.Collections.IEnumerator UploadSave(int requestId) + System.Collections.IEnumerator UploadSave(int requestId, bool urgent) { if (!MenuHelper.InGame || !dedicated || session == null) yield break; + // Speichern lässt das Spiel einige Sekunden hängen: bei einer Routine-Anfrage nicht mitten in schneller Fahrt, + // sondern einen ruhigen Moment abwarten (höchstens 20 s - der Server wartet länger) + if (!urgent) + { + float giveUp = Time.realtimeSinceStartup + 20f; + while (Time.realtimeSinceStartup < giveUp && localPlayer != null && session != null) + { + Vector3 from = localPlayer.position; + float t0 = Time.realtimeSinceStartup; + while (Time.realtimeSinceStartup < t0 + 0.5f) yield return null; + if (localPlayer == null) break; + float speed = (localPlayer.position - from).magnitude / Mathf.Max(0.05f, Time.realtimeSinceStartup - t0); + if (speed < 3f) break; + } + if (session == null || !MenuHelper.InGame) yield break; + } + // Erst den Hinweis zeigen, dann speichern (sonst friert das Bild ein, bevor man ihn sieht) + centerMessage = "Saving the world for the server ..."; + centerMessageUntil = Time.realtimeSinceStartup + 4f; + yield return null; + yield return null; lastSaveTriggered = Time.realtimeSinceStartup; PlayMakerFSM.BroadcastEvent("SAVEGAME"); float until = Time.realtimeSinceStartup + 3f; // das Spiel schreibt die Dateien über mehrere Frames @@ -2630,10 +2671,21 @@ namespace MSCO void OnGUI() { PartStart(); + // Eigene dynamische Schrift (siehe UiTheme.DynamicFont); danach wieder die bisherige + GUISkin skin = GUI.skin; + Font previousFont = skin.font; + Font font = UiTheme.DynamicFont; + if (font != null) skin.font = font; try { DrawGui(); } - finally { PartEnd("interface (OnGUI)"); } + finally + { + skin.font = previousFont; + PartEnd("interface (OnGUI)"); + } } + string centerMessageSource, centerMessageUpper; // Großschreibung nur einmal pro Meldung (nicht jedes Frame) + void DrawGui() { InputLock.KeepCursorFree(); @@ -2652,7 +2704,8 @@ namespace MSCO { int oldSize = savingStyle.fontSize; savingStyle.fontSize = 30; - GUI.Label(new Rect(0, Screen.height / 2f - 40, Screen.width, 80), centerMessage.ToUpper(), savingStyle); + if (!ReferenceEquals(centerMessage, centerMessageSource)) { centerMessageSource = centerMessage; centerMessageUpper = centerMessage.ToUpper(); } + GUI.Label(new Rect(0, Screen.height / 2f - 40, Screen.width, 80), centerMessageUpper, savingStyle); savingStyle.fontSize = oldSize; } diff --git a/NetSession.cs b/NetSession.cs index b5aee3f..2bd018d 100644 --- a/NetSession.cs +++ b/NetSession.cs @@ -200,7 +200,8 @@ namespace MSCO { if (payload == null) payload = new byte[0]; var frame = new byte[5 + payload.Length]; - BitConverter.GetBytes(payload.Length + 1).CopyTo(frame, 0); + int length = payload.Length + 1; // wie BitConverter (Little Endian), ohne zusätzliches Array + frame[0] = (byte)length; frame[1] = (byte)(length >> 8); frame[2] = (byte)(length >> 16); frame[3] = (byte)(length >> 24); frame[4] = (byte)type; Buffer.BlockCopy(payload, 0, frame, 5, payload.Length); return frame; @@ -215,16 +216,30 @@ namespace MSCO } } - /// TCP-Verbindung (LAN / IP). Lesen ist nicht-blockierend und wird über Poll() abgefragt. + /// + /// TCP-Verbindung (LAN / IP). Lesen ist nicht-blockierend und wird über Poll() abgefragt. Senden läuft über eine + /// Warteschlange und einen eigenen Thread: vorher schrieb das Spiel selbst (blockierend) - war die Leitung kurz voll + /// (Spielstand hochladen, schwaches WLAN), stand das ganze Spiel. Kleine Pakete werden dabei zusammengefasst. + /// public class Connection : IConnection { - public bool IsClosed { get; private set; } - public string CloseReason { get; private set; } + volatile bool closed; + volatile string closeReason; + public bool IsClosed { get { return closed; } } + public string CloseReason { get { return closeReason; } } + + // Mehr als das unterwegs = die Gegenseite nimmt nichts mehr an + const long MaxQueuedBytes = 96L * 1024 * 1024; + const int MaxBatchBytes = 256 * 1024; readonly TcpClient client; readonly NetworkStream stream; readonly FrameParser parser = new FrameParser(); readonly byte[] readBuffer = new byte[64 * 1024]; + readonly Queue outgoing = new Queue(); + readonly object gate = new object(); + long queuedBytes; + readonly System.Threading.Thread writer; public Connection(TcpClient client) { @@ -232,11 +247,46 @@ namespace MSCO client.NoDelay = true; client.SendBufferSize = 4 * 1024 * 1024; stream = client.GetStream(); + writer = new System.Threading.Thread(WriteLoop) { IsBackground = true, Name = "MSCO send" }; + writer.Start(); } + /// Sende-Thread: wartet auf Pakete und schreibt sie (gebündelt) in die Verbindung. + void WriteLoop() + { + var batch = new MemoryStream(MaxBatchBytes); + try + { + while (true) + { + batch.SetLength(0); + lock (gate) + { + while (outgoing.Count == 0 && !closed) System.Threading.Monitor.Wait(gate); + if (outgoing.Count == 0) return; // geschlossen und nichts mehr zu senden + // Mehrere kleine Pakete in einem Rutsch (große einzeln) + do + { + byte[] frame = outgoing.Dequeue(); + queuedBytes -= frame.Length; + batch.Write(frame, 0, frame.Length); + } while (outgoing.Count > 0 && batch.Length + outgoing.Peek().Length <= MaxBatchBytes); + System.Threading.Monitor.PulseAll(gate); // Close() wartet evtl. aufs Leerwerden + } + stream.Write(batch.GetBuffer(), 0, (int)batch.Length); + } + } + catch (Exception e) + { + if (!closed) Close("Network error: " + e.Message); + } + } + + readonly List polled = new List(); // wiederverwendet: der Aufrufer geht sie sofort durch public List Poll() { - var messages = new List(); + var messages = polled; + messages.Clear(); if (IsClosed) return messages; try { @@ -263,23 +313,37 @@ namespace MSCO public void Send(MsgType type, byte[] payload) { - if (IsClosed) return; - try + if (closed) return; + byte[] frame = FrameParser.Frame(type, payload); + bool tooMuch; + lock (gate) { - byte[] frame = FrameParser.Frame(type, payload); - stream.Write(frame, 0, frame.Length); // blockierend, damit große Dateien komplett rausgehen - } - catch (Exception e) - { - Close("Network error: " + e.Message); + tooMuch = queuedBytes + frame.Length > MaxQueuedBytes; + if (!tooMuch) + { + outgoing.Enqueue(frame); + queuedBytes += frame.Length; + System.Threading.Monitor.Pulse(gate); + } } + if (tooMuch) Close("The connection is too slow - too much data waiting to be sent"); } public void Close(string reason) { - if (IsClosed) return; - IsClosed = true; - CloseReason = reason; + lock (gate) + { + if (closed) return; + // Was schon in der Warteschlange liegt (z.B. eine Abschiedsnachricht), kurz noch rausgehen lassen + var until = DateTime.UtcNow.AddMilliseconds(300); + while (outgoing.Count > 0 && DateTime.UtcNow < until && System.Threading.Thread.CurrentThread != writer) + System.Threading.Monitor.Wait(gate, 50); + closed = true; + closeReason = reason; + outgoing.Clear(); + queuedBytes = 0; + System.Threading.Monitor.PulseAll(gate); + } try { client.Close(); } catch { } } } @@ -313,9 +377,11 @@ namespace MSCO lastReceive = Time.realtimeSinceStartup; } + readonly List polled = new List(); // wiederverwendet: der Aufrufer geht sie sofort durch public List Poll() { - var messages = new List(); + var messages = polled; + messages.Clear(); if (IsClosed) return messages; try { parser.Parse(messages); } catch (Exception e) { Close("Network error: " + e.Message); } @@ -419,9 +485,11 @@ namespace MSCO return new NetSession { IsSteam = true, server = new SteamConnection(host), pendingConnected = true }; } + readonly List polledEvents = new List(); // wiederverwendet (jedes Frame) public List Poll() { - var events = new List(); + var events = polledEvents; + events.Clear(); if (IsClosed) return events; try { @@ -568,15 +636,41 @@ namespace MSCO try { if (listener != null) listener.Stop(); } catch { } } + // Puffer zum Bauen von Nachrichten pro Thread wiederverwenden (vorher pro Nachricht neuer Speicherstrom + Schreiber: + // bei vielen Nachrichten pro Sekunde viel Speichermüll -> häufiger Garbage Collector -> Ruckler) + [ThreadStatic] static MemoryStream buildStream; + [ThreadStatic] static BinaryWriter buildWriter; + [ThreadStatic] static bool building; + public static byte[] Build(Action write) { - using (var ms = new MemoryStream()) - using (var w = new BinaryWriter(ms)) + if (building) { - write(w); - w.Flush(); - return ms.ToArray(); + // Build innerhalb von Build: eigener Puffer + using (var ms = new MemoryStream()) + using (var w = new BinaryWriter(ms)) + { + write(w); + w.Flush(); + return ms.ToArray(); + } } + if (buildStream == null) + { + buildStream = new MemoryStream(4096); + buildWriter = new BinaryWriter(buildStream); + } + building = true; + try + { + buildStream.SetLength(0); + write(buildWriter); + buildWriter.Flush(); + byte[] result = buildStream.ToArray(); + if (buildStream.Capacity > 1024 * 1024) { buildStream = null; buildWriter = null; } // große Dateien nicht ewig im Speicher halten + return result; + } + finally { building = false; } } /// Stellt der Nachricht die Absender-ID voran (für weitergeleitete Nachrichten). diff --git a/UiTheme.cs b/UiTheme.cs index e2c58d9..5ad6c6c 100644 --- a/UiTheme.cs +++ b/UiTheme.cs @@ -10,6 +10,26 @@ namespace MSCO /// public static class UiTheme { + static Font dynamicFont; + static bool fontSearched; + + /// + /// Dynamische Schrift (eingebautes Arial) für alle MSCO-Oberflächen: Schriftgröße/Fett gehen nur mit dynamischen + /// Schriften - sonst schreibt Unity jedes Mal eine Warnung ins Log (400-mal in einer Sitzung, mit Rucklern). + /// + public static Font DynamicFont + { + get + { + if (!fontSearched) + { + fontSearched = true; + try { dynamicFont = Resources.GetBuiltinResource(typeof(Font), "Arial.ttf") as Font; } catch { } + if (dynamicFont != null && !dynamicFont.dynamic) dynamicFont = null; + } + return dynamicFont; + } + } public static readonly Color Background = Hex(0x16, 0x16, 0x1a, 0.97f); public static readonly Color Panel = Hex(0x0f, 0x0f, 0x12, 0.98f); public static readonly Color Row = Hex(0x22, 0x22, 0x28); @@ -170,6 +190,7 @@ namespace MSCO public static GUIStyle Button(Color normal, Color hover, Color text, int fontSize = 13, bool bold = true) { var s = new GUIStyle(GUI.skin.button); + if (DynamicFont != null) s.font = DynamicFont; s.normal.background = Rounded(normal); s.hover.background = Rounded(hover); s.active.background = Rounded(hover); @@ -186,6 +207,7 @@ namespace MSCO public static GUIStyle Field(int fontSize = 14) { var s = new GUIStyle(GUI.skin.textField); + if (DynamicFont != null) s.font = DynamicFont; s.normal.background = Rounded(FieldColor); s.hover.background = Rounded(FieldFocus); s.focused.background = Rounded(FieldFocus); @@ -200,6 +222,7 @@ namespace MSCO public static GUIStyle Label(int fontSize, Color color, FontStyle font = FontStyle.Normal, TextAnchor align = TextAnchor.UpperLeft) { var s = new GUIStyle(GUI.skin.label); + if (DynamicFont != null) s.font = DynamicFont; s.fontSize = fontSize; s.fontStyle = font; s.normal.textColor = color; diff --git a/VehicleAdmin.cs b/VehicleAdmin.cs index 206a404..6813fdc 100644 --- a/VehicleAdmin.cs +++ b/VehicleAdmin.cs @@ -61,10 +61,13 @@ namespace MSCO new[] { "Radiator", "Water", "Coolant", "6" }, }; - /// Database neu einlesen (höchstens alle 5 s, außer erzwungen). + /// + /// Database neu einlesen (höchstens alle 30 s, außer erzwungen - nach jeder Aktion). Das Durchsuchen aller FSMs + /// dauert spürbar (Ruckler bei offenem Admin-Menü); die angezeigten Werte selbst sind ohnehin live. + /// public static void Refresh(bool force) { - if (!force && Time.realtimeSinceStartup - scannedAt < 5f) return; + if (!force && Time.realtimeSinceStartup - scannedAt < 30f) return; scannedAt = Time.realtimeSinceStartup; fluids.Clear(); parts.Clear(); diff --git a/WorldAdmin.cs b/WorldAdmin.cs index eb692c7..7645bee 100644 --- a/WorldAdmin.cs +++ b/WorldAdmin.cs @@ -28,9 +28,9 @@ namespace MSCO static void Find() { if (sun != null && weather != null) return; - if (Time.realtimeSinceStartup - searchedAt < 3f) return; + if (Time.realtimeSinceStartup - searchedAt < 15f) return; // nicht ständig suchen, falls es sie nicht gibt searchedAt = Time.realtimeSinceStartup; - foreach (PlayMakerFSM f in SceneCache.Fsms(5f)) + foreach (PlayMakerFSM f in SceneCache.Fsms(60f)) { if (f == null) continue; if (sun == null && f.FsmVariables.FindFsmInt("Time") != null && f.Fsm.GetState("12-14") != null diff --git a/examples/ShowBoltSize/ShowBoltSize.cs b/examples/ShowBoltSize/ShowBoltSize.cs index 9504df0..31dc5e8 100644 --- a/examples/ShowBoltSize/ShowBoltSize.cs +++ b/examples/ShowBoltSize/ShowBoltSize.cs @@ -7,9 +7,9 @@ using UnityEngine; [assembly: AssemblyTitle("Show Bolt Size")] [assembly: AssemblyProduct("Show Bolt Size")] [assembly: AssemblyDescription("Shows the size of the bolt you look at, so you pick the right wrench.")] -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] -[assembly: AssemblyInformationalVersion("1.0.0")] +[assembly: AssemblyVersion("1.0.1.0")] +[assembly: AssemblyFileVersion("1.0.1.0")] +[assembly: AssemblyInformationalVersion("1.0.1")] namespace ShowBoltSize { @@ -22,12 +22,12 @@ namespace ShowBoltSize { public override string ID { get { return "showboltsize"; } } public override string Name { get { return "Show Bolt Size"; } } - public override string Version { get { return "1.0.0"; } } + public override string Version { get { return "1.0.1"; } } public override string Author { get { return "MBM"; } } const float MaxDistance = 1.3f; const float BoltRadius = 0.025f; // so groß wirkt eine Schraube beim Zielen - const float ScanInterval = 2f; + const float ScanInterval = 5f; sealed class Bolt { @@ -41,28 +41,47 @@ namespace ShowBoltSize float nextScan; string label; GUIStyle style; + Camera camera; + bool reported; - public override void OnMenuLoad() { inGame = false; bolts.Clear(); label = null; } + public override void OnMenuLoad() { inGame = false; bolts.Clear(); label = null; camera = null; reported = false; } public override void OnGameLoad() { inGame = true; nextScan = 0f; } + /// + /// Spielerkamera: Camera.main gibt es in My Summer Car nicht immer (die Kamera des Spielers ist "FPSCamera"). + /// + Camera FindCamera() + { + if (camera != null && camera.enabled && camera.gameObject.activeInHierarchy) return camera; + camera = null; + foreach (Camera c in Camera.allCameras) + if (c.name == "FPSCamera") { camera = c; break; } + if (camera == null) camera = Camera.main; + return camera; + } + public override void Update() { label = null; if (!inGame) return; - Camera cam = Camera.main; + Camera cam = FindCamera(); if (cam == null) return; - if (Time.realtimeSinceStartup >= nextScan) Scan(); + if (Time.realtimeSinceStartup >= nextScan) Scan(cam); Transform eye = cam.transform; + Vector3 eyePos = eye.position, forward = eye.forward; Bolt best = null; float bestAngle = float.MaxValue; - foreach (Bolt b in bolts) + for (int i = 0; i < bolts.Count; i++) { - if (b.Transform == null || !b.Transform.gameObject.activeInHierarchy) continue; - Vector3 to = b.Transform.position - eye.position; - float distance = to.magnitude; - if (distance > MaxDistance || distance < 0.05f) continue; - float angle = Vector3.Angle(eye.forward, to); + Bolt b = bolts[i]; + if (b.Transform == null) continue; + Vector3 to = b.Transform.position - eyePos; + float sq = to.sqrMagnitude; + if (sq > MaxDistance * MaxDistance || sq < 0.0025f) continue; + if (!b.Transform.gameObject.activeInHierarchy) continue; + float distance = Mathf.Sqrt(sq); + float angle = Vector3.Angle(forward, to); float limit = Mathf.Atan2(BoltRadius, distance) * Mathf.Rad2Deg + 0.5f; if (angle < limit && angle < bestAngle) { @@ -73,11 +92,15 @@ namespace ShowBoltSize if (best != null) label = Describe(best); } - void Scan() + /// + /// Schrauben aus MSCOs gemeinsamer Liste aller FSMs holen (SceneCache) - nicht selbst das ganze Spiel durchsuchen, + /// das ließ das Spiel alle paar Sekunden kurz hängen. + /// + void Scan(Camera cam) { nextScan = Time.realtimeSinceStartup + ScanInterval; bolts.Clear(); - foreach (PlayMakerFSM fsm in Object.FindObjectsOfType()) + foreach (PlayMakerFSM fsm in SceneCache.Fsms(30f)) { if (fsm == null || fsm.FsmName != "Screw") continue; FsmFloat f = fsm.FsmVariables.FindFsmFloat("BoltSize"); @@ -85,6 +108,11 @@ namespace ShowBoltSize if (f == null && i == null) continue; bolts.Add(new Bolt { Transform = fsm.transform, SizeF = f, SizeI = i }); } + if (!reported) + { + reported = true; + Log("camera " + cam.name + ", " + bolts.Count + " bolts with a size"); + } } /// Größe in mm. Das Spiel speichert sie je nach Schraube als 10 oder als 1.0 (= 10 mm).