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
|
# 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
|
## 2.10.7
|
||||||
|
|
||||||
**Sicherheit**
|
**Sicherheit**
|
||||||
|
|||||||
@@ -1178,6 +1178,7 @@ namespace MSCO
|
|||||||
static GUIStyle Label(int size, Color color, FontStyle font)
|
static GUIStyle Label(int size, Color color, FontStyle font)
|
||||||
{
|
{
|
||||||
var s = new GUIStyle(GUI.skin.label);
|
var s = new GUIStyle(GUI.skin.label);
|
||||||
|
if (UiTheme.DynamicFont != null) s.font = UiTheme.DynamicFont;
|
||||||
s.fontSize = size;
|
s.fontSize = size;
|
||||||
s.fontStyle = font;
|
s.fontStyle = font;
|
||||||
s.normal.textColor = color;
|
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))
|
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);
|
var s = new GUIStyle(GUI.skin.button);
|
||||||
|
if (UiTheme.DynamicFont != null) s.font = UiTheme.DynamicFont;
|
||||||
bool withBorder = border.a > 0f;
|
bool withBorder = border.a > 0f;
|
||||||
int h = radius * 2 + 4;
|
int h = radius * 2 + 4;
|
||||||
Texture2D n = UiTheme.RoundedEx(normal, normal, border, withBorder, radius, h);
|
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
|
/// 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.
|
/// Servern und Mitspielern derselben ersten zwei Stellen kompatibel.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const string ModVersion = "2.10.7";
|
public const string ModVersion = "2.10.8";
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Netzwerk-Version (die ersten beiden Stellen). Nur erhöhen, wenn sich das Protokoll inkompatibel ändert - dann
|
/// Netzwerk-Version (die ersten beiden Stellen). Nur erhöhen, wenn sich das Protokoll inkompatibel ändert - dann
|
||||||
/// auch GameServer.ModVersion im Server.
|
/// auch GameServer.ModVersion im Server.
|
||||||
|
|||||||
+66
-13
@@ -1635,7 +1635,12 @@ namespace MSCO
|
|||||||
|
|
||||||
case MsgType.SaveRequest:
|
case MsgType.SaveRequest:
|
||||||
// Server möchte den aktuellen Spielstand (für neue Spieler / zum Sichern)
|
// 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;
|
break;
|
||||||
|
|
||||||
case MsgType.Reconnect:
|
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>
|
/// <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()
|
void CheckHitch()
|
||||||
{
|
{
|
||||||
float dt = Time.unscaledDeltaTime;
|
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;
|
hitchesSinceReport++;
|
||||||
int total = Mathf.RoundToInt(dt * 1000f);
|
if (Time.realtimeSinceStartup >= nextHitchReport)
|
||||||
if (frameMsco > dt * 1000f * 0.4f)
|
{
|
||||||
MscoLog.Warning("[MSCO] Hitch " + total + " ms - caused by MSCO: " + slowestPart + " " + slowestMs.ToString("0") +
|
nextHitchReport = Time.realtimeSinceStartup + 20f;
|
||||||
" ms (MSCO total " + frameMsco.ToString("0") + " ms)");
|
int total = Mathf.RoundToInt(dt * 1000f);
|
||||||
else
|
string more = hitchesSinceReport > 1 ? " [" + hitchesSinceReport + " hitches since the last report]" : "";
|
||||||
MscoLog.Info("[MSCO] Hitch " + total + " ms - not MSCO (MSCO used " + frameMsco.ToString("0") + " ms)" +
|
hitchesSinceReport = 0;
|
||||||
(Time.realtimeSinceStartup - lastSaveTriggered < 5f ? " - the game was saving" : " - the game itself (loading, saving, physics)"));
|
// 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;
|
frameMsco = 0;
|
||||||
slowestMs = 0;
|
slowestMs = 0;
|
||||||
@@ -2490,9 +2510,30 @@ namespace MSCO
|
|||||||
/// genau die aktuelle Welt - Schrauben, Teile, alles - statt eines alten Stands mit nachgespielten Aktionen.
|
/// 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.
|
/// Der eigene Spielstand ist davon nicht betroffen: er liegt gesichert im Backup und kommt im Hauptmenü zurück.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
System.Collections.IEnumerator UploadSave(int requestId)
|
System.Collections.IEnumerator UploadSave(int requestId, bool urgent)
|
||||||
{
|
{
|
||||||
if (!MenuHelper.InGame || !dedicated || session == null) yield break;
|
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;
|
lastSaveTriggered = Time.realtimeSinceStartup;
|
||||||
PlayMakerFSM.BroadcastEvent("SAVEGAME");
|
PlayMakerFSM.BroadcastEvent("SAVEGAME");
|
||||||
float until = Time.realtimeSinceStartup + 3f; // das Spiel schreibt die Dateien über mehrere Frames
|
float until = Time.realtimeSinceStartup + 3f; // das Spiel schreibt die Dateien über mehrere Frames
|
||||||
@@ -2630,10 +2671,21 @@ namespace MSCO
|
|||||||
void OnGUI()
|
void OnGUI()
|
||||||
{
|
{
|
||||||
PartStart();
|
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(); }
|
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()
|
void DrawGui()
|
||||||
{
|
{
|
||||||
InputLock.KeepCursorFree();
|
InputLock.KeepCursorFree();
|
||||||
@@ -2652,7 +2704,8 @@ namespace MSCO
|
|||||||
{
|
{
|
||||||
int oldSize = savingStyle.fontSize;
|
int oldSize = savingStyle.fontSize;
|
||||||
savingStyle.fontSize = 30;
|
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;
|
savingStyle.fontSize = oldSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+117
-23
@@ -200,7 +200,8 @@ namespace MSCO
|
|||||||
{
|
{
|
||||||
if (payload == null) payload = new byte[0];
|
if (payload == null) payload = new byte[0];
|
||||||
var frame = new byte[5 + payload.Length];
|
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;
|
frame[4] = (byte)type;
|
||||||
Buffer.BlockCopy(payload, 0, frame, 5, payload.Length);
|
Buffer.BlockCopy(payload, 0, frame, 5, payload.Length);
|
||||||
return frame;
|
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 class Connection : IConnection
|
||||||
{
|
{
|
||||||
public bool IsClosed { get; private set; }
|
volatile bool closed;
|
||||||
public string CloseReason { get; private set; }
|
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 TcpClient client;
|
||||||
readonly NetworkStream stream;
|
readonly NetworkStream stream;
|
||||||
readonly FrameParser parser = new FrameParser();
|
readonly FrameParser parser = new FrameParser();
|
||||||
readonly byte[] readBuffer = new byte[64 * 1024];
|
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)
|
public Connection(TcpClient client)
|
||||||
{
|
{
|
||||||
@@ -232,11 +247,46 @@ namespace MSCO
|
|||||||
client.NoDelay = true;
|
client.NoDelay = true;
|
||||||
client.SendBufferSize = 4 * 1024 * 1024;
|
client.SendBufferSize = 4 * 1024 * 1024;
|
||||||
stream = client.GetStream();
|
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()
|
public List<Message> Poll()
|
||||||
{
|
{
|
||||||
var messages = new List<Message>();
|
var messages = polled;
|
||||||
|
messages.Clear();
|
||||||
if (IsClosed) return messages;
|
if (IsClosed) return messages;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -263,23 +313,37 @@ namespace MSCO
|
|||||||
|
|
||||||
public void Send(MsgType type, byte[] payload)
|
public void Send(MsgType type, byte[] payload)
|
||||||
{
|
{
|
||||||
if (IsClosed) return;
|
if (closed) return;
|
||||||
try
|
byte[] frame = FrameParser.Frame(type, payload);
|
||||||
|
bool tooMuch;
|
||||||
|
lock (gate)
|
||||||
{
|
{
|
||||||
byte[] frame = FrameParser.Frame(type, payload);
|
tooMuch = queuedBytes + frame.Length > MaxQueuedBytes;
|
||||||
stream.Write(frame, 0, frame.Length); // blockierend, damit große Dateien komplett rausgehen
|
if (!tooMuch)
|
||||||
}
|
{
|
||||||
catch (Exception e)
|
outgoing.Enqueue(frame);
|
||||||
{
|
queuedBytes += frame.Length;
|
||||||
Close("Network error: " + e.Message);
|
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)
|
public void Close(string reason)
|
||||||
{
|
{
|
||||||
if (IsClosed) return;
|
lock (gate)
|
||||||
IsClosed = true;
|
{
|
||||||
CloseReason = reason;
|
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 { }
|
try { client.Close(); } catch { }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -313,9 +377,11 @@ namespace MSCO
|
|||||||
lastReceive = Time.realtimeSinceStartup;
|
lastReceive = Time.realtimeSinceStartup;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
readonly List<Message> polled = new List<Message>(); // wiederverwendet: der Aufrufer geht sie sofort durch
|
||||||
public List<Message> Poll()
|
public List<Message> Poll()
|
||||||
{
|
{
|
||||||
var messages = new List<Message>();
|
var messages = polled;
|
||||||
|
messages.Clear();
|
||||||
if (IsClosed) return messages;
|
if (IsClosed) return messages;
|
||||||
try { parser.Parse(messages); }
|
try { parser.Parse(messages); }
|
||||||
catch (Exception e) { Close("Network error: " + e.Message); }
|
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 };
|
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()
|
public List<NetEvent> Poll()
|
||||||
{
|
{
|
||||||
var events = new List<NetEvent>();
|
var events = polledEvents;
|
||||||
|
events.Clear();
|
||||||
if (IsClosed) return events;
|
if (IsClosed) return events;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -568,15 +636,41 @@ namespace MSCO
|
|||||||
try { if (listener != null) listener.Stop(); } catch { }
|
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)
|
public static byte[] Build(Action<BinaryWriter> write)
|
||||||
{
|
{
|
||||||
using (var ms = new MemoryStream())
|
if (building)
|
||||||
using (var w = new BinaryWriter(ms))
|
|
||||||
{
|
{
|
||||||
write(w);
|
// Build innerhalb von Build: eigener Puffer
|
||||||
w.Flush();
|
using (var ms = new MemoryStream())
|
||||||
return ms.ToArray();
|
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>
|
/// <summary>Stellt der Nachricht die Absender-ID voran (für weitergeleitete Nachrichten).</summary>
|
||||||
|
|||||||
+23
@@ -10,6 +10,26 @@ namespace MSCO
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static class UiTheme
|
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 Background = Hex(0x16, 0x16, 0x1a, 0.97f);
|
||||||
public static readonly Color Panel = Hex(0x0f, 0x0f, 0x12, 0.98f);
|
public static readonly Color Panel = Hex(0x0f, 0x0f, 0x12, 0.98f);
|
||||||
public static readonly Color Row = Hex(0x22, 0x22, 0x28);
|
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)
|
public static GUIStyle Button(Color normal, Color hover, Color text, int fontSize = 13, bool bold = true)
|
||||||
{
|
{
|
||||||
var s = new GUIStyle(GUI.skin.button);
|
var s = new GUIStyle(GUI.skin.button);
|
||||||
|
if (DynamicFont != null) s.font = DynamicFont;
|
||||||
s.normal.background = Rounded(normal);
|
s.normal.background = Rounded(normal);
|
||||||
s.hover.background = Rounded(hover);
|
s.hover.background = Rounded(hover);
|
||||||
s.active.background = Rounded(hover);
|
s.active.background = Rounded(hover);
|
||||||
@@ -186,6 +207,7 @@ namespace MSCO
|
|||||||
public static GUIStyle Field(int fontSize = 14)
|
public static GUIStyle Field(int fontSize = 14)
|
||||||
{
|
{
|
||||||
var s = new GUIStyle(GUI.skin.textField);
|
var s = new GUIStyle(GUI.skin.textField);
|
||||||
|
if (DynamicFont != null) s.font = DynamicFont;
|
||||||
s.normal.background = Rounded(FieldColor);
|
s.normal.background = Rounded(FieldColor);
|
||||||
s.hover.background = Rounded(FieldFocus);
|
s.hover.background = Rounded(FieldFocus);
|
||||||
s.focused.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)
|
public static GUIStyle Label(int fontSize, Color color, FontStyle font = FontStyle.Normal, TextAnchor align = TextAnchor.UpperLeft)
|
||||||
{
|
{
|
||||||
var s = new GUIStyle(GUI.skin.label);
|
var s = new GUIStyle(GUI.skin.label);
|
||||||
|
if (DynamicFont != null) s.font = DynamicFont;
|
||||||
s.fontSize = fontSize;
|
s.fontSize = fontSize;
|
||||||
s.fontStyle = font;
|
s.fontStyle = font;
|
||||||
s.normal.textColor = color;
|
s.normal.textColor = color;
|
||||||
|
|||||||
+5
-2
@@ -61,10 +61,13 @@ namespace MSCO
|
|||||||
new[] { "Radiator", "Water", "Coolant", "6" },
|
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)
|
public static void Refresh(bool force)
|
||||||
{
|
{
|
||||||
if (!force && Time.realtimeSinceStartup - scannedAt < 5f) return;
|
if (!force && Time.realtimeSinceStartup - scannedAt < 30f) return;
|
||||||
scannedAt = Time.realtimeSinceStartup;
|
scannedAt = Time.realtimeSinceStartup;
|
||||||
fluids.Clear();
|
fluids.Clear();
|
||||||
parts.Clear();
|
parts.Clear();
|
||||||
|
|||||||
+2
-2
@@ -28,9 +28,9 @@ namespace MSCO
|
|||||||
static void Find()
|
static void Find()
|
||||||
{
|
{
|
||||||
if (sun != null && weather != null) return;
|
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;
|
searchedAt = Time.realtimeSinceStartup;
|
||||||
foreach (PlayMakerFSM f in SceneCache.Fsms(5f))
|
foreach (PlayMakerFSM f in SceneCache.Fsms(60f))
|
||||||
{
|
{
|
||||||
if (f == null) continue;
|
if (f == null) continue;
|
||||||
if (sun == null && f.FsmVariables.FindFsmInt("Time") != null && f.Fsm.GetState("12-14") != null
|
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: AssemblyTitle("Show Bolt Size")]
|
||||||
[assembly: AssemblyProduct("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: AssemblyDescription("Shows the size of the bolt you look at, so you pick the right wrench.")]
|
||||||
[assembly: AssemblyVersion("1.0.0.0")]
|
[assembly: AssemblyVersion("1.0.1.0")]
|
||||||
[assembly: AssemblyFileVersion("1.0.0.0")]
|
[assembly: AssemblyFileVersion("1.0.1.0")]
|
||||||
[assembly: AssemblyInformationalVersion("1.0.0")]
|
[assembly: AssemblyInformationalVersion("1.0.1")]
|
||||||
|
|
||||||
namespace ShowBoltSize
|
namespace ShowBoltSize
|
||||||
{
|
{
|
||||||
@@ -22,12 +22,12 @@ namespace ShowBoltSize
|
|||||||
{
|
{
|
||||||
public override string ID { get { return "showboltsize"; } }
|
public override string ID { get { return "showboltsize"; } }
|
||||||
public override string Name { get { return "Show Bolt Size"; } }
|
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"; } }
|
public override string Author { get { return "MBM"; } }
|
||||||
|
|
||||||
const float MaxDistance = 1.3f;
|
const float MaxDistance = 1.3f;
|
||||||
const float BoltRadius = 0.025f; // so groß wirkt eine Schraube beim Zielen
|
const float BoltRadius = 0.025f; // so groß wirkt eine Schraube beim Zielen
|
||||||
const float ScanInterval = 2f;
|
const float ScanInterval = 5f;
|
||||||
|
|
||||||
sealed class Bolt
|
sealed class Bolt
|
||||||
{
|
{
|
||||||
@@ -41,28 +41,47 @@ namespace ShowBoltSize
|
|||||||
float nextScan;
|
float nextScan;
|
||||||
string label;
|
string label;
|
||||||
GUIStyle style;
|
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; }
|
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()
|
public override void Update()
|
||||||
{
|
{
|
||||||
label = null;
|
label = null;
|
||||||
if (!inGame) return;
|
if (!inGame) return;
|
||||||
Camera cam = Camera.main;
|
Camera cam = FindCamera();
|
||||||
if (cam == null) return;
|
if (cam == null) return;
|
||||||
if (Time.realtimeSinceStartup >= nextScan) Scan();
|
if (Time.realtimeSinceStartup >= nextScan) Scan(cam);
|
||||||
|
|
||||||
Transform eye = cam.transform;
|
Transform eye = cam.transform;
|
||||||
|
Vector3 eyePos = eye.position, forward = eye.forward;
|
||||||
Bolt best = null;
|
Bolt best = null;
|
||||||
float bestAngle = float.MaxValue;
|
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;
|
Bolt b = bolts[i];
|
||||||
Vector3 to = b.Transform.position - eye.position;
|
if (b.Transform == null) continue;
|
||||||
float distance = to.magnitude;
|
Vector3 to = b.Transform.position - eyePos;
|
||||||
if (distance > MaxDistance || distance < 0.05f) continue;
|
float sq = to.sqrMagnitude;
|
||||||
float angle = Vector3.Angle(eye.forward, to);
|
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;
|
float limit = Mathf.Atan2(BoltRadius, distance) * Mathf.Rad2Deg + 0.5f;
|
||||||
if (angle < limit && angle < bestAngle)
|
if (angle < limit && angle < bestAngle)
|
||||||
{
|
{
|
||||||
@@ -73,11 +92,15 @@ namespace ShowBoltSize
|
|||||||
if (best != null) label = Describe(best);
|
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;
|
nextScan = Time.realtimeSinceStartup + ScanInterval;
|
||||||
bolts.Clear();
|
bolts.Clear();
|
||||||
foreach (PlayMakerFSM fsm in Object.FindObjectsOfType<PlayMakerFSM>())
|
foreach (PlayMakerFSM fsm in SceneCache.Fsms(30f))
|
||||||
{
|
{
|
||||||
if (fsm == null || fsm.FsmName != "Screw") continue;
|
if (fsm == null || fsm.FsmName != "Screw") continue;
|
||||||
FsmFloat f = fsm.FsmVariables.FindFsmFloat("BoltSize");
|
FsmFloat f = fsm.FsmVariables.FindFsmFloat("BoltSize");
|
||||||
@@ -85,6 +108,11 @@ namespace ShowBoltSize
|
|||||||
if (f == null && i == null) continue;
|
if (f == null && i == null) continue;
|
||||||
bolts.Add(new Bolt { Transform = fsm.transform, SizeF = f, SizeI = i });
|
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>
|
/// <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