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:
@@ -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**
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user