2.10.8: fewer hitches

- Sending on a background thread (the game thread blocked whenever the TCP
  buffer was full, e.g. while uploading the save); small frames are batched
- Server save requests: routine saves wait for a calm moment (not while driving
  fast) and are announced; urgent flag from server 2.10.5
- Dynamic built-in font for all MSCO GUI (Unity logged hundreds of
  "Font size and style overrides" warnings with stutters)
- Admin VEHICLE tab rescans every 30 s / after actions instead of every 5 s;
  time/weather FSM search less often
- Reused buffers for Build/Frame/Poll (less garbage, fewer GC pauses)
- Hitch log: GC and save detection, count of hitches between reports
- Show Bolt Size 1.0.1: finds the FPSCamera (Camera.main may be missing), uses
  MSCO's shared FSM list instead of FindObjectsOfType every 2 s

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-04 00:08:36 +02:00
co-authored by Claude Opus 5.5
parent e32ddadbc8
commit d195a484d8
9 changed files with 277 additions and 57 deletions
+17
View File
@@ -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**
+2
View File
@@ -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);
+1 -1
View File
@@ -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.
/// </summary>
public const string ModVersion = "2.10.7";
public const string ModVersion = "2.10.8";
/// <summary>
/// Netzwerk-Version (die ersten beiden Stellen). Nur erhöhen, wenn sich das Protokoll inkompatibel ändert - dann
/// auch GameServer.ModVersion im Server.
+66 -13
View File
@@ -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
}
/// <summary>Zu Beginn jedes Bilds: war das letzte Bild ein Hänger? Dann Ursache melden (höchstens alle 20 s).</summary>
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.
/// </summary>
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;
}
+117 -23
View File
@@ -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
}
}
/// <summary>TCP-Verbindung (LAN / IP). Lesen ist nicht-blockierend und wird über Poll() abgefragt.</summary>
/// <summary>
/// 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.
/// </summary>
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<byte[]> outgoing = new Queue<byte[]>();
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();
}
/// <summary>Sende-Thread: wartet auf Pakete und schreibt sie (gebündelt) in die Verbindung.</summary>
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<Message> polled = new List<Message>(); // wiederverwendet: der Aufrufer geht sie sofort durch
public List<Message> Poll()
{
var messages = new List<Message>();
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<Message> polled = new List<Message>(); // wiederverwendet: der Aufrufer geht sie sofort durch
public List<Message> Poll()
{
var messages = new List<Message>();
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<NetEvent> polledEvents = new List<NetEvent>(); // wiederverwendet (jedes Frame)
public List<NetEvent> Poll()
{
var events = new List<NetEvent>();
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<BinaryWriter> 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; }
}
/// <summary>Stellt der Nachricht die Absender-ID voran (für weitergeleitete Nachrichten).</summary>
+23
View File
@@ -10,6 +10,26 @@ namespace MSCO
/// </summary>
public static class UiTheme
{
static Font dynamicFont;
static bool fontSearched;
/// <summary>
/// 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).
/// </summary>
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;
+5 -2
View File
@@ -61,10 +61,13 @@ namespace MSCO
new[] { "Radiator", "Water", "Coolant", "6" },
};
/// <summary>Database neu einlesen (höchstens alle 5 s, außer erzwungen).</summary>
/// <summary>
/// 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.
/// </summary>
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();
+2 -2
View File
@@ -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
+44 -16
View File
@@ -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; }
/// <summary>
/// Spielerkamera: Camera.main gibt es in My Summer Car nicht immer (die Kamera des Spielers ist "FPSCamera").
/// </summary>
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()
/// <summary>
/// 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.
/// </summary>
void Scan(Camera cam)
{
nextScan = Time.realtimeSinceStartup + ScanInterval;
bolts.Clear();
foreach (PlayMakerFSM fsm in Object.FindObjectsOfType<PlayMakerFSM>())
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");
}
}
/// <summary>Größe in mm. Das Spiel speichert sie je nach Schraube als 10 oder als 1.0 (= 10 mm).</summary>