Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
06d3834576 | ||
|
|
47c172859c | ||
|
|
e46b26c1d7 | ||
|
|
1da5e3bb59 | ||
|
|
b2ff221526 | ||
|
|
8bac7d194a | ||
|
|
ceaa2e29aa | ||
|
|
733a0d9635 | ||
|
|
7bef8fe265 | ||
|
|
05e68a683d | ||
|
|
d567d5992a | ||
|
|
fe08fa5213 | ||
|
|
578bd30b3d | ||
|
|
dd4e004d51 | ||
|
|
d195a484d8 | ||
|
|
e32ddadbc8 | ||
|
|
77c51b52c5 | ||
|
|
a499577b7c | ||
|
|
9c933ed486 | ||
|
|
92a6125f22 | ||
|
|
0a9da7bc9c | ||
|
|
a067e238da | ||
|
|
2502a0800e | ||
|
|
13e5193616 | ||
|
|
4be6b68f25 | ||
|
|
06df8fecc5 | ||
|
|
29a3697cc8 | ||
|
|
7b4a89d50f | ||
|
|
adc6eff9e8 | ||
|
|
4c1366b5d9 | ||
|
|
027296d27c | ||
|
|
166f873669 | ||
|
|
461d94c5fc | ||
|
|
ee162bd64f | ||
|
|
6bd4e67382 | ||
|
|
15c9ee548d | ||
|
|
3ea0e8ebca |
+280
@@ -0,0 +1,280 @@
|
|||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Reflection;
|
||||||
|
using Harmony;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace Doorstop
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Einstieg ohne MSCLoader: Unity Doorstop (winhttp.dll im Spielordner) ruft beim Spielstart diese Methode auf - der
|
||||||
|
/// MSCO Launcher startet das Spiel mit "--doorstop-target-assembly ...\MSCO\MSCO.dll", MSCLoader wird dann gar nicht
|
||||||
|
/// geladen. Hier läuft noch nichts von Unity: nur vorbereiten, gestartet wird im Hauptmenü (siehe MSCO.Bootstrap).
|
||||||
|
/// </summary>
|
||||||
|
public static class Entrypoint
|
||||||
|
{
|
||||||
|
public static void Start()
|
||||||
|
{
|
||||||
|
MSCO.Bootstrap.Start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace MSCO
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Startet MSCO eigenständig: Abhängigkeiten (Harmony) aus dem MSCO-Ordner laden, warten bis das
|
||||||
|
/// Spiel seinen Code geladen hat, dann beim ersten PlayMakerArrayListProxy.Awake (Hauptmenü, Hauptthread) den
|
||||||
|
/// MultiplayerManager anlegen. Protokoll: <Spiel>\MSCO\MSCO_Loader.txt.
|
||||||
|
/// </summary>
|
||||||
|
public static class Bootstrap
|
||||||
|
{
|
||||||
|
static string folder;
|
||||||
|
/// <summary>Ordner von MSCO.dll (<Spiel>\MSCO).</summary>
|
||||||
|
public static string Folder { get { return folder; } }
|
||||||
|
static bool patchedGame, patchedPlayMaker, started;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Singleplayer mit Mods (Launcher: SINGLEPLAYER, "-msco-singleplayer"): nur die eigenen Mods laden - kein
|
||||||
|
/// MSCO-Menü, kein Multiplayer, das Spiel bleibt sonst unverändert.
|
||||||
|
/// </summary>
|
||||||
|
public static readonly bool Singleplayer = Array.IndexOf(Environment.GetCommandLineArgs(), "-msco-singleplayer") >= 0;
|
||||||
|
static StreamWriter log;
|
||||||
|
|
||||||
|
public static void Start()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
folder = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
|
||||||
|
log = new StreamWriter(Path.Combine(folder, "MSCO_Loader.txt"), false) { AutoFlush = true };
|
||||||
|
Log("MSCO " + MSCO.ModVersion + " loader started (standalone, without MSCLoader)");
|
||||||
|
Log("Command line: " + string.Join(" ", Environment.GetCommandLineArgs()));
|
||||||
|
if (Singleplayer) Log("Singleplayer mode: only mods, no multiplayer");
|
||||||
|
AppDomain.CurrentDomain.AssemblyResolve += Resolve;
|
||||||
|
AppDomain.CurrentDomain.AssemblyLoad += (_, e) => OnAssemblyLoaded(e.LoadedAssembly);
|
||||||
|
foreach (Assembly a in AppDomain.CurrentDomain.GetAssemblies()) OnAssemblyLoaded(a);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Log("Loader failed: " + e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Log(string line)
|
||||||
|
{
|
||||||
|
try { if (log != null) log.WriteLine(DateTime.Now.ToString("HH:mm:ss") + " " + line); } catch { }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Unsere Abhängigkeiten liegen neben MSCO.dll (das Spiel sucht nur in Managed).</summary>
|
||||||
|
static Assembly Resolve(object sender, ResolveEventArgs args)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string name = new AssemblyName(args.Name).Name;
|
||||||
|
if (name == typeof(Bootstrap).Assembly.GetName().Name) return typeof(Bootstrap).Assembly; // Mods verweisen auf MSCO
|
||||||
|
string file = Path.Combine(folder, name + ".dll");
|
||||||
|
if (File.Exists(file)) return Assembly.LoadFrom(file);
|
||||||
|
}
|
||||||
|
catch (Exception e) { Log("Resolve " + args.Name + " failed: " + e.Message); }
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void OnAssemblyLoaded(Assembly assembly)
|
||||||
|
{
|
||||||
|
if (patchedGame && patchedPlayMaker) return;
|
||||||
|
string name;
|
||||||
|
try { name = assembly.GetName().Name; } catch { return; }
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (name == "Assembly-CSharp" && !patchedGame) { patchedGame = true; Hook(assembly, "PlayMakerArrayListProxy", "Awake"); }
|
||||||
|
// PlayMaker läuft schon im Startbildschirm (Nachrichten, "Press any key") - so startet MSCO sofort
|
||||||
|
if (name == "PlayMaker" && !patchedPlayMaker) { patchedPlayMaker = true; Hook(assembly, "PlayMakerFSM", "Awake"); }
|
||||||
|
}
|
||||||
|
catch (Exception e) { Log("Could not hook into " + name + ": " + e); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Haken in eine Awake-Methode, die früh auf dem Hauptthread läuft (PlayMakerFSM schon im Startbildschirm,
|
||||||
|
/// PlayMakerArrayListProxy im Hauptmenü als Rückfallebene). Der erste, der läuft, startet MSCO.
|
||||||
|
/// </summary>
|
||||||
|
static void Hook(Assembly assembly, string typeName, string methodName)
|
||||||
|
{
|
||||||
|
Type type = assembly.GetType(typeName);
|
||||||
|
MethodInfo method = type != null ? type.GetMethod(methodName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) : null;
|
||||||
|
if (method == null)
|
||||||
|
{
|
||||||
|
Log(typeName + "." + methodName + " not found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
HarmonyInstance.Create("mbm.msco.bootstrap." + typeName).Patch(method,
|
||||||
|
new HarmonyMethod(typeof(Bootstrap).GetMethod("BeforeAwake", BindingFlags.Static | BindingFlags.NonPublic)), null);
|
||||||
|
Log("Hooked into " + typeName + "." + methodName);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void BeforeAwake()
|
||||||
|
{
|
||||||
|
if (started) return;
|
||||||
|
started = true;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var go = new GameObject("MSCO_Host");
|
||||||
|
UnityEngine.Object.DontDestroyOnLoad(go);
|
||||||
|
go.AddComponent<MscoHost>();
|
||||||
|
Log("MSCO started");
|
||||||
|
}
|
||||||
|
catch (Exception e) { Log("Start failed: " + e); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Lebt über alle Szenen: legt im Hauptmenü den MultiplayerManager an (früher MSCLoaders OnMenuLoad).</summary>
|
||||||
|
public class MscoHost : MonoBehaviour
|
||||||
|
{
|
||||||
|
void Start()
|
||||||
|
{
|
||||||
|
host = this;
|
||||||
|
MscoLog.Info("Welcome to MSCO by mbm.wtf");
|
||||||
|
if (!Bootstrap.Singleplayer) MenuBranding.ApplyWindow();
|
||||||
|
// Den Nachrichten-Bildschirm des Spiels ("Latest news ... press any key") überspringen - direkt ins Hauptmenü
|
||||||
|
string scene = Application.loadedLevelName;
|
||||||
|
Bootstrap.Log("First scene: " + scene);
|
||||||
|
if (scene != "MainMenu" && scene != "GAME")
|
||||||
|
{
|
||||||
|
// Erst einen Frame lang den MSCO-Ladebildschirm zeigen (Kameras und Ton des Startbildschirms aus), dann
|
||||||
|
// laden - sonst steht während des Ladens (ein paar Sekunden) "Press any key" auf dem Bildschirm
|
||||||
|
Bootstrap.Log("Skipping the intro/news screen");
|
||||||
|
skipping = true;
|
||||||
|
skipFrame = Time.frameCount;
|
||||||
|
foreach (Camera c in Camera.allCameras) c.enabled = false;
|
||||||
|
previousVolume = AudioListener.volume;
|
||||||
|
AudioListener.volume = 0f;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!Bootstrap.Singleplayer) MultiplayerManager.Ensure();
|
||||||
|
SceneChanged(scene);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Mods: beim ersten Hauptmenü laden, dann Menü/Spiel melden.</summary>
|
||||||
|
static void SceneChanged(string scene)
|
||||||
|
{
|
||||||
|
if (scene == "MainMenu")
|
||||||
|
{
|
||||||
|
ModLoader.LoadLocal();
|
||||||
|
ModLoader.MenuLoad();
|
||||||
|
}
|
||||||
|
else if (scene == "GAME")
|
||||||
|
{
|
||||||
|
ModLoader.GameLoad();
|
||||||
|
string[] cmd = Environment.GetCommandLineArgs();
|
||||||
|
int i = Array.IndexOf(cmd, "-msco-dump");
|
||||||
|
if (i >= 0 && i + 1 < cmd.Length && host != null) host.StartCoroutine(DumpLater(cmd[i + 1]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static MscoHost host;
|
||||||
|
|
||||||
|
static System.Collections.IEnumerator DumpLater(string filter)
|
||||||
|
{
|
||||||
|
yield return new WaitForSeconds(25f);
|
||||||
|
try { Bootstrap.Log("FSM dump: " + FsmDump.Write(filter)); }
|
||||||
|
catch (Exception e) { Bootstrap.Log("FSM dump failed: " + e); }
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly System.Diagnostics.Stopwatch modWatch = new System.Diagnostics.Stopwatch();
|
||||||
|
|
||||||
|
/// <summary>Zeit der Mods messen (PERF-Bericht, Konsole "perf").</summary>
|
||||||
|
void MeasureMods(string part, Action call)
|
||||||
|
{
|
||||||
|
modWatch.Reset();
|
||||||
|
modWatch.Start();
|
||||||
|
try { call(); }
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
modWatch.Stop();
|
||||||
|
PerfStats.Add(part, modWatch.Elapsed.TotalMilliseconds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static readonly Action modFixedUpdate = ModLoader.FixedUpdate, modUpdate = ModLoader.Update, modOnGui = ModLoader.OnGUI;
|
||||||
|
|
||||||
|
void FixedUpdate()
|
||||||
|
{
|
||||||
|
MeasureMods("mods (physics)", modFixedUpdate);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool skipping, loadingScreen;
|
||||||
|
float previousVolume = -1f;
|
||||||
|
int skipFrame;
|
||||||
|
Texture2D black;
|
||||||
|
|
||||||
|
void Update()
|
||||||
|
{
|
||||||
|
MeasureMods("mods", modUpdate);
|
||||||
|
if (!skipping || Time.frameCount < skipFrame + 2) return;
|
||||||
|
skipping = false;
|
||||||
|
loadingScreen = true;
|
||||||
|
Application.LoadLevel("MainMenu");
|
||||||
|
}
|
||||||
|
|
||||||
|
void OnGUI()
|
||||||
|
{
|
||||||
|
if (!skipping && !loadingScreen)
|
||||||
|
{
|
||||||
|
// Mods zeichnen mit der dynamischen Schrift: Schriftgröße/Fett gehen sonst nicht, und Unity schreibt bei jedem
|
||||||
|
// Zeichnen eine Warnung samt Stacktrace ins Log (z.B. Show Bolt Size: hunderte pro Minute, kostet FPS)
|
||||||
|
GUISkin skin = GUI.skin;
|
||||||
|
Font previous = skin.font, font = UiTheme.DynamicFont;
|
||||||
|
if (font != null) skin.font = font;
|
||||||
|
try { MeasureMods("mods (interface)", modOnGui); }
|
||||||
|
finally { skin.font = previous; }
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
GUI.depth = -10000;
|
||||||
|
if (black == null)
|
||||||
|
{
|
||||||
|
black = new Texture2D(1, 1);
|
||||||
|
black.SetPixel(0, 0, new Color(0.05f, 0.05f, 0.063f));
|
||||||
|
black.Apply();
|
||||||
|
}
|
||||||
|
GUI.DrawTexture(new Rect(0, 0, Screen.width, Screen.height), black);
|
||||||
|
Texture2D logo = Branding.LogoTexture;
|
||||||
|
float size = Mathf.Round(Screen.height * 0.14f);
|
||||||
|
if (logo != null) GUI.DrawTexture(new Rect((Screen.width - size) / 2f, Screen.height * 0.40f - size / 2f, size, size), logo);
|
||||||
|
var style = new GUIStyle(GUI.skin.label) { alignment = TextAnchor.MiddleCenter, fontSize = Mathf.RoundToInt(Screen.height * 0.022f) };
|
||||||
|
style.normal.textColor = new Color(0.56f, 0.56f, 0.6f);
|
||||||
|
GUI.Label(new Rect(0, Screen.height * 0.40f + size / 2f + 16f, Screen.width, 40f), "Loading MSCO …", style);
|
||||||
|
}
|
||||||
|
|
||||||
|
void OnLevelWasLoaded(int level)
|
||||||
|
{
|
||||||
|
MenuHelper.SceneChanged();
|
||||||
|
loadingScreen = false;
|
||||||
|
if (previousVolume >= 0f) { AudioListener.volume = previousVolume; previousVolume = -1f; }
|
||||||
|
Bootstrap.Log("Level loaded: " + Application.loadedLevelName);
|
||||||
|
if (!Bootstrap.Singleplayer)
|
||||||
|
{
|
||||||
|
MenuBranding.ApplyWindow();
|
||||||
|
if (Application.loadedLevelName == "MainMenu") StartCoroutine(BrandMenu());
|
||||||
|
MultiplayerManager.Ensure();
|
||||||
|
}
|
||||||
|
SceneChanged(Application.loadedLevelName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Logo im Hauptmenü ersetzen - ein paar Mal, falls das Menü es erst nach dem Laden zeigt.</summary>
|
||||||
|
System.Collections.IEnumerator BrandMenu()
|
||||||
|
{
|
||||||
|
for (int i = 0; i < 4 && Application.loadedLevelName == "MainMenu"; i++)
|
||||||
|
{
|
||||||
|
yield return new WaitForSeconds(i == 0 ? 0.1f : 1f);
|
||||||
|
try { MenuBranding.ReplaceMenuLogo(); }
|
||||||
|
catch (System.Exception e) { Bootstrap.Log("Menu logo: " + e.Message); yield break; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void OnApplicationQuit()
|
||||||
|
{
|
||||||
|
try { PerfStats.FinalReport(); } catch { }
|
||||||
|
// Wer beendet das Spiel? (Diagnose)
|
||||||
|
Bootstrap.Log("Game is quitting (scene " + Application.loadedLevelName + "): " + System.Environment.StackTrace);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-1
@@ -6,7 +6,7 @@ namespace MSCO
|
|||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// MSCO-Logo (branding/logo-128.png, als Ressource in die DLL eingebettet):
|
/// MSCO-Logo (branding/logo-128.png, als Ressource in die DLL eingebettet):
|
||||||
/// Symbol in der MSCLoader-Modliste und im Launcher.
|
/// Symbol im MSCO-Launcher (Hauptmenü) und in Overlays.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class Branding
|
public static class Branding
|
||||||
{
|
{
|
||||||
|
|||||||
+186
@@ -1,5 +1,191 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2.10.15
|
||||||
|
|
||||||
|
- Leistungsbericht: der erste kommt 2 Minuten nach dem Laden der Welt (das Laden zählt nicht mehr mit), danach alle
|
||||||
|
5 Minuten, und beim Beenden des Spiels einer für die letzten Minuten
|
||||||
|
|
||||||
|
## 2.10.14
|
||||||
|
|
||||||
|
- **Hinweis bei Debug-Startoptionen:** Stehen in den Steam-Startoptionen von My Summer Car noch
|
||||||
|
`--doorstop-mono-debug-enabled true` (vom MSCLoader-Entwicklermodus), läuft das ganze Spiel mit Mono-Debugger und
|
||||||
|
spürbar langsamer. MSCO meldet das jetzt beim Start (Hinweis und Konsole F7)
|
||||||
|
|
||||||
|
## 2.10.13
|
||||||
|
|
||||||
|
- **Mehr FPS mit Mods:** Mods (z.B. Show Bolt Size) zeichnen jetzt mit der richtigen Schrift. Vorher schrieb Unity bei
|
||||||
|
jedem Zeichnen eine Warnung samt Stacktrace ins Log – in einer Sitzung über 400-mal, spürbar als Ruckeln
|
||||||
|
- **Leistungsbericht:** Im Spiel schreibt MSCO alle 5 Minuten eine Zeile nach `MSCO\MSCO_Loader.txt` (FPS, schlimmstes
|
||||||
|
Bild, Zeit von MSCO und den Mods). Die Mods stehen jetzt auch in `perf` (Konsole F7)
|
||||||
|
- **Ankündigungen im MSCO-Menü** neu gestaltet: Abstand innen, farbiger Balken je nach Art, Abzeichen und Titel auf
|
||||||
|
einer Linie, gut lesbarer Text
|
||||||
|
|
||||||
|
## 2.10.12
|
||||||
|
|
||||||
|
- **Fix:** Das MSCO-Menü wurde in 2.10.11 nicht angezeigt (Fehler beim Anlegen der Schrift für Ankündigungen)
|
||||||
|
|
||||||
|
## 2.10.11
|
||||||
|
|
||||||
|
- **Sicherheitsstufe:** Server und P2P-Hosts, denen MSCO-Sicherheitsupdates fehlen (Server älter als 2.10.6), kann man
|
||||||
|
nicht mehr betreten – im Server-Browser stehen sie als **OUTDATED**, statt JOIN steht dort „UPDATE REQUIRED“, und
|
||||||
|
wer trotzdem verbindet, bekommt den Grund angezeigt. Server-Betreiber: im Web-Panel auf *Update* klicken
|
||||||
|
- **Ankündigungen & Wartung:** Oben im MSCO-Menü stehen Ankündigungen des MSCO-Teams und ein Hinweis, wenn MSCO gerade
|
||||||
|
gewartet wird (mit × ausblendbar). Neue Ankündigungen während des Spielens erscheinen kurz am Bildschirmrand und
|
||||||
|
in der Konsole (F7)
|
||||||
|
|
||||||
|
## 2.10.10
|
||||||
|
|
||||||
|
- **Mods im Singleplayer:** Startet der Launcher mit SINGLEPLAYER, lädt MSCO nur noch die eigenen Mods
|
||||||
|
(`Mods\*.dll`) – kein MSCO-Menü, kein Multiplayer, das Spiel und der eigene Spielstand bleiben sonst unverändert
|
||||||
|
- Für Mod-Entwickler: `IsSingleplayer` in `MscoMod`
|
||||||
|
|
||||||
|
## 2.10.9
|
||||||
|
|
||||||
|
- **Beta-Hinweis:** im MSCO-Menü steht „BETA“ neben dem Schriftzug und ein kurzer Hinweis, dass Fehler auftreten
|
||||||
|
können; die Statusleiste im Spiel zeigt „MSCO BETA“
|
||||||
|
- Die Statusleiste baut ihren Text nur noch bei Änderungen neu (nicht jedes Frame)
|
||||||
|
|
||||||
|
## 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**
|
||||||
|
- Daten von Servern und Mitspielern werden auf gültige Längen geprüft – eine gefälschte Längenangabe ließ das Spiel
|
||||||
|
vorher mit „Out of Memory“ abstürzen
|
||||||
|
- Mitspieler können nicht mehr beliebige Spiel-Zustände bei anderen auslösen (z.B. am eigenen Spieler oder an
|
||||||
|
Systemen wie Tod/Speichern) – nur noch Klicks auf klickbare Dinge und echte Schraub-/Montage-Schritte
|
||||||
|
- Uhrzeit/Wetter und Meldungen im Stil von Server-/Admin-Nachrichten („* …“) nur noch vom Admin bzw. Host
|
||||||
|
- Release ist mit dem MSCO-Release-Schlüssel signiert
|
||||||
|
|
||||||
|
## 2.10.6
|
||||||
|
|
||||||
|
- Neuer Konsolenbefehl **oilcheck**: zeigt, woher Öl ausläuft – die Leck-Werte des Motors (Ölwanne, Ölfilter,
|
||||||
|
Zylinderkopfdichtung, Ventildeckel), die Schrauben der Ölwanne samt Ablassschraube und den Ölfilter (eingebaut,
|
||||||
|
Festigkeit). Wichtig: der Ölfilter ist ein gekauftes Teil und steht in **items.txt** – fehlt diese Datei im
|
||||||
|
Spielstand eines Servers, fehlt der Filter am Auto und Öl läuft aus
|
||||||
|
- Neue Beispiel-Mod **Show Bolt Size** (examples/ShowBoltSize); Mods lassen sich jetzt über den Launcher
|
||||||
|
veröffentlichen (siehe MODDING.md)
|
||||||
|
|
||||||
|
## 2.10.5
|
||||||
|
|
||||||
|
- **Teleport zu Orten** (Admin, Reiter TELEPORT) landet wieder am Ort: vorher zählten alle weit verstreuten Teile
|
||||||
|
eines Ortes mit (Briefkasten, Wege, abgeschaltete Objekte) und man landete irgendwo im Wald; außerdem wurde
|
||||||
|
manchmal ein gleichnamiges Unterobjekt statt des Ortes genommen
|
||||||
|
|
||||||
|
## 2.10.4
|
||||||
|
|
||||||
|
**Admin-Menü neu aufgebaut**
|
||||||
|
- Festes Fenster mit gleich breiten Reitern, Zeilen mit Status-Abzeichen statt langer Listen
|
||||||
|
- VEHICLE: Suche, Filter **PROBLEMS** (kaputt, lose, verschlissen) und je Kategorie, Füllstände ein-/ausklappbar
|
||||||
|
- **INSTALL** für nicht eingebaute Teile: das Teil wird wie beim Laden eines Spielstands eingebaut und sitzt exakt
|
||||||
|
auf seinem Halter (auch bei den Mitspielern); fehlt das Trägerteil, steht der Grund im Log
|
||||||
|
- Status **LOOSE** kommt jetzt aus der echten Festigkeit (vorher blieb nach LOOSEN „BOLTED“ stehen)
|
||||||
|
- WORLD: **Uhrzeit** (in 2-Stunden-Schritten) und **Wetter** (klar, Regen, Gewitter – direkt über dir) für alle
|
||||||
|
- ITEMS: Suchfeld; Konsole und Admin-Menü überdecken sich nicht mehr
|
||||||
|
|
||||||
|
**JOIN ohne Rückfrage**
|
||||||
|
- JOIN im Server-Browser oder auf der Website verbindet im Hauptmenü sofort. Server-Mods fragen weiterhin nach
|
||||||
|
|
||||||
|
## 2.10.3
|
||||||
|
|
||||||
|
**Schrauben synchron**
|
||||||
|
- Jede Schraube wird jetzt als Stufe (0–8) übertragen und die Festigkeit jedes Teils dazu: vorher drehte sich beim
|
||||||
|
Mitspieler nur die Schraube, sein Teil blieb „lose“ bzw. „fest“ (die Festigkeit zählt im Spiel das Werkzeug hoch) –
|
||||||
|
Teile ließen sich dann nicht abbauen oder fielen ab
|
||||||
|
- Wird ein Teil festgezogen (Werkzeug, Admin oder vom Mitspieler), springt es exakt auf seinen Halter – verrutschte
|
||||||
|
Teile sitzen wieder richtig (nicht bei eingedrehten Teilen wie Zündkerzen/Ölfilter und nicht bei Türen/Hauben)
|
||||||
|
|
||||||
|
**Admin-Menü, Reiter VEHICLE**
|
||||||
|
- Je Teil **TIGHTEN** / **LOOSEN** (alle Schrauben ganz fest bzw. ganz lose, wie mit dem Werkzeug) und oben
|
||||||
|
**TIGHTEN ALL** / **LOOSEN ALL**; Anzeige „bolted“/„loose“; das Fenster ist auf diesem Reiter breiter
|
||||||
|
|
||||||
|
## 2.10.2
|
||||||
|
|
||||||
|
- **Beifahrer**: automatisch festgehalten wird man nur noch, wenn das Fahrzeug fährt (egal wer fährt) – am stehenden
|
||||||
|
Auto kann man sich frei bewegen und schrauben (vorher hing man fest, sobald man sich in den Motorraum beugte, und
|
||||||
|
wurde nach dem Aussteigen gleich wieder eingefangen)
|
||||||
|
- **Öl lief endlos aus**: die Ablassschraube war bei Mitspielern unterschiedlich weit eingedreht (offen = Leck). Schrauben
|
||||||
|
mit Funktion (Ablassschraube) werden jetzt als Stufe übertragen und über die Logik des Spiels angewendet; beim Beitreten
|
||||||
|
gibt die Welt-Autorität ihren Stand vor
|
||||||
|
- **Admin-Menü, Reiter VEHICLE**: Füllstände (Benzin, Öl, Brems-/Kupplungsflüssigkeit, Kühlwasser) mit FILL/EMPTY, alle
|
||||||
|
Teile mit Verschleiß und Zustand – einzeln REPAIR/BREAK, REPAIR ALL, Filter (zum Testen; im Chat angezeigt)
|
||||||
|
- **Server-Profil** im Server-Browser aufgeräumt: Infos und Spieler als zwei gleich hohe Karten, nichts bricht mehr um
|
||||||
|
- Diagnose `fsmdump` zeigt jetzt auch die Variablen der FSMs
|
||||||
|
|
||||||
|
## 2.10.1
|
||||||
|
|
||||||
|
**Türen**
|
||||||
|
- Autotüren (Satsuma, Hayosiko, Kekmet, Gifu, Ferndale, Ruscko ...) werden jetzt als offen/zu übertragen: beim
|
||||||
|
Mitspieler geht die Tür wirklich auf (Halte-Joint weg, Scharnier frei) und schwingt mit, Zumachen läuft über die
|
||||||
|
Schließ-Logik des Spiels (mit Geräusch)
|
||||||
|
- Behoben: Satsuma-Türen ließen sich manchmal gar nicht mehr öffnen – jedes übertragene Zumachen hängte einen weiteren
|
||||||
|
Halte-Joint an. Solche „verklemmten“ Türen werden automatisch repariert
|
||||||
|
- Garagentor (und andere Tore/Türen mit Scharnier) synchron
|
||||||
|
|
||||||
|
**Leistung**
|
||||||
|
- Szenenname wird nur noch einmal pro Bild abgefragt (vorher hundertfach – jedes Mal ein neuer String)
|
||||||
|
- Konsole `perf`: FPS und Zeit pro MSCO-Teil (inkl. Oberfläche); Messung auf einem Server: MSCO braucht ~0,2 ms pro
|
||||||
|
Bild – die FPS begrenzt das Spiel selbst
|
||||||
|
- Konsole `fsmdump <text>`: Diagnose für Spiel-Logik (Zustände, Aktionen, Scharniere) in `MSCO\fsmdump.txt`
|
||||||
|
|
||||||
|
## 2.10.0
|
||||||
|
|
||||||
|
**Mod-Support**
|
||||||
|
- Eigene MSCO-Mods: DLLs in `<Spielordner>\MSCO\Mods` mit einer Klasse, die von `MscoMod` erbt (OnLoad, OnMenuLoad,
|
||||||
|
OnGameLoad, Update, OnGUI, Spieler kommen/gehen, Sitzung, eigene Netzwerk-Nachrichten zwischen den Spielern).
|
||||||
|
Anleitung und Beispiel: `MODDING.md`, `examples/HelloMod`
|
||||||
|
- **Server-Mods**: Server mit Mods schicken beim Beitreten die Liste; fehlende Mods lädt das Spiel nach Zustimmung
|
||||||
|
(„INSTALL & JOIN“) direkt vom Server (SHA-256 geprüft). Sie sind nur auf diesem Server aktiv
|
||||||
|
- Neue Seite **Mods** im MSCO-Menü, Konsole `mods`
|
||||||
|
- Server-Browser: Pille **MODS n**, Filter **VANILLA** / **MODDED**, Mod-Liste im Server-Profil
|
||||||
|
- **ESC im Hauptmenü** beendet das Spiel nicht mehr (zum Beenden: QUIT)
|
||||||
|
- Netzwerk-Version bleibt 2.8 – Server und Mitspieler mit 2.8.x/2.9.x funktionieren weiter
|
||||||
|
|
||||||
|
## 2.9.1
|
||||||
|
|
||||||
|
**Fehler ohne MSCLoader behoben**
|
||||||
|
- Server-Browser: „Could not load System.Runtime.Serialization“ – MSCO braucht Newtonsoft.Json nicht mehr (eigener
|
||||||
|
kleiner JSON-Leser); die Bibliothek kam vorher von MSCLoader
|
||||||
|
- Mauszeiger im Admin-Menü, Chat und in der Konsole wieder sichtbar (wie im Pausemenü des Spiels)
|
||||||
|
- Startbildschirm „Press any key“: statt ihm erscheint sofort ein MSCO-Ladebildschirm, dann das Hauptmenü
|
||||||
|
- JOIN im Browser, während das Spiel schon läuft: der Link kommt im Spiel an (Hinweis bzw. Beitreten im Hauptmenü)
|
||||||
|
|
||||||
|
**Admin-Menü: Teleport**
|
||||||
|
- Neuer Reiter **TELEPORT**: Haus, Laden (Teimo), Werkstatt, Prüfstelle, Peräjärvi, Müllkippe, Tanzhalle, Ferienhaus,
|
||||||
|
Hütte, Rallye, Dragrace, Fußballplatz, Kilju-Käufer, Theater, Satsuma – man landet neben dem Ort auf dem Boden
|
||||||
|
|
||||||
|
**MSCO-Look**
|
||||||
|
- Fenstertitel „MSCO - My Summer Car Online“ und MSCO-Symbol; im Hauptmenü ist das MSCO-Logo statt des Spiel-Logos
|
||||||
|
- Oberfläche im Stil der Website: rundere Karten mit feinem Rand, Filter als Pillen, oranger Schimmer, schlanke
|
||||||
|
Scrollleisten, Navigation mit abgerundeten Einträgen
|
||||||
|
|
||||||
|
## 2.9.0
|
||||||
|
|
||||||
|
**Eigenständig – ohne MSCLoader**
|
||||||
|
- MSCO braucht MSCLoader nicht mehr: es startet selbst über Unity Doorstop (`winhttp.dll`) aus `<Spielordner>\MSCO`.
|
||||||
|
Der MSCO Launcher richtet alles ein und stellt `doorstop_config.ini` auf MSCO um – auch der Start über Steam lädt dann
|
||||||
|
MSCO. MSCLoader ist dabei blockiert (seine Konfiguration wird als `doorstop_config.mscloader.ini` gesichert)
|
||||||
|
- Der Nachrichten-Bildschirm beim Spielstart („Press any key“) wird übersprungen, MSCO öffnet direkt im Hauptmenü
|
||||||
|
- Tasten ändern jetzt mit `bind` in der F7-Konsole (`bind passenger O`), gespeichert in den Spiel-Einstellungen
|
||||||
|
- Harmony und Newtonsoft.Json liegen neben `MSCO.dll`; Lade-Protokoll in `MSCO\MSCO_Loader.txt`
|
||||||
|
- `msco://`-Links werden vom Launcher übernommen, wenn er installiert ist
|
||||||
|
- Netzwerk-Version bleibt 2.8: spielt mit 2.8.x-Servern und -Mitspielern
|
||||||
|
|
||||||
## 2.8.1
|
## 2.8.1
|
||||||
|
|
||||||
**Mehr Events synchron**
|
**Mehr Events synchron**
|
||||||
|
|||||||
@@ -42,6 +42,99 @@ namespace MSCO
|
|||||||
return done > 0 ? "Hunger, thirst, stress, fatigue, bladder and dirt reset" : "Player needs not found (only works in the game)";
|
return done > 0 ? "Hunger, thirst, stress, fatigue, bladder and dirt reset" : "Player needs not found (only works in the game)";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Teleport-Ziele im Admin-Menü: Anzeigename und Objekt im Spiel.</summary>
|
||||||
|
public static readonly string[][] Places =
|
||||||
|
{
|
||||||
|
new[] { "Home", "YARD" }, new[] { "Shop (Teimo)", "STORE" }, new[] { "Repair shop", "REPAIRSHOP" },
|
||||||
|
new[] { "Inspection", "INSPECTION" }, new[] { "Peräjärvi", "PERAJARVI" }, new[] { "Landfill", "LANDFILL" },
|
||||||
|
new[] { "Dance hall", "DANCEHALL" }, new[] { "Cottage", "COTTAGE" }, new[] { "Cabin", "CABIN" },
|
||||||
|
new[] { "Rally", "RALLY" }, new[] { "Drag race", "DRAGRACE" }, new[] { "Soccer field", "SOCCER" },
|
||||||
|
new[] { "Kilju buyer", "KILJUGUY" }, new[] { "Theatre", "THEATRE" }, new[] { "Satsuma", "SATSUMA" },
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Wohin bei einem Teleport-Ziel? Zu einem Ort gehören oft weit verstreute Teile (Briefkasten, Wege, Strecken,
|
||||||
|
/// abgeschaltete Objekte) - deren gemeinsame Mitte liegt irgendwo im Wald. Deshalb: Median aller Teile (der Ort
|
||||||
|
/// selbst, Ausreißer zählen nicht), darum die dichte Gruppe (60 m), daneben ein Stück außerhalb auf den Boden
|
||||||
|
/// (nicht ins Dach oder in eine Wand).
|
||||||
|
/// </summary>
|
||||||
|
public static bool FindPlace(string objectName, out Vector3 position)
|
||||||
|
{
|
||||||
|
position = Vector3.zero;
|
||||||
|
GameObject root = FindRoot(objectName);
|
||||||
|
if (root == null) return false;
|
||||||
|
|
||||||
|
var centers = new List<Vector3>();
|
||||||
|
foreach (Renderer r in root.GetComponentsInChildren<Renderer>(true))
|
||||||
|
{
|
||||||
|
if (r == null || r is ParticleRenderer || r is ParticleSystemRenderer) continue;
|
||||||
|
Vector3 size = r.bounds.size;
|
||||||
|
if (size == Vector3.zero || size.x > 150f || size.z > 150f) continue; // Gelände-/Wasserflächen
|
||||||
|
centers.Add(r.bounds.center);
|
||||||
|
}
|
||||||
|
if (centers.Count == 0)
|
||||||
|
{
|
||||||
|
Vector3 ground0;
|
||||||
|
position = Ground(root.transform.position, root.transform.position.y + 50f, out ground0) ? ground0 : root.transform.position;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Vector3 median = new Vector3(Median(centers, 0), Median(centers, 1), Median(centers, 2));
|
||||||
|
Bounds cluster = new Bounds(median, Vector3.zero);
|
||||||
|
foreach (Vector3 c in centers)
|
||||||
|
if (new Vector2(c.x - median.x, c.z - median.z).sqrMagnitude < 60f * 60f) cluster.Encapsulate(c);
|
||||||
|
|
||||||
|
Vector3 center = cluster.center;
|
||||||
|
float ox = Mathf.Min(cluster.extents.x, 20f) + 4f, oz = Mathf.Min(cluster.extents.z, 20f) + 4f;
|
||||||
|
Vector3[] candidates =
|
||||||
|
{
|
||||||
|
center + new Vector3(ox, 0f, 0f), center + new Vector3(-ox, 0f, 0f),
|
||||||
|
center + new Vector3(0f, 0f, oz), center + new Vector3(0f, 0f, -oz), center,
|
||||||
|
};
|
||||||
|
foreach (Vector3 c in candidates)
|
||||||
|
{
|
||||||
|
Vector3 ground;
|
||||||
|
if (Ground(c, cluster.max.y + 30f, out ground)) { position = ground; return true; }
|
||||||
|
}
|
||||||
|
position = center + Vector3.up * 2f;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static float Median(List<Vector3> points, int axis)
|
||||||
|
{
|
||||||
|
var values = new List<float>(points.Count);
|
||||||
|
foreach (Vector3 p in points) values.Add(p[axis]);
|
||||||
|
values.Sort();
|
||||||
|
return values[values.Count / 2];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Wurzelobjekt eines Ortes (nur oberste Ebene - "YARD" o.ä. gibt es auch als Unterobjekt).</summary>
|
||||||
|
static GameObject FindRoot(string name)
|
||||||
|
{
|
||||||
|
GameObject found = null;
|
||||||
|
// auch ausgeblendete Objekte (das Spiel schaltet entfernte Orte ab); aktive bevorzugt
|
||||||
|
foreach (GameObject candidate in Resources.FindObjectsOfTypeAll<GameObject>())
|
||||||
|
{
|
||||||
|
if (candidate.name != name || candidate.transform.parent != null || candidate.hideFlags != HideFlags.None) continue;
|
||||||
|
if (candidate.activeInHierarchy) return candidate;
|
||||||
|
if (found == null) found = candidate;
|
||||||
|
}
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Höchster fester Boden unter einem Punkt (ohne Trigger).</summary>
|
||||||
|
static bool Ground(Vector3 at, float top, out Vector3 ground)
|
||||||
|
{
|
||||||
|
ground = at;
|
||||||
|
bool found = false;
|
||||||
|
foreach (RaycastHit hit in Physics.RaycastAll(new Vector3(at.x, top, at.z), Vector3.down, 1000f))
|
||||||
|
{
|
||||||
|
if (hit.collider == null || hit.collider.isTrigger || hit.normal.y < 0.6f) continue;
|
||||||
|
if (!found || hit.point.y > ground.y) { ground = hit.point; found = true; }
|
||||||
|
}
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Eigenen Spieler an eine Position setzen (etwas darüber, damit er nicht im Boden steckt).</summary>
|
/// <summary>Eigenen Spieler an eine Position setzen (etwas darüber, damit er nicht im Boden steckt).</summary>
|
||||||
public static bool Teleport(Transform player, Vector3 position)
|
public static bool Teleport(Transform player, Vector3 position)
|
||||||
{
|
{
|
||||||
|
|||||||
+42
-1
@@ -1,10 +1,11 @@
|
|||||||
|
using UnityEngine;
|
||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace MSCO
|
namespace MSCO
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Alle MSCO-Befehle an einer Stelle - genutzt von der eigenen F7-Konsole (ohne "mp" davor)
|
/// Alle MSCO-Befehle an einer Stelle - genutzt von der eigenen F7-Konsole (ohne "mp" davor)
|
||||||
/// und von der MSCLoader-Konsole ("mp ...").
|
/// (früher auch aus der MSCLoader-Konsole - MSCO läuft jetzt ohne MSCLoader).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class Commands
|
public static class Commands
|
||||||
{
|
{
|
||||||
@@ -17,12 +18,16 @@ namespace MSCO
|
|||||||
"disconnect - leave the server / game\n" +
|
"disconnect - leave the server / game\n" +
|
||||||
"host [port] / join <ip> [port] - direct LAN game (no Steam)\n" +
|
"host [port] / join <ip> [port] - direct LAN game (no Steam)\n" +
|
||||||
"save - save the game (host / singleplayer)\n" +
|
"save - save the game (host / singleplayer)\n" +
|
||||||
|
"bind [action key] - show / change keys (passenger, console, chat)\n" +
|
||||||
|
"mods - loaded MSCO mods (folder MSCO\\Mods)\n" +
|
||||||
|
"perf [reset] - FPS and how long each MSCO part takes per frame\n" +
|
||||||
"ping [on|off] - show your ping to the server (toggle)\n" +
|
"ping [on|off] - show your ping to the server (toggle)\n" +
|
||||||
"fps [on|off] - show the frames per second (toggle)\n" +
|
"fps [on|off] - show the frames per second (toggle)\n" +
|
||||||
"/admin - admin menu (kick / ban / teleport / cheats)\n" +
|
"/admin - admin menu (kick / ban / teleport / cheats)\n" +
|
||||||
"vehicles | rig - debug info about vehicles / player models\n" +
|
"vehicles | rig - debug info about vehicles / player models\n" +
|
||||||
"model [game|classic] - other players as MSC characters or the MSCO figure\n" +
|
"model [game|classic] - other players as MSC characters or the MSCO figure\n" +
|
||||||
"vars <filter> - synced vehicle values, e.g. vars oil (debug)\n" +
|
"vars <filter> - synced vehicle values, e.g. vars oil (debug)\n" +
|
||||||
|
"oilcheck - where does oil leak out? (all leak sources of the engine)\n" +
|
||||||
"debug - toggle sync debug output\n" +
|
"debug - toggle sync debug output\n" +
|
||||||
"clear - clear this console";
|
"clear - clear this console";
|
||||||
|
|
||||||
@@ -115,6 +120,39 @@ namespace MSCO
|
|||||||
else mp.ShowPing = !mp.ShowPing;
|
else mp.ShowPing = !mp.ShowPing;
|
||||||
print("Ping: " + mp.PingText + " (display " + (mp.ShowPing ? "on" : "off") + ")");
|
print("Ping: " + mp.PingText + " (display " + (mp.ShowPing ? "on" : "off") + ")");
|
||||||
break;
|
break;
|
||||||
|
case "fsmdump":
|
||||||
|
print(FsmDump.Write(args.Length > 1 ? string.Join(" ", args, 1, args.Length - 1) : ""));
|
||||||
|
break;
|
||||||
|
case "perf":
|
||||||
|
if (args.Length > 1 && args[1].ToLower() == "reset") { PerfStats.Reset(); print("Performance statistics reset"); break; }
|
||||||
|
print(PerfStats.Report());
|
||||||
|
break;
|
||||||
|
case "mods":
|
||||||
|
if (ModLoader.All.Count == 0) { print("No mods. Put MSCO mods (.dll) into " + ModLoader.Folder + " and restart the game."); break; }
|
||||||
|
foreach (ModLoader.LoadedMod m in ModLoader.All)
|
||||||
|
{
|
||||||
|
string name = "?", version = "";
|
||||||
|
try { name = m.Mod.Name; version = m.Mod.Version; } catch { }
|
||||||
|
print(" " + name + " " + version + (m.FromServer ? " [server]" : "") +
|
||||||
|
(m.Error != null ? " turned off: " + m.Error : m.Active ? " active" : " inactive"));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "bind":
|
||||||
|
{
|
||||||
|
// Tasten ändern: "bind" zeigt alle, "bind passenger O" setzt eine (früher MSCLoader-Einstellungen)
|
||||||
|
if (args.Length < 3)
|
||||||
|
{
|
||||||
|
foreach (KeyBinding k in MSCO.Keys) print(" " + k.Id.PadRight(10) + " " + k.Key + " (" + k.Description + ", default " + k.Default + ")");
|
||||||
|
print("Change: bind <" + string.Join("|", System.Array.ConvertAll(MSCO.Keys, k => k.Id)) + "> <key> e.g. bind passenger O");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
KeyBinding target = System.Array.Find(MSCO.Keys, k => k.Id.Equals(args[1], System.StringComparison.OrdinalIgnoreCase));
|
||||||
|
KeyCode key;
|
||||||
|
if (target == null) print("Unknown action " + args[1] + " - see 'bind'");
|
||||||
|
else if (!KeyBinding.TryParse(args[2], out key)) print("Unknown key " + args[2] + " (Unity key names, e.g. O, F6, Keypad5, Mouse3)");
|
||||||
|
else { target.Key = key; print(target.Description + ": " + key); }
|
||||||
|
break;
|
||||||
|
}
|
||||||
case "fps":
|
case "fps":
|
||||||
// "fps" schaltet die Anzeige um, "fps on/off" setzt sie
|
// "fps" schaltet die Anzeige um, "fps on/off" setzt sie
|
||||||
if (args.Length > 1) mp.ShowFps = args[1].ToLower() == "on" || args[1] == "1";
|
if (args.Length > 1) mp.ShowFps = args[1].ToLower() == "on" || args[1] == "1";
|
||||||
@@ -142,6 +180,9 @@ namespace MSCO
|
|||||||
// Diagnose: welche Fahrzeugwerte synchronisiert werden (z.B. "vars oil")
|
// Diagnose: welche Fahrzeugwerte synchronisiert werden (z.B. "vars oil")
|
||||||
foreach (string line in mp.DescribeVehicleValues(args.Length > 1 ? args[1] : "")) print(line);
|
foreach (string line in mp.DescribeVehicleValues(args.Length > 1 ? args[1] : "")) print(line);
|
||||||
break;
|
break;
|
||||||
|
case "oilcheck":
|
||||||
|
foreach (string line in OilCheck.Report()) print(line);
|
||||||
|
break;
|
||||||
case "debug":
|
case "debug":
|
||||||
FsmSync.Debug = !FsmSync.Debug;
|
FsmSync.Debug = !FsmSync.Debug;
|
||||||
print("Sync debug output " + (FsmSync.Debug ? "ON" : "OFF"));
|
print("Sync debug output " + (FsmSync.Debug ? "ON" : "OFF"));
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ using System.Collections.Generic;
|
|||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using MSCLoader;
|
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
|
|
||||||
namespace MSCO
|
namespace MSCO
|
||||||
|
|||||||
+130
-3
@@ -2,7 +2,6 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using HutongGames.PlayMaker;
|
using HutongGames.PlayMaker;
|
||||||
using MSCLoader;
|
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
|
|
||||||
namespace MSCO
|
namespace MSCO
|
||||||
@@ -33,8 +32,19 @@ namespace MSCO
|
|||||||
public PlayMakerFSM Fsm;
|
public PlayMakerFSM Fsm;
|
||||||
public string LastState;
|
public string LastState;
|
||||||
public float QuietUntil;
|
public float QuietUntil;
|
||||||
|
// Autotür (FSM mit Bool "Open" und Zuständen "Open door"/"Close door"): übertragen wird nur offen/zu
|
||||||
|
public bool IsCarDoor;
|
||||||
|
public FsmBool OpenVar;
|
||||||
|
public bool LastOpen;
|
||||||
|
public GameObject Door;
|
||||||
|
public HingeJoint Hinge;
|
||||||
|
public bool HasOpenLimits;
|
||||||
|
public float OpenMin, OpenMax;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Statt Zustandsnamen: offen/zu einer Autotür (ältere Mods kennen diese "Zustände" nicht und ignorieren sie)
|
||||||
|
const string OpenToken = "__open", ClosedToken = "__closed";
|
||||||
|
|
||||||
readonly Dictionary<string, Tracked> byKey = new Dictionary<string, Tracked>();
|
readonly Dictionary<string, Tracked> byKey = new Dictionary<string, Tracked>();
|
||||||
float nextScan;
|
float nextScan;
|
||||||
|
|
||||||
@@ -58,6 +68,18 @@ namespace MSCO
|
|||||||
foreach (Tracked t in byKey.Values)
|
foreach (Tracked t in byKey.Values)
|
||||||
{
|
{
|
||||||
if (t.Fsm == null) continue;
|
if (t.Fsm == null) continue;
|
||||||
|
if (t.IsCarDoor)
|
||||||
|
{
|
||||||
|
bool open = t.OpenVar.Value;
|
||||||
|
if (open == t.LastOpen) continue;
|
||||||
|
t.LastOpen = open;
|
||||||
|
if (now < t.QuietUntil) continue; // Folge einer übernommenen Änderung
|
||||||
|
t.QuietUntil = now + Cooldown;
|
||||||
|
string doorKey = t.Key, token = open ? OpenToken : ClosedToken;
|
||||||
|
send(MsgType.DoorState, NetSession.Build(w => { w.Write(doorKey); w.Write(token); }));
|
||||||
|
if (FsmSync.Debug) MscoLog.Info("[MSCO] >> CAR DOOR " + doorKey + " -> " + token);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
string state = t.Fsm.ActiveStateName;
|
string state = t.Fsm.ActiveStateName;
|
||||||
if (state == t.LastState) continue;
|
if (state == t.LastState) continue;
|
||||||
t.LastState = state;
|
t.LastState = state;
|
||||||
@@ -92,6 +114,14 @@ namespace MSCO
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (t.IsCarDoor)
|
||||||
|
{
|
||||||
|
// Ältere Mods schicken Zustandsnamen: die des Zumachens als "zu" verstehen, den Rest ignorieren
|
||||||
|
bool closing = state == ClosedToken || state == "Sound" || state == "Close door" || state == "State 1";
|
||||||
|
if (state == OpenToken || closing) ApplyCarDoor(t, state == OpenToken);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (t.Fsm.ActiveStateName == state) return;
|
if (t.Fsm.ActiveStateName == state) return;
|
||||||
FsmState target = t.Fsm.Fsm.GetState(state);
|
FsmState target = t.Fsm.Fsm.GetState(state);
|
||||||
if (target == null) return;
|
if (target == null) return;
|
||||||
@@ -113,11 +143,108 @@ namespace MSCO
|
|||||||
|
|
||||||
string key = FsmSync.PathOf(fsm.transform) + "|" + fsm.FsmName;
|
string key = FsmSync.PathOf(fsm.transform) + "|" + fsm.FsmName;
|
||||||
Tracked t;
|
Tracked t;
|
||||||
if (byKey.TryGetValue(key, out t) && t.Fsm == fsm) continue;
|
if (byKey.TryGetValue(key, out t) && t.Fsm == fsm)
|
||||||
byKey[key] = new Tracked { Key = key, Fsm = fsm, LastState = fsm.ActiveStateName };
|
{
|
||||||
|
if (t.IsCarDoor && !t.OpenVar.Value) RemoveExtraFixedJoints(t);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
t = new Tracked { Key = key, Fsm = fsm, LastState = fsm.ActiveStateName };
|
||||||
|
DetectCarDoor(t);
|
||||||
|
byKey[key] = t;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------- Autotüren
|
||||||
|
// Alle Autotüren im Spiel: Bool "Open", "Open door" (Halte-Joint weg, Scharnier-Grenzen auf, Drehmoment solange
|
||||||
|
// die Maus gedrückt ist) und "Close door" (zu, Grenzen 0..0 bzw. beim Satsuma ein neuer FixedJoint). "Open door"
|
||||||
|
// prüft die Maustaste und wurde deshalb nie übertragen - beim Mitspieler blieb die Tür fest. Und jedes übertragene
|
||||||
|
// Zumachen einer schon geschlossenen Satsuma-Tür hängte einen weiteren FixedJoint an - beim nächsten Öffnen wurde
|
||||||
|
// nur einer entfernt und die Tür ging gar nicht mehr auf.
|
||||||
|
|
||||||
|
static void DetectCarDoor(Tracked t)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Fsm f = t.Fsm.Fsm;
|
||||||
|
FsmState openState = f.GetState("Open door");
|
||||||
|
if (openState == null || f.GetState("Close door") == null) return;
|
||||||
|
FsmBool open = t.Fsm.FsmVariables.FindFsmBool("Open");
|
||||||
|
if (open == null) return;
|
||||||
|
FsmGameObject door = t.Fsm.FsmVariables.FindFsmGameObject("Door");
|
||||||
|
t.Door = door != null && door.Value != null ? door.Value : t.Fsm.gameObject;
|
||||||
|
t.Hinge = t.Door.GetComponent<HingeJoint>();
|
||||||
|
t.OpenVar = open;
|
||||||
|
t.LastOpen = open.Value;
|
||||||
|
t.IsCarDoor = true;
|
||||||
|
|
||||||
|
// Grenzen beim Öffnen: aus der Aktion "SetHingeJointLimits" des Zustands "Open door"
|
||||||
|
if (!openState.ActionsLoaded) openState.LoadActions();
|
||||||
|
if (openState.Actions == null) return;
|
||||||
|
foreach (FsmStateAction a in openState.Actions)
|
||||||
|
{
|
||||||
|
if (a == null || a.GetType().Name != "SetHingeJointLimits") continue;
|
||||||
|
var min = a.GetType().GetField("min") != null ? a.GetType().GetField("min").GetValue(a) as FsmFloat : null;
|
||||||
|
var max = a.GetType().GetField("max") != null ? a.GetType().GetField("max").GetValue(a) as FsmFloat : null;
|
||||||
|
if (min == null || max == null) continue;
|
||||||
|
t.OpenMin = min.Value;
|
||||||
|
t.OpenMax = max.Value;
|
||||||
|
t.HasOpenLimits = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
t.IsCarDoor = false;
|
||||||
|
MscoLog.Warning("[MSCO] Car door " + t.Key + ": " + e.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Tür auf- bzw. zumachen wie beim Spieler, der sie bedient hat.</summary>
|
||||||
|
void ApplyCarDoor(Tracked t, bool open)
|
||||||
|
{
|
||||||
|
if (t.Door == null) return;
|
||||||
|
t.QuietUntil = Time.realtimeSinceStartup + Cooldown;
|
||||||
|
t.LastOpen = open;
|
||||||
|
if (open)
|
||||||
|
{
|
||||||
|
if (t.OpenVar.Value && t.Door.GetComponent<FixedJoint>() == null) return; // schon offen
|
||||||
|
foreach (FixedJoint j in t.Door.GetComponents<FixedJoint>()) UnityEngine.Object.Destroy(j);
|
||||||
|
if (t.Hinge != null && t.HasOpenLimits)
|
||||||
|
{
|
||||||
|
JointLimits l = t.Hinge.limits;
|
||||||
|
l.min = t.OpenMin;
|
||||||
|
l.max = t.OpenMax;
|
||||||
|
t.Hinge.limits = l;
|
||||||
|
t.Hinge.useLimits = true;
|
||||||
|
}
|
||||||
|
t.OpenVar.Value = true;
|
||||||
|
Rigidbody rb = t.Door.GetComponent<Rigidbody>();
|
||||||
|
if (rb != null) rb.WakeUp();
|
||||||
|
// Ruht die FSM (nicht gerade selbst an der Tür): neu prüfen lassen, damit sie "offen" kennt (Innenlicht ...)
|
||||||
|
string active = t.Fsm.ActiveStateName ?? "";
|
||||||
|
FsmState idle = t.Fsm.Fsm.GetState("Mouse off");
|
||||||
|
if (idle != null && (active.StartsWith("Mouse") || active == "Check position")) FsmSync.SwitchStateSilently(t.Fsm, idle);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (!t.OpenVar.Value) { RemoveExtraFixedJoints(t); return; } // schon zu
|
||||||
|
// Halte-Joints weg, dann die Schließ-Logik des Spiels (Geräusch, zu, beim Satsuma genau EIN neuer FixedJoint)
|
||||||
|
foreach (FixedJoint j in t.Door.GetComponents<FixedJoint>()) UnityEngine.Object.Destroy(j);
|
||||||
|
FsmState close = t.Fsm.Fsm.GetState("Sound") ?? t.Fsm.Fsm.GetState("Close door");
|
||||||
|
if (close != null) FsmSync.SwitchStateSilently(t.Fsm, close);
|
||||||
|
}
|
||||||
|
if (FsmSync.Debug) MscoLog.Info("[MSCO] << CAR DOOR " + t.Key + " -> " + (open ? "open" : "closed"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Mehrere FixedJoints an einer geschlossenen Tür (alter Fehler): bis auf einen entfernen.</summary>
|
||||||
|
static void RemoveExtraFixedJoints(Tracked t)
|
||||||
|
{
|
||||||
|
if (t.Door == null) return;
|
||||||
|
FixedJoint[] joints = t.Door.GetComponents<FixedJoint>();
|
||||||
|
if (joints.Length <= 1) return;
|
||||||
|
for (int i = 1; i < joints.Length; i++) UnityEngine.Object.Destroy(joints[i]);
|
||||||
|
MscoLog.Info("[MSCO] Fixed a stuck door (" + (joints.Length - 1) + " extra lock(s) removed): " + t.Door.name);
|
||||||
|
}
|
||||||
|
|
||||||
static bool IsPartLogic(PlayMakerFSM fsm)
|
static bool IsPartLogic(PlayMakerFSM fsm)
|
||||||
{
|
{
|
||||||
string f = fsm.FsmName.ToLower(), o = fsm.gameObject.name.ToLower();
|
string f = fsm.FsmName.ToLower(), o = fsm.gameObject.name.ToLower();
|
||||||
|
|||||||
+142
@@ -0,0 +1,142 @@
|
|||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Text;
|
||||||
|
using HutongGames.PlayMaker;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace MSCO
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Diagnose: PlayMaker-FSMs (Zustände, Aktionen mit Werten, Übergänge) und Scharniere von Objekten, deren Pfad einen
|
||||||
|
/// Suchtext enthält, in MSCO\fsmdump.txt schreiben. Konsole (F7): "fsmdump <text>". Mit der Startoption
|
||||||
|
/// "-msco-dump text1,text2" automatisch 25 s nach dem Laden des Spiels.
|
||||||
|
/// </summary>
|
||||||
|
public static class FsmDump
|
||||||
|
{
|
||||||
|
public static string Write(string filter)
|
||||||
|
{
|
||||||
|
string[] filters = (filter ?? "").ToLowerInvariant().Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
if (filter != null && filter.Trim().ToLowerInvariant() == "pivots") return WritePivots();
|
||||||
|
if (filters.Length == 0) return "fsmdump <text>[,text2] - e.g. fsmdump garage";
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
int count = 0;
|
||||||
|
foreach (PlayMakerFSM fsm in Resources.FindObjectsOfTypeAll<PlayMakerFSM>())
|
||||||
|
{
|
||||||
|
if (fsm == null || fsm.gameObject == null || fsm.gameObject.hideFlags != HideFlags.None) continue;
|
||||||
|
string path, lower;
|
||||||
|
try { path = FsmSync.PathOf(fsm.transform); lower = (path + "|" + fsm.FsmName).ToLowerInvariant(); }
|
||||||
|
catch { continue; }
|
||||||
|
if (Array.FindIndex(filters, f => lower.Contains(f.Trim())) < 0) continue;
|
||||||
|
if (++count > 400) break;
|
||||||
|
try { Dump(sb, fsm, path); }
|
||||||
|
catch (Exception e) { sb.Append(" (error: ").Append(e.Message).Append(")").AppendLine(); }
|
||||||
|
}
|
||||||
|
foreach (HingeJoint j in Resources.FindObjectsOfTypeAll<HingeJoint>())
|
||||||
|
{
|
||||||
|
if (j == null || j.gameObject.hideFlags != HideFlags.None) continue;
|
||||||
|
string path = FsmSync.PathOf(j.transform);
|
||||||
|
if (Array.FindIndex(filters, f => path.ToLowerInvariant().Contains(f.Trim())) < 0) continue;
|
||||||
|
Rigidbody rb = j.GetComponent<Rigidbody>();
|
||||||
|
try {
|
||||||
|
sb.Append("HINGE ").Append(path).Append(" -> ").Append(j.connectedBody != null ? FsmSync.PathOf(j.connectedBody.transform) : "-")
|
||||||
|
.Append(" | active ").Append(j.gameObject.activeInHierarchy).Append(" angle ").Append(j.angle.ToString("0.0"))
|
||||||
|
.Append(" limits ").Append(j.useLimits).Append(' ').Append(j.limits.min.ToString("0")).Append("..").Append(j.limits.max.ToString("0"))
|
||||||
|
.Append(" spring ").Append(j.useSpring).Append(' ').Append(j.spring.spring.ToString("0")).Append('/').Append(j.spring.targetPosition.ToString("0"))
|
||||||
|
.Append(" motor ").Append(j.useMotor)
|
||||||
|
.Append(" kinematic ").Append(rb != null && rb.isKinematic).Append(" break ").Append(j.breakForce).AppendLine();
|
||||||
|
} catch (Exception e) { sb.Append(" (error: ").Append(e.Message).Append(")").AppendLine(); }
|
||||||
|
}
|
||||||
|
string file = Path.Combine(Bootstrap.Folder ?? ".", "fsmdump.txt");
|
||||||
|
File.WriteAllText(file, sb.ToString());
|
||||||
|
return count + " FSM(s) written to " + file;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Eingebaute Teile: Lage relativ zu ihrem Halter (pivot_...) - ist sie immer 0?</summary>
|
||||||
|
static string WritePivots()
|
||||||
|
{
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
int count = 0;
|
||||||
|
foreach (Transform t in Resources.FindObjectsOfTypeAll<Transform>())
|
||||||
|
{
|
||||||
|
if (t == null || t.gameObject.hideFlags != HideFlags.None || !t.name.StartsWith("pivot_")) continue;
|
||||||
|
foreach (Transform child in t)
|
||||||
|
{
|
||||||
|
count++;
|
||||||
|
sb.Append(FsmSync.PathOf(child)).Append(" | pos ").Append(child.localPosition.ToString("F4"))
|
||||||
|
.Append(" rot ").Append(child.localEulerAngles.ToString("F1")).Append(" active ").Append(child.gameObject.activeInHierarchy).AppendLine();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
string file = Path.Combine(Bootstrap.Folder ?? ".", "fsmdump.txt");
|
||||||
|
File.WriteAllText(file, sb.ToString());
|
||||||
|
return count + " installed part(s) written to " + file;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void Dump(StringBuilder sb, PlayMakerFSM fsm, string path)
|
||||||
|
{
|
||||||
|
sb.Append("==== ").Append(path).Append(" | ").Append(fsm.FsmName).Append(" | active ").Append(fsm.gameObject.activeInHierarchy)
|
||||||
|
.Append(" | state ").Append(fsm.ActiveStateName).AppendLine();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
FsmVariables fv = fsm.FsmVariables;
|
||||||
|
var vb = new StringBuilder();
|
||||||
|
foreach (FsmFloat f in fv.FloatVariables) vb.Append(" ").Append(f.Name).Append("=").Append(f.Value.ToString("0.###"));
|
||||||
|
foreach (FsmInt n in fv.IntVariables) vb.Append(" ").Append(n.Name).Append("=").Append(n.Value);
|
||||||
|
foreach (FsmBool b in fv.BoolVariables) vb.Append(" ").Append(b.Name).Append("=").Append(b.Value);
|
||||||
|
foreach (FsmString t in fv.StringVariables) vb.Append(" ").Append(t.Name).Append("=\"").Append(t.Value).Append("\"");
|
||||||
|
if (vb.Length > 0) sb.Append(" VARS").Append(vb).AppendLine();
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
if (fsm.FsmGlobalTransitions != null)
|
||||||
|
foreach (FsmTransition t in fsm.FsmGlobalTransitions)
|
||||||
|
sb.Append(" GLOBAL ").Append(t.EventName).Append(" -> ").Append(t.ToState).AppendLine();
|
||||||
|
if (fsm.FsmStates == null) return;
|
||||||
|
foreach (FsmState s in fsm.FsmStates)
|
||||||
|
{
|
||||||
|
if (s == null) continue;
|
||||||
|
if (!s.ActionsLoaded) s.LoadActions();
|
||||||
|
sb.Append(" [").Append(s.Name).Append(']');
|
||||||
|
if (s.Transitions != null) foreach (FsmTransition t in s.Transitions) if (t != null) sb.Append(" ").Append(t.EventName).Append("->").Append(t.ToState);
|
||||||
|
sb.AppendLine();
|
||||||
|
if (s.Actions == null) continue;
|
||||||
|
foreach (FsmStateAction a in s.Actions)
|
||||||
|
{
|
||||||
|
if (a == null) continue;
|
||||||
|
sb.Append(" ").Append(a.GetType().Name).Append(Fields(a)).AppendLine();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static string Fields(FsmStateAction a)
|
||||||
|
{
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
foreach (FieldInfo f in a.GetType().GetFields(BindingFlags.Public | BindingFlags.Instance))
|
||||||
|
{
|
||||||
|
object v;
|
||||||
|
try { v = f.GetValue(a); } catch { continue; }
|
||||||
|
string text = Describe(v);
|
||||||
|
if (text == null) continue;
|
||||||
|
sb.Append(' ').Append(f.Name).Append('=').Append(text);
|
||||||
|
}
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
static string Describe(object v)
|
||||||
|
{
|
||||||
|
if (v == null) return null;
|
||||||
|
var named = v as NamedVariable;
|
||||||
|
if (named != null)
|
||||||
|
{
|
||||||
|
string value;
|
||||||
|
try { value = Convert.ToString(v.GetType().GetProperty("Value").GetValue(v, null)); } catch { value = "?"; }
|
||||||
|
return (string.IsNullOrEmpty(named.Name) ? "" : "{" + named.Name + "}") + value;
|
||||||
|
}
|
||||||
|
var ev = v as FsmEvent;
|
||||||
|
if (ev != null) return "event:" + ev.Name;
|
||||||
|
var owner = v as FsmOwnerDefault;
|
||||||
|
if (owner != null) return owner.OwnerOption == OwnerDefaultOption.UseOwner ? "owner" : Describe(owner.GameObject);
|
||||||
|
if (v is string || v is bool || v is int || v is float || v.GetType().IsEnum) return Convert.ToString(v);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+49
-6
@@ -4,7 +4,6 @@ using System.IO;
|
|||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using Harmony;
|
using Harmony;
|
||||||
using HutongGames.PlayMaker;
|
using HutongGames.PlayMaker;
|
||||||
using MSCLoader;
|
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
|
|
||||||
namespace MSCO
|
namespace MSCO
|
||||||
@@ -133,6 +132,7 @@ namespace MSCO
|
|||||||
|
|
||||||
static void SwitchStatePrefix(Fsm __instance, FsmState toState)
|
static void SwitchStatePrefix(Fsm __instance, FsmState toState)
|
||||||
{
|
{
|
||||||
|
PerfStats.SwitchStateCalls++;
|
||||||
if (!Enabled || applying > 0 || toState == null) return;
|
if (!Enabled || applying > 0 || toState == null) return;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -167,18 +167,25 @@ namespace MSCO
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static Camera aimCamera;
|
||||||
|
static int aimCameraFrame = -1;
|
||||||
|
|
||||||
static bool IsAimedAt(Fsm fsm, out float aimDistance)
|
static bool IsAimedAt(Fsm fsm, out float aimDistance)
|
||||||
{
|
{
|
||||||
aimDistance = float.MaxValue;
|
aimDistance = float.MaxValue;
|
||||||
GameObject go = fsm.GameObject;
|
GameObject go = fsm.GameObject;
|
||||||
Camera cam = Camera.main;
|
// Läuft im Klick-Fenster für jede FSM, die den Zustand wechselt: Kamera nur einmal pro Bild suchen
|
||||||
|
if (aimCameraFrame != Time.frameCount) { aimCameraFrame = Time.frameCount; aimCamera = Camera.main; }
|
||||||
|
Camera cam = aimCamera;
|
||||||
if (go == null || cam == null) return false;
|
if (go == null || cam == null) return false;
|
||||||
|
|
||||||
|
// Erst der billige Abstand, dann die Namen (jeder .name-Zugriff erzeugt einen neuen String)
|
||||||
|
Transform camT = cam.transform;
|
||||||
|
Vector3 toObject = go.transform.position - camT.position;
|
||||||
|
if (toObject.sqrMagnitude > ClickRange * ClickRange || Vector3.Dot(toObject, camT.forward) < 0f) return false;
|
||||||
string root = go.transform.root.name;
|
string root = go.transform.root.name;
|
||||||
if (root == "PLAYER" || root.StartsWith("MSCO_")) return false;
|
if (root == "PLAYER" || root.StartsWith("MSCO_")) return false;
|
||||||
|
aimDistance = Vector3.Cross(camT.forward, toObject).magnitude; // Abstand zur Blickachse
|
||||||
Vector3 toObject = go.transform.position - cam.transform.position;
|
|
||||||
if (toObject.magnitude > ClickRange || Vector3.Dot(toObject, cam.transform.forward) < 0f) return false;
|
|
||||||
aimDistance = Vector3.Cross(cam.transform.forward, toObject).magnitude; // Abstand zur Blickachse
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -236,6 +243,7 @@ namespace MSCO
|
|||||||
|
|
||||||
static void EventPrefix(Fsm __instance, FsmEvent fsmEvent)
|
static void EventPrefix(Fsm __instance, FsmEvent fsmEvent)
|
||||||
{
|
{
|
||||||
|
PerfStats.EventCalls++;
|
||||||
if (!Enabled || applying > 0 || fsmEvent == null) return;
|
if (!Enabled || applying > 0 || fsmEvent == null) return;
|
||||||
if (Array.IndexOf(SyncedEvents, fsmEvent.Name) < 0) return;
|
if (Array.IndexOf(SyncedEvents, fsmEvent.Name) < 0) return;
|
||||||
|
|
||||||
@@ -291,6 +299,12 @@ namespace MSCO
|
|||||||
if (Debug) MscoLog.Warning("[MSCO] << not found: " + msg.Path + " [" + msg.FsmName + "]");
|
if (Debug) MscoLog.Warning("[MSCO] << not found: " + msg.Path + " [" + msg.FsmName + "]");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (!Allowed(fsm, msg))
|
||||||
|
{
|
||||||
|
if (Debug) MscoLog.Warning("[MSCO] << refused: " + msg.EventName + " " + msg.Path + " [" + msg.FsmName + " -> " + msg.State + "]");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (msg.EventName == VehicleAdmin.InstallEvent) { VehicleAdmin.InstallRemote(fsm); return; } // Admin hat ein Teil eingebaut
|
||||||
FsmState state = fsm.Fsm.GetState(msg.State);
|
FsmState state = fsm.Fsm.GetState(msg.State);
|
||||||
if (state == null || switchState == null) return;
|
if (state == null || switchState == null) return;
|
||||||
|
|
||||||
@@ -302,6 +316,35 @@ namespace MSCO
|
|||||||
finally { applying--; }
|
finally { applying--; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Darf ein Mitspieler diese FSM in diesen Zustand schalten? Sonst könnte ein manipulierter Client beliebige
|
||||||
|
/// Zustände bei allen anderen auslösen (z.B. Tod oder Speichern). Nie: der eigene Spieler (PLAYER), Systeme
|
||||||
|
/// (Tod, Speichern ...), MSCO selbst. Klicks nur auf FSMs, die auf einen Klick warten; Schrauben/Montage nur
|
||||||
|
/// über einen Übergang, den die FSM für genau dieses Event auch hat.
|
||||||
|
/// </summary>
|
||||||
|
static bool Allowed(PlayMakerFSM fsm, FsmEventMsg msg)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string root = fsm.transform.root.name;
|
||||||
|
if (root == "PLAYER" || root == "Systems" || root.StartsWith("MSCO_")) return false;
|
||||||
|
if (msg.EventName == VehicleAdmin.InstallEvent) return fsm.FsmName == "Assembly";
|
||||||
|
if (msg.EventName == "CLICK")
|
||||||
|
{
|
||||||
|
foreach (FsmState s in fsm.FsmStates) if (ChecksInput(s)) return fsm.Fsm.GetState(msg.State) != null;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (Array.IndexOf(SyncedEvents, msg.EventName) < 0) return false;
|
||||||
|
foreach (FsmState s in fsm.FsmStates)
|
||||||
|
foreach (FsmTransition t in s.Transitions)
|
||||||
|
if (t.EventName == msg.EventName && t.ToState == msg.State) return true;
|
||||||
|
foreach (FsmTransition t in fsm.Fsm.GlobalTransitions)
|
||||||
|
if (t.EventName == msg.EventName && t.ToState == msg.State) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
catch { return false; }
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>FSM in einen Zustand schalten, ohne dass das als eigene Aktion erkannt und gesendet wird.</summary>
|
/// <summary>FSM in einen Zustand schalten, ohne dass das als eigene Aktion erkannt und gesendet wird.</summary>
|
||||||
public static void SwitchStateSilently(PlayMakerFSM fsm, FsmState state)
|
public static void SwitchStateSilently(PlayMakerFSM fsm, FsmState state)
|
||||||
{
|
{
|
||||||
|
|||||||
+25
-5
@@ -40,6 +40,7 @@ namespace MSCO
|
|||||||
public float TargetTime = -100f;
|
public float TargetTime = -100f;
|
||||||
public bool Driving; // Feder gerade von uns gesetzt
|
public bool Driving; // Feder gerade von uns gesetzt
|
||||||
public float LocalAuthorityUntil = -100f;
|
public float LocalAuthorityUntil = -100f;
|
||||||
|
public Collider Collider; // für den Abstand zum Spieler (große Türen wie das Garagentor)
|
||||||
public bool OrigUseSpring;
|
public bool OrigUseSpring;
|
||||||
public JointSpring OrigSpring;
|
public JointSpring OrigSpring;
|
||||||
}
|
}
|
||||||
@@ -102,10 +103,13 @@ namespace MSCO
|
|||||||
/// <summary>Jedes Frame: Klick (linke Maustaste / Benutzen) in der Nähe einer Tür = wir bewegen sie gerade.</summary>
|
/// <summary>Jedes Frame: Klick (linke Maustaste / Benutzen) in der Nähe einer Tür = wir bewegen sie gerade.</summary>
|
||||||
public void NoteInteraction(Vector3 playerPos)
|
public void NoteInteraction(Vector3 playerPos)
|
||||||
{
|
{
|
||||||
if (!Input.GetMouseButtonDown(0) && !Input.GetKeyDown(KeyCode.F)) return;
|
// Links öffnet, rechts schließt (Garagentor), F = Benutzen
|
||||||
|
if (!Input.GetMouseButtonDown(0) && !Input.GetMouseButtonDown(1) && !Input.GetKeyDown(KeyCode.F)) return;
|
||||||
foreach (Hinge h in hinges.Values)
|
foreach (Hinge h in hinges.Values)
|
||||||
{
|
{
|
||||||
if (h.Rb == null || (h.Rb.worldCenterOfMass - playerPos).sqrMagnitude > InteractRange * InteractRange) continue;
|
if (h.Rb == null) continue;
|
||||||
|
Vector3 near = h.Collider != null ? h.Collider.ClosestPointOnBounds(playerPos) : h.Rb.worldCenterOfMass;
|
||||||
|
if ((near - playerPos).sqrMagnitude > InteractRange * InteractRange) continue;
|
||||||
h.LocalAuthorityUntil = Time.time + InteractAuthority;
|
h.LocalAuthorityUntil = Time.time + InteractAuthority;
|
||||||
h.TargetTime = -100f; // eigene Bewegung: keine Feder mehr auf den alten Winkel
|
h.TargetTime = -100f; // eigene Bewegung: keine Feder mehr auf den alten Winkel
|
||||||
}
|
}
|
||||||
@@ -183,10 +187,17 @@ namespace MSCO
|
|||||||
if (j == null || !seen.Add(j)) continue;
|
if (j == null || !seen.Add(j)) continue;
|
||||||
Rigidbody rb = j.GetComponent<Rigidbody>();
|
Rigidbody rb = j.GetComponent<Rigidbody>();
|
||||||
Rigidbody body = j.connectedBody;
|
Rigidbody body = j.connectedBody;
|
||||||
if (rb == null || body == null || !IsVehicle(body.transform.root.name)) continue;
|
if (rb == null) continue;
|
||||||
int key = Hash(FsmSync.PathOf(j.transform) + "|" + FsmSync.PathOf(body.transform));
|
// Fahrzeugtüren/-hauben - und Türen/Tore der Welt mit Scharnier (z.B. das Garagentor)
|
||||||
|
bool vehicle = body != null && IsVehicle(body.transform.root.name);
|
||||||
|
if (!vehicle && !IsWorldDoor(j)) continue;
|
||||||
|
int key = Hash(FsmSync.PathOf(j.transform) + "|" + (body != null ? FsmSync.PathOf(body.transform) : "world"));
|
||||||
if (hinges.ContainsKey(key)) continue;
|
if (hinges.ContainsKey(key)) continue;
|
||||||
hinges[key] = new Hinge { Key = key, Root = body.transform.root.name, Joint = j, Rb = rb };
|
hinges[key] = new Hinge
|
||||||
|
{
|
||||||
|
Key = key, Root = (body != null ? body.transform : j.transform).root.name, Joint = j, Rb = rb,
|
||||||
|
Collider = rb.GetComponentInChildren<Collider>()
|
||||||
|
};
|
||||||
}
|
}
|
||||||
// Abgebaute Teile (Scharnier weg) vergessen
|
// Abgebaute Teile (Scharnier weg) vergessen
|
||||||
var gone = new List<int>();
|
var gone = new List<int>();
|
||||||
@@ -197,6 +208,15 @@ namespace MSCO
|
|||||||
if (hinges.Count != before) MscoLog.Info("[MSCO] Vehicle doors/hood/trunk: " + hinges.Count + " synced");
|
if (hinges.Count != before) MscoLog.Info("[MSCO] Vehicle doors/hood/trunk: " + hinges.Count + " synced");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Tür/Tor der Welt (nicht Verkehr/NPC-Autos, nicht der Spieler): Name enthält door/gate.</summary>
|
||||||
|
static bool IsWorldDoor(HingeJoint j)
|
||||||
|
{
|
||||||
|
string root = j.transform.root.name;
|
||||||
|
if (root == "PLAYER" || root == "NPC_CARS" || root == "TRAFFIC" || root.StartsWith("MSCO_")) return false;
|
||||||
|
string path = FsmSync.PathOf(j.transform).ToLowerInvariant();
|
||||||
|
return path.Contains("door") || path.Contains("gate");
|
||||||
|
}
|
||||||
|
|
||||||
static bool IsVehicle(string root)
|
static bool IsVehicle(string root)
|
||||||
{
|
{
|
||||||
foreach (string v in ObjectSync.VehicleRoots) if (root.StartsWith(v)) return true;
|
foreach (string v in ObjectSync.VehicleRoots) if (root.StartsWith(v)) return true;
|
||||||
|
|||||||
+17
-2
@@ -2,7 +2,7 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using Harmony;
|
using Harmony;
|
||||||
using MSCLoader;
|
using HutongGames.PlayMaker;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
|
|
||||||
namespace MSCO
|
namespace MSCO
|
||||||
@@ -21,7 +21,7 @@ namespace MSCO
|
|||||||
|
|
||||||
static readonly List<Behaviour> disabled = new List<Behaviour>();
|
static readonly List<Behaviour> disabled = new List<Behaviour>();
|
||||||
static bool locked;
|
static bool locked;
|
||||||
static bool previousVisible;
|
static bool previousVisible, previousInMenu;
|
||||||
static CursorLockMode previousLock;
|
static CursorLockMode previousLock;
|
||||||
static bool patched;
|
static bool patched;
|
||||||
|
|
||||||
@@ -38,6 +38,8 @@ namespace MSCO
|
|||||||
PatchCursorActions();
|
PatchCursorActions();
|
||||||
previousVisible = Cursor.visible;
|
previousVisible = Cursor.visible;
|
||||||
previousLock = Cursor.lockState;
|
previousLock = Cursor.lockState;
|
||||||
|
FsmBool inMenu = PlayerInMenu();
|
||||||
|
previousInMenu = inMenu != null && inMenu.Value;
|
||||||
GameObject player = GameObject.Find("PLAYER");
|
GameObject player = GameObject.Find("PLAYER");
|
||||||
if (player != null)
|
if (player != null)
|
||||||
{
|
{
|
||||||
@@ -58,6 +60,8 @@ namespace MSCO
|
|||||||
PlayMakerGUI.HideCursor = !previousVisible;
|
PlayMakerGUI.HideCursor = !previousVisible;
|
||||||
Cursor.visible = previousVisible;
|
Cursor.visible = previousVisible;
|
||||||
Cursor.lockState = previousLock;
|
Cursor.lockState = previousLock;
|
||||||
|
FsmBool inMenu = PlayerInMenu();
|
||||||
|
if (inMenu != null) inMenu.Value = previousInMenu;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,6 +76,16 @@ namespace MSCO
|
|||||||
PlayMakerGUI.HideCursor = false;
|
PlayMakerGUI.HideCursor = false;
|
||||||
Cursor.visible = true;
|
Cursor.visible = true;
|
||||||
Cursor.lockState = CursorLockMode.None;
|
Cursor.lockState = CursorLockMode.None;
|
||||||
|
// So zeigt MSC selbst den Mauszeiger (Pausemenü, Kasse ...) - ohne MSCLoader brauchen wir das auch
|
||||||
|
FsmBool inMenu = PlayerInMenu();
|
||||||
|
if (inMenu != null) inMenu.Value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Globale Spielvariable "PlayerInMenu": true = Zeiger frei, Mausblick aus.</summary>
|
||||||
|
static FsmBool PlayerInMenu()
|
||||||
|
{
|
||||||
|
try { return FsmVariables.GlobalVariables.FindFsmBool("PlayerInMenu"); }
|
||||||
|
catch { return null; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -137,6 +151,7 @@ namespace MSCO
|
|||||||
// Harmony-Prefixe: solange ein Eingabefenster offen ist, meldet cInput "nichts gedrückt"
|
// Harmony-Prefixe: solange ein Eingabefenster offen ist, meldet cInput "nichts gedrückt"
|
||||||
static bool NoKeyWhileLocked(ref bool __result)
|
static bool NoKeyWhileLocked(ref bool __result)
|
||||||
{
|
{
|
||||||
|
PerfStats.InputCalls++;
|
||||||
if (!locked) return true;
|
if (!locked) return true;
|
||||||
__result = false;
|
__result = false;
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
+31
-2
@@ -1,6 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
using Microsoft.Win32;
|
using Microsoft.Win32;
|
||||||
using MSCLoader;
|
|
||||||
|
|
||||||
namespace MSCO
|
namespace MSCO
|
||||||
{
|
{
|
||||||
@@ -9,7 +8,7 @@ namespace MSCO
|
|||||||
/// Die Mod registriert den Link-Typ in Windows so, dass ein Klick Steam direkt mit
|
/// Die Mod registriert den Link-Typ in Windows so, dass ein Klick Steam direkt mit
|
||||||
/// "-applaunch 516750 +msco_connect <link>" startet - ohne cmd/Skript dazwischen, damit
|
/// "-applaunch 516750 +msco_connect <link>" startet - ohne cmd/Skript dazwischen, damit
|
||||||
/// präparierte Links keine Befehle ausführen können. Die Mod liest die Adresse beim Start
|
/// präparierte Links keine Befehle ausführen können. Die Mod liest die Adresse beim Start
|
||||||
/// aus der Kommandozeile und fragt im Hauptmenü, ob verbunden werden soll.
|
/// aus der Kommandozeile und verbindet im Hauptmenü sofort (Server-Mods fragen weiterhin nach).
|
||||||
/// (Läuft das Spiel schon, gibt Steam den Link nicht weiter - dann den Server-Browser nutzen.)
|
/// (Läuft das Spiel schon, gibt Steam den Link nicht weiter - dann den Server-Browser nutzen.)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class JoinLinks
|
public static class JoinLinks
|
||||||
@@ -31,6 +30,13 @@ namespace MSCO
|
|||||||
}
|
}
|
||||||
string command = "\"" + steamExe + "\" -applaunch " + MySummerCarAppId + " " + Argument + " \"%1\"";
|
string command = "\"" + steamExe + "\" -applaunch " + MySummerCarAppId + " " + Argument + " \"%1\"";
|
||||||
|
|
||||||
|
// Ist der MSCO Launcher installiert, gehören die Links ihm: er startet das Spiel mit MSCO (ohne MSCLoader)
|
||||||
|
using (RegistryKey existing = Registry.CurrentUser.OpenSubKey(@"Software\Classes\" + Scheme + @"\shell\open\command"))
|
||||||
|
{
|
||||||
|
string current = existing != null ? existing.GetValue("") as string : null;
|
||||||
|
if (current != null && current.IndexOf("MSCO-Launcher", StringComparison.OrdinalIgnoreCase) >= 0) return;
|
||||||
|
}
|
||||||
|
|
||||||
using (RegistryKey key = Registry.CurrentUser.CreateSubKey(@"Software\Classes\" + Scheme))
|
using (RegistryKey key = Registry.CurrentUser.CreateSubKey(@"Software\Classes\" + Scheme))
|
||||||
{
|
{
|
||||||
key.SetValue("", "URL:MSCO Join Link");
|
key.SetValue("", "URL:MSCO Join Link");
|
||||||
@@ -54,6 +60,29 @@ namespace MSCO
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static float nextPoll;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// JOIN-Link, während das Spiel schon läuft: der MSCO Launcher legt ihn als <Spiel>\MSCO\join.txt ab (Steam
|
||||||
|
/// gibt Links an ein laufendes Spiel nicht weiter). Einmal pro Sekunde nachsehen.
|
||||||
|
/// </summary>
|
||||||
|
public static bool TryTakeRequest(out string host, out int port)
|
||||||
|
{
|
||||||
|
host = null;
|
||||||
|
port = 0;
|
||||||
|
if (UnityEngine.Time.realtimeSinceStartup < nextPoll) return false;
|
||||||
|
nextPoll = UnityEngine.Time.realtimeSinceStartup + 1f;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string file = System.IO.Path.Combine(Bootstrap.Folder ?? "", "join.txt");
|
||||||
|
if (Bootstrap.Folder == null || !System.IO.File.Exists(file)) return false;
|
||||||
|
string text = System.IO.File.ReadAllText(file);
|
||||||
|
System.IO.File.Delete(file);
|
||||||
|
return TryParse(text, out host, out port);
|
||||||
|
}
|
||||||
|
catch { return false; }
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Wurde das Spiel über einen JOIN-Link gestartet?</summary>
|
/// <summary>Wurde das Spiel über einen JOIN-Link gestartet?</summary>
|
||||||
public static bool TryGetFromCommandLine(out string host, out int port)
|
public static bool TryGetFromCommandLine(out string host, out int port)
|
||||||
{
|
{
|
||||||
|
|||||||
+341
-61
@@ -1,4 +1,4 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
|
|
||||||
namespace MSCO
|
namespace MSCO
|
||||||
@@ -10,7 +10,7 @@ namespace MSCO
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class LobbyUI
|
public class LobbyUI
|
||||||
{
|
{
|
||||||
enum Page { P2P, ServerBrowser, Favorites }
|
enum Page { P2P, ServerBrowser, Favorites, Mods }
|
||||||
|
|
||||||
// Vollbild, automatisch skaliert: gebaut für RefHeight x RefWidth, vergrößert/verkleinert passend zur Auflösung
|
// Vollbild, automatisch skaliert: gebaut für RefHeight x RefWidth, vergrößert/verkleinert passend zur Auflösung
|
||||||
const float RefHeight = 760f;
|
const float RefHeight = 760f;
|
||||||
@@ -33,10 +33,10 @@ namespace MSCO
|
|||||||
float ListHeight(float reserved, float min) { return Mathf.Max(min, contentHeight - reserved); }
|
float ListHeight(float reserved, float min) { return Mathf.Max(min, contentHeight - reserved); }
|
||||||
|
|
||||||
|
|
||||||
static readonly Color Background = Hex(0x17, 0x17, 0x1b);
|
static readonly Color Background = Hex(0x11, 0x11, 0x14);
|
||||||
static readonly Color Sidebar = Hex(0x0f, 0x0f, 0x12);
|
static readonly Color Sidebar = Hex(0x0c, 0x0c, 0x0f);
|
||||||
static readonly Color Field = Hex(0x24, 0x24, 0x2a);
|
static readonly Color Field = Hex(0x1b, 0x1b, 0x20);
|
||||||
static readonly Color FieldFocus = Hex(0x2e, 0x2e, 0x36);
|
static readonly Color FieldFocus = Hex(0x24, 0x24, 0x2a);
|
||||||
static readonly Color Accent = Hex(0xf2, 0x6b, 0x1d);
|
static readonly Color Accent = Hex(0xf2, 0x6b, 0x1d);
|
||||||
static readonly Color AccentHover = Hex(0xff, 0x82, 0x3a);
|
static readonly Color AccentHover = Hex(0xff, 0x82, 0x3a);
|
||||||
static readonly Color Secondary = Hex(0x2c, 0x2c, 0x33);
|
static readonly Color Secondary = Hex(0x2c, 0x2c, 0x33);
|
||||||
@@ -54,7 +54,7 @@ namespace MSCO
|
|||||||
bool browserLoaded;
|
bool browserLoaded;
|
||||||
string selectedServer; // "adresse:port" des geöffneten Server-Profils
|
string selectedServer; // "adresse:port" des geöffneten Server-Profils
|
||||||
string searchInput = "";
|
string searchInput = "";
|
||||||
bool showSteam = true, showNonSteam = true, hideFull;
|
bool showSteam = true, showNonSteam = true, hideFull, showVanilla = true, showModded = true;
|
||||||
readonly Dictionary<string, string> nicknameEdits = new Dictionary<string, string>(); // Favoriten-Tab: Adresse -> bearbeiteter Kurzname
|
readonly Dictionary<string, string> nicknameEdits = new Dictionary<string, string>(); // Favoriten-Tab: Adresse -> bearbeiteter Kurzname
|
||||||
string favoriteAddInput = "";
|
string favoriteAddInput = "";
|
||||||
Vector2 favoritesScroll;
|
Vector2 favoritesScroll;
|
||||||
@@ -65,13 +65,21 @@ namespace MSCO
|
|||||||
|
|
||||||
bool stylesReady;
|
bool stylesReady;
|
||||||
Texture2D texBackground, texSidebar, texAccent, texOnline, texPending, texMuted, texRow;
|
Texture2D texBackground, texSidebar, texAccent, texOnline, texPending, texMuted, texRow;
|
||||||
Texture2D texDivider, texGold, texBarBack, texBarFill, texBarFull;
|
Texture2D texDivider, texGold, texBarBack, texBarFill, texBarFull, texGlow;
|
||||||
|
GUIStyle scrollbarStyle, scrollThumbStyle;
|
||||||
GUIStyle rowHoverBox, pingGoodStyle, pingMidStyle, pingBadStyle;
|
GUIStyle rowHoverBox, pingGoodStyle, pingMidStyle, pingBadStyle;
|
||||||
List<ServerListEntry> shownAll, shownServers; // Stand des letzten Layout-Durchgangs
|
List<ServerListEntry> shownAll, shownServers; // Stand des letzten Layout-Durchgangs
|
||||||
string layoutError;
|
string layoutError;
|
||||||
GUIStyle dangerStyle;
|
GUIStyle dangerStyle;
|
||||||
GUIStyle windowBox, sidebarBox, rowBox, accentBox, pillStyle, pillMutedStyle, chipOnStyle, chipOffStyle;
|
GUIStyle windowBox, sidebarBox, rowBox, accentBox, pillStyle, pillMutedStyle, chipOnStyle, chipOffStyle;
|
||||||
GUIStyle featuredRowBox, pillFeaturedStyle;
|
GUIStyle featuredRowBox, pillFeaturedStyle, pillDangerStyle, newsTitleStyle, newsBoxStyle, newsTextStyle, newsCloseStyle;
|
||||||
|
Texture2D texNewsInfo, texNewsWarning, texNewsCritical;
|
||||||
|
// Ankündigungen/Wartung vom Keymaster - nur im Layout-Durchgang übernehmen (Liste kann sich im Hintergrund ändern)
|
||||||
|
List<Announcement> shownNews;
|
||||||
|
bool shownMaintenance;
|
||||||
|
string shownMaintenanceText;
|
||||||
|
readonly HashSet<string> dismissedNews = new HashSet<string>();
|
||||||
|
GUIStyle betaPillStyle, betaNoteStyle;
|
||||||
GUIStyle logoStyle, taglineStyle, navStyle, navActiveStyle, titleStyle, textStyle, captionStyle,
|
GUIStyle logoStyle, taglineStyle, navStyle, navActiveStyle, titleStyle, textStyle, captionStyle,
|
||||||
fieldStyle, primaryStyle, secondaryStyle, statusStyle, tabStyle, headerCellStyle,
|
fieldStyle, primaryStyle, secondaryStyle, statusStyle, tabStyle, headerCellStyle,
|
||||||
rowNameStyle, rowTagStyle, badgeStyle, toggleStyle, placeholderStyle;
|
rowNameStyle, rowTagStyle, badgeStyle, toggleStyle, placeholderStyle;
|
||||||
@@ -105,6 +113,7 @@ namespace MSCO
|
|||||||
if (Minimized && !mp.IsInSession && (mp.DisconnectNotice != null || mp.PendingJoinHost != null || mp.PendingVersionWarning != null))
|
if (Minimized && !mp.IsInSession && (mp.DisconnectNotice != null || mp.PendingJoinHost != null || mp.PendingVersionWarning != null))
|
||||||
Minimized = false;
|
Minimized = false;
|
||||||
if (Minimized && mp.IsReconnecting) Minimized = false;
|
if (Minimized && mp.IsReconnecting) Minimized = false;
|
||||||
|
if (Minimized && mp.PendingMods != null) Minimized = false;
|
||||||
if (Minimized) { DrawMinimized(mp); return; }
|
if (Minimized) { DrawMinimized(mp); return; }
|
||||||
ScreenRect = new Rect(0, 0, Screen.width, Screen.height);
|
ScreenRect = new Rect(0, 0, Screen.width, Screen.height);
|
||||||
DrawScaled(mp);
|
DrawScaled(mp);
|
||||||
@@ -121,8 +130,11 @@ namespace MSCO
|
|||||||
// Getrennt / Neu-Verbinden: eigenes Fenster in der Bildmitte, alles andere gesperrt
|
// Getrennt / Neu-Verbinden: eigenes Fenster in der Bildmitte, alles andere gesperrt
|
||||||
if (disconnected) { DrawPrompt(mp, false); return; }
|
if (disconnected) { DrawPrompt(mp, false); return; }
|
||||||
if (mp.IsReconnecting) { DrawPrompt(mp, true); return; }
|
if (mp.IsReconnecting) { DrawPrompt(mp, true); return; }
|
||||||
|
if (mp.PendingMods != null && mp.IsInSession) { DrawModsPrompt(mp); return; }
|
||||||
var window = new Rect(0, 0, vw, vh);
|
var window = new Rect(0, 0, vw, vh);
|
||||||
GUI.Box(window, GUIContent.none, windowBox);
|
GUI.Box(window, GUIContent.none, windowBox);
|
||||||
|
float glowWidth = Mathf.Min(vw * 0.9f, S(1300));
|
||||||
|
GUI.DrawTexture(new Rect(S(SidebarWidth) + (vw - S(SidebarWidth) - glowWidth) / 2f, -S(60), glowWidth, S(420)), texGlow);
|
||||||
|
|
||||||
DrawSidebar(new Rect(0, 0, S(SidebarWidth), vh - S(StatusHeight)));
|
DrawSidebar(new Rect(0, 0, S(SidebarWidth), vh - S(StatusHeight)));
|
||||||
|
|
||||||
@@ -131,14 +143,21 @@ namespace MSCO
|
|||||||
float width = Mathf.Min(S(MaxContentWidth), free - S(96f));
|
float width = Mathf.Min(S(MaxContentWidth), free - S(96f));
|
||||||
var content = new Rect(S(SidebarWidth) + Mathf.Round((free - width) / 2f), S(40f), width, vh - S(StatusHeight) - S(72f));
|
var content = new Rect(S(SidebarWidth) + Mathf.Round((free - width) / 2f), S(40f), width, vh - S(StatusHeight) - S(72f));
|
||||||
contentHeight = content.height;
|
contentHeight = content.height;
|
||||||
|
GUIStyle oldBar = GUI.skin.verticalScrollbar, oldThumb = GUI.skin.verticalScrollbarThumb;
|
||||||
|
GUI.skin.verticalScrollbar = scrollbarStyle;
|
||||||
|
GUI.skin.verticalScrollbarThumb = scrollThumbStyle;
|
||||||
GUILayout.BeginArea(content);
|
GUILayout.BeginArea(content);
|
||||||
|
if (!versionWarning && !pendingJoin && !mp.IsInSession) DrawAnnouncements();
|
||||||
if (versionWarning) DrawVersionWarning(mp);
|
if (versionWarning) DrawVersionWarning(mp);
|
||||||
else if (pendingJoin) DrawJoinConfirm(mp);
|
else if (pendingJoin) DrawJoinConfirm(mp);
|
||||||
else if (mp.IsInSession) DrawSession(mp);
|
else if (mp.IsInSession) DrawSession(mp);
|
||||||
else if (page == Page.P2P) DrawP2P(mp);
|
else if (page == Page.P2P) DrawP2P(mp);
|
||||||
else if (page == Page.Favorites) DrawFavorites(mp);
|
else if (page == Page.Favorites) DrawFavorites(mp);
|
||||||
|
else if (page == Page.Mods) DrawMods();
|
||||||
else DrawServerBrowser(mp);
|
else DrawServerBrowser(mp);
|
||||||
GUILayout.EndArea();
|
GUILayout.EndArea();
|
||||||
|
GUI.skin.verticalScrollbar = oldBar;
|
||||||
|
GUI.skin.verticalScrollbarThumb = oldThumb;
|
||||||
|
|
||||||
DrawStatusBar(mp, new Rect(0, vh - S(StatusHeight), vw, S(StatusHeight)));
|
DrawStatusBar(mp, new Rect(0, vh - S(StatusHeight), vw, S(StatusHeight)));
|
||||||
|
|
||||||
@@ -182,6 +201,9 @@ namespace MSCO
|
|||||||
textX = r.x + S(72);
|
textX = r.x + S(72);
|
||||||
}
|
}
|
||||||
GUI.Label(new Rect(textX, r.y + S(24), r.xMax - textX, S(40)), "MSCO", logoStyle);
|
GUI.Label(new Rect(textX, r.y + S(24), r.xMax - textX, S(40)), "MSCO", logoStyle);
|
||||||
|
// Beta-Abzeichen direkt hinter dem Schriftzug
|
||||||
|
float logoWidth = logoStyle.CalcSize(new GUIContent("MSCO")).x;
|
||||||
|
GUI.Box(new Rect(textX + logoWidth + S(6), r.y + S(36), S(40), S(18)), "BETA", betaPillStyle);
|
||||||
GUI.Label(new Rect(r.x + S(22), r.y + S(70), r.width - S(30), S(20)), "MY SUMMER CAR ONLINE", taglineStyle);
|
GUI.Label(new Rect(r.x + S(22), r.y + S(70), r.width - S(30), S(20)), "MY SUMMER CAR ONLINE", taglineStyle);
|
||||||
GUI.DrawTexture(new Rect(r.x + S(20), r.y + S(98), r.width - S(40), S(1)), texDivider);
|
GUI.DrawTexture(new Rect(r.x + S(20), r.y + S(98), r.width - S(40), S(1)), texDivider);
|
||||||
|
|
||||||
@@ -192,6 +214,12 @@ namespace MSCO
|
|||||||
y += S(44);
|
y += S(44);
|
||||||
int favCount = Favorites.All.Count;
|
int favCount = Favorites.All.Count;
|
||||||
if (NavButton(new Rect(r.x, y, r.width, S(42)), "Favorites" + (favCount > 0 ? " (" + favCount + ")" : ""), page == Page.Favorites)) page = Page.Favorites;
|
if (NavButton(new Rect(r.x, y, r.width, S(42)), "Favorites" + (favCount > 0 ? " (" + favCount + ")" : ""), page == Page.Favorites)) page = Page.Favorites;
|
||||||
|
y += S(44);
|
||||||
|
int modCount = ModLoader.All.Count;
|
||||||
|
if (NavButton(new Rect(r.x, y, r.width, S(42)), "Mods" + (modCount > 0 ? " (" + modCount + ")" : ""), page == Page.Mods)) page = Page.Mods;
|
||||||
|
y += S(54);
|
||||||
|
GUI.Label(new Rect(r.x + S(22), y, r.width - S(40), S(64)),
|
||||||
|
"MSCO is still in beta - bugs can happen. If something goes wrong, please tell us.", betaNoteStyle);
|
||||||
|
|
||||||
// Eigenes Hauptmenü: Spiel beenden (die Knöpfe des Spiel-Menüs sind ausgeblendet)
|
// Eigenes Hauptmenü: Spiel beenden (die Knöpfe des Spiel-Menüs sind ausgeblendet)
|
||||||
if (quitConfirm)
|
if (quitConfirm)
|
||||||
@@ -206,8 +234,10 @@ namespace MSCO
|
|||||||
|
|
||||||
bool NavButton(Rect r, string text, bool active)
|
bool NavButton(Rect r, string text, bool active)
|
||||||
{
|
{
|
||||||
if (active) GUI.Box(new Rect(r.x + S(4), r.y + S(8), S(4), r.height - S(16)), GUIContent.none, accentBox); // Akzentbalken links
|
var item = new Rect(r.x + S(12), r.y, r.width - S(24), r.height);
|
||||||
return GUI.Button(r, text, active ? navActiveStyle : navStyle);
|
bool clicked = GUI.Button(item, text, active ? navActiveStyle : navStyle);
|
||||||
|
if (active) GUI.Box(new Rect(item.x + S(8), item.y + S(11), S(3), item.height - S(22)), GUIContent.none, accentBox); // Akzentbalken
|
||||||
|
return clicked;
|
||||||
}
|
}
|
||||||
|
|
||||||
void DrawP2P(MultiplayerManager mp)
|
void DrawP2P(MultiplayerManager mp)
|
||||||
@@ -277,6 +307,10 @@ namespace MSCO
|
|||||||
showNonSteam = Chip(showNonSteam, "NON-STEAM");
|
showNonSteam = Chip(showNonSteam, "NON-STEAM");
|
||||||
GUILayout.Space(S(6));
|
GUILayout.Space(S(6));
|
||||||
hideFull = Chip(hideFull, "HIDE FULL");
|
hideFull = Chip(hideFull, "HIDE FULL");
|
||||||
|
GUILayout.Space(S(12));
|
||||||
|
showVanilla = Chip(showVanilla, "VANILLA");
|
||||||
|
GUILayout.Space(S(6));
|
||||||
|
showModded = Chip(showModded, "MODDED");
|
||||||
GUILayout.EndHorizontal();
|
GUILayout.EndHorizontal();
|
||||||
GUILayout.Space(S(6));
|
GUILayout.Space(S(6));
|
||||||
|
|
||||||
@@ -285,14 +319,16 @@ namespace MSCO
|
|||||||
shownServers = all.FindAll(s =>
|
shownServers = all.FindAll(s =>
|
||||||
(s.RequireSteam ? showSteam : showNonSteam) &&
|
(s.RequireSteam ? showSteam : showNonSteam) &&
|
||||||
(!hideFull || s.Players < s.MaxPlayers) &&
|
(!hideFull || s.Players < s.MaxPlayers) &&
|
||||||
|
(s.Modded ? showModded : showVanilla) &&
|
||||||
(search.Length == 0 || (s.Name ?? "").ToLower().Contains(search) || (s.Owner ?? "").ToLower().Contains(search) ||
|
(search.Length == 0 || (s.Name ?? "").ToLower().Contains(search) || (s.Owner ?? "").ToLower().Contains(search) ||
|
||||||
(s.Tags != null && s.Tags.Exists(t => (t ?? "").Contains(search)))));
|
(s.Tags != null && s.Tags.Exists(t => (t ?? "").Contains(search)))));
|
||||||
List<ServerListEntry> servers = shownServers;
|
List<ServerListEntry> servers = shownServers;
|
||||||
string shownError = Event.current.type == EventType.Layout ? (layoutError = browser.Error) : layoutError;
|
string shownError = Event.current.type == EventType.Layout ? (layoutError = browser.Error) : layoutError;
|
||||||
|
|
||||||
// Tabellenkopf
|
// Tabellenkopf
|
||||||
|
GUILayout.Space(S(6));
|
||||||
Rect header = GUILayoutUtility.GetRect(S(10), S(26), GUILayout.ExpandWidth(true));
|
Rect header = GUILayoutUtility.GetRect(S(10), S(26), GUILayout.ExpandWidth(true));
|
||||||
GUI.Box(header, GUIContent.none, rowBox);
|
GUI.DrawTexture(new Rect(header.x, header.yMax - S(1), header.width, S(1)), texDivider);
|
||||||
GUI.Label(new Rect(header.x + S(12), header.y, header.width * 0.48f, header.height), "SERVER NAME", headerCellStyle);
|
GUI.Label(new Rect(header.x + S(12), header.y, header.width * 0.48f, header.height), "SERVER NAME", headerCellStyle);
|
||||||
GUI.Label(new Rect(header.x + header.width * 0.5f, header.y, S(70), header.height), "PLAYERS", headerCellStyle);
|
GUI.Label(new Rect(header.x + header.width * 0.5f, header.y, S(70), header.height), "PLAYERS", headerCellStyle);
|
||||||
GUI.Label(new Rect(header.x + header.width * 0.62f, header.y, S(70), header.height), "TYPE", headerCellStyle);
|
GUI.Label(new Rect(header.x + header.width * 0.62f, header.y, S(70), header.height), "TYPE", headerCellStyle);
|
||||||
@@ -323,11 +359,13 @@ namespace MSCO
|
|||||||
Favorites.Favorite fav = Favorites.FindByAddress(s.Address, s.Port);
|
Favorites.Favorite fav = Favorites.FindByAddress(s.Address, s.Port);
|
||||||
if (s.Featured) nameX += Pill(nameX, row, "FEATURED", pillFeaturedStyle);
|
if (s.Featured) nameX += Pill(nameX, row, "FEATURED", pillFeaturedStyle);
|
||||||
if (fav != null) nameX += Pill(nameX, row, fav.Nickname, pillMutedStyle);
|
if (fav != null) nameX += Pill(nameX, row, fav.Nickname, pillMutedStyle);
|
||||||
|
if (s.Modded) nameX += Pill(nameX, row, s.Mods.Count > 0 ? "MODS " + s.Mods.Count : "MODDED", pillStyle);
|
||||||
if (s.IsLocal)
|
if (s.IsLocal)
|
||||||
{
|
{
|
||||||
// Server auf diesem PC (localhost)
|
// Server auf diesem PC (localhost)
|
||||||
nameX += Pill(nameX, row, "LOCAL", pillStyle);
|
nameX += Pill(nameX, row, "LOCAL", pillStyle);
|
||||||
}
|
}
|
||||||
|
if (s.Outdated) nameX += Pill(nameX, row, "OUTDATED", pillDangerStyle); // fehlende Sicherheitsupdates
|
||||||
if (!string.IsNullOrEmpty(s.Version) && !MSCO.Compatible(s.Version))
|
if (!string.IsNullOrEmpty(s.Version) && !MSCO.Compatible(s.Version))
|
||||||
{
|
{
|
||||||
nameX += Pill(nameX, row, "v" + s.Version, pillStyle); // abweichende Version
|
nameX += Pill(nameX, row, "v" + s.Version, pillStyle); // abweichende Version
|
||||||
@@ -378,7 +416,52 @@ namespace MSCO
|
|||||||
|
|
||||||
static bool CanJoin(MultiplayerManager mp, ServerListEntry s)
|
static bool CanJoin(MultiplayerManager mp, ServerListEntry s)
|
||||||
{
|
{
|
||||||
return s.Players < s.MaxPlayers && (!s.RequireSteam || mp.SteamAvailable);
|
return !s.Outdated && s.Players < s.MaxPlayers && (!s.RequireSteam || mp.SteamAvailable);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Oben im MSCO-Menü: Wartungshinweis und Ankündigungen der MSCO-Admins (Keymaster → Wartung & News).
|
||||||
|
/// Ankündigungen lassen sich mit × für diese Sitzung ausblenden.
|
||||||
|
/// </summary>
|
||||||
|
void DrawAnnouncements()
|
||||||
|
{
|
||||||
|
if (shownNews == null || Event.current.type == EventType.Layout)
|
||||||
|
{
|
||||||
|
shownNews = SiteStatus.Announcements.FindAll(a => !dismissedNews.Contains(a.Id));
|
||||||
|
if (shownNews.Count > 3) shownNews.RemoveRange(3, shownNews.Count - 3);
|
||||||
|
shownMaintenance = SiteStatus.Maintenance;
|
||||||
|
shownMaintenanceText = SiteStatus.MaintenanceMessage;
|
||||||
|
}
|
||||||
|
if (shownMaintenance)
|
||||||
|
NewsBox("MAINTENANCE", pillDangerStyle, texNewsCritical, "MSCO is in maintenance right now",
|
||||||
|
string.IsNullOrEmpty(shownMaintenanceText) ? "Some things may not work for a while." : shownMaintenanceText, null);
|
||||||
|
foreach (Announcement a in shownNews)
|
||||||
|
NewsBox(a.Level == "critical" ? "IMPORTANT" : a.Level == "warning" ? "NOTICE" : "INFO",
|
||||||
|
a.Level == "critical" ? pillDangerStyle : a.Level == "warning" ? pillFeaturedStyle : pillMutedStyle,
|
||||||
|
a.Level == "critical" ? texNewsCritical : a.Level == "warning" ? texNewsWarning : texNewsInfo, a.Title, a.Text, a.Id);
|
||||||
|
if (shownMaintenance || shownNews.Count > 0) GUILayout.Space(S(10));
|
||||||
|
}
|
||||||
|
|
||||||
|
void NewsBox(string label, GUIStyle pill, Texture2D stripe, string title, string text, string id)
|
||||||
|
{
|
||||||
|
GUILayout.BeginVertical(newsBoxStyle);
|
||||||
|
GUILayout.BeginHorizontal();
|
||||||
|
// Abzeichen und Titel gleich hoch und mittig - sonst sitzt der Titel tiefer als das Abzeichen
|
||||||
|
GUILayout.Label(label, pill, GUILayout.Height(S(22)), GUILayout.ExpandWidth(false));
|
||||||
|
GUILayout.Space(S(10));
|
||||||
|
GUILayout.Label(title, newsTitleStyle, GUILayout.MinHeight(S(22)), GUILayout.ExpandWidth(true));
|
||||||
|
if (id != null && GUILayout.Button(new GUIContent("×", "Hide"), newsCloseStyle, GUILayout.Width(S(24)), GUILayout.Height(S(22))))
|
||||||
|
dismissedNews.Add(id);
|
||||||
|
GUILayout.EndHorizontal();
|
||||||
|
if (!string.IsNullOrEmpty(text)) GUILayout.Label(text, newsTextStyle);
|
||||||
|
GUILayout.EndVertical();
|
||||||
|
// Farbiger Balken links (Art der Ankündigung)
|
||||||
|
if (Event.current.type == EventType.Repaint)
|
||||||
|
{
|
||||||
|
Rect box = GUILayoutUtility.GetLastRect();
|
||||||
|
GUI.DrawTexture(new Rect(box.x, box.y + S(10), S(3), box.height - S(20)), stripe);
|
||||||
|
}
|
||||||
|
GUILayout.Space(S(8));
|
||||||
}
|
}
|
||||||
|
|
||||||
void DrawNameField(MultiplayerManager mp, float width)
|
void DrawNameField(MultiplayerManager mp, float width)
|
||||||
@@ -576,13 +659,14 @@ namespace MSCO
|
|||||||
GUILayout.BeginVertical();
|
GUILayout.BeginVertical();
|
||||||
GUILayout.Label(s.Name, titleStyle);
|
GUILayout.Label(s.Name, titleStyle);
|
||||||
GUILayout.BeginHorizontal();
|
GUILayout.BeginHorizontal();
|
||||||
Rect badgeRow = GUILayoutUtility.GetRect(S(10), S(20), GUILayout.ExpandWidth(true));
|
Rect badgeRow = GUILayoutUtility.GetRect(S(10), S(22), GUILayout.ExpandWidth(true));
|
||||||
float bx = badgeRow.x;
|
float bx = badgeRow.x + titleStyle.padding.left;
|
||||||
if (s.Featured) bx += Pill(bx, badgeRow, "FEATURED", pillFeaturedStyle);
|
if (s.Featured) bx += Pill(bx, badgeRow, "FEATURED", pillFeaturedStyle);
|
||||||
bool official = s.Tags != null && s.Tags.Contains("official"); // vergibt nur ein MSCO-Admin
|
bool official = s.Tags != null && s.Tags.Contains("official"); // vergibt nur ein MSCO-Admin
|
||||||
if (official) bx += Pill(bx, badgeRow, "OFFICIAL", pillStyle);
|
if (official) bx += Pill(bx, badgeRow, "OFFICIAL", pillStyle);
|
||||||
bx += Pill(bx, badgeRow, s.RequireSteam ? "STEAM" : "NON-STEAM", s.RequireSteam ? pillStyle : pillMutedStyle);
|
bx += Pill(bx, badgeRow, s.RequireSteam ? "STEAM" : "NON-STEAM", s.RequireSteam ? pillStyle : pillMutedStyle);
|
||||||
if (s.IsLocal) bx += Pill(bx, badgeRow, "LOCAL", pillStyle);
|
if (s.IsLocal) bx += Pill(bx, badgeRow, "LOCAL", pillStyle);
|
||||||
|
if (s.Outdated) bx += Pill(bx, badgeRow, "OUTDATED", pillDangerStyle);
|
||||||
if (!MSCO.Compatible(s.Version)) bx += Pill(bx, badgeRow, "v" + s.Version, pillStyle);
|
if (!MSCO.Compatible(s.Version)) bx += Pill(bx, badgeRow, "v" + s.Version, pillStyle);
|
||||||
if (s.Tags != null)
|
if (s.Tags != null)
|
||||||
foreach (string tag in s.Tags)
|
foreach (string tag in s.Tags)
|
||||||
@@ -595,10 +679,13 @@ namespace MSCO
|
|||||||
Heart(new Rect(heartSlot.x + S(11), heartSlot.y + S(11), S(22), S(22)), s);
|
Heart(new Rect(heartSlot.x + S(11), heartSlot.y + S(11), S(22), S(22)), s);
|
||||||
GUILayout.Space(S(8));
|
GUILayout.Space(S(8));
|
||||||
GUI.enabled = CanJoin(mp, s);
|
GUI.enabled = CanJoin(mp, s);
|
||||||
if (GUILayout.Button(s.Players >= s.MaxPlayers ? "SERVER FULL" : "JOIN SERVER", primaryStyle, GUILayout.Height(S(44)), GUILayout.Width(S(180))))
|
if (GUILayout.Button(s.Outdated ? "UPDATE REQUIRED" : s.Players >= s.MaxPlayers ? "SERVER FULL" : "JOIN SERVER", primaryStyle, GUILayout.Height(S(44)), GUILayout.Width(S(180))))
|
||||||
mp.JoinServer(s.Address, s.Port, s.Name, s.MaxPlayers);
|
mp.JoinServer(s.Address, s.Port, s.Name, s.MaxPlayers);
|
||||||
GUI.enabled = true;
|
GUI.enabled = true;
|
||||||
GUILayout.EndHorizontal();
|
GUILayout.EndHorizontal();
|
||||||
|
if (s.Outdated)
|
||||||
|
GUILayout.Label("This server is missing MSCO security updates" + (string.IsNullOrEmpty(s.ServerVersion) ? "" : " (server " + s.ServerVersion + ")") +
|
||||||
|
" - you can join again once its owner has updated it.", textStyle);
|
||||||
if (!string.IsNullOrEmpty(s.Description)) GUILayout.Label(s.Description, textStyle);
|
if (!string.IsNullOrEmpty(s.Description)) GUILayout.Label(s.Description, textStyle);
|
||||||
GUILayout.Space(S(10));
|
GUILayout.Space(S(10));
|
||||||
|
|
||||||
@@ -628,38 +715,84 @@ namespace MSCO
|
|||||||
GUILayout.Space(S(10));
|
GUILayout.Space(S(10));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Infos links, Spielerliste rechts
|
// Zwei Karten gleicher Höhe: Infos links, Spielerliste rechts
|
||||||
bool versionOk = MSCO.Compatible(s.Version);
|
bool versionOk = MSCO.Compatible(s.Version);
|
||||||
GUILayout.BeginHorizontal();
|
float cardHeight = Mathf.Clamp(contentHeight - S(250), S(240), S(380));
|
||||||
GUILayout.BeginVertical(GUILayout.Width(S(250)));
|
Rect area = GUILayoutUtility.GetRect(S(10), cardHeight, GUILayout.ExpandWidth(true));
|
||||||
InfoLine("HOSTED BY", string.IsNullOrEmpty(s.Owner) ? "unknown" : s.Owner, rowNameStyle);
|
float gap = S(16), leftWidth = Mathf.Round(area.width * 0.44f);
|
||||||
InfoLine("VERSION", "v" + s.Version + (versionOk ? "" : " (you: v" + MSCO.ModVersion + ")"), versionOk ? rowNameStyle : badgeStyle);
|
var info = new Rect(area.x, area.y, leftWidth, area.height);
|
||||||
InfoLine("STEAM", s.RequireSteam ? "Required" : "Not required", rowNameStyle);
|
var list = new Rect(area.x + leftWidth + gap, area.y, area.width - leftWidth - gap, area.height);
|
||||||
InfoLine("ONLINE FOR", s.UptimeSeconds >= 0 ? FormatUptime(s.UptimeSeconds) : "-", rowNameStyle);
|
GUI.Box(info, GUIContent.none, rowBox);
|
||||||
InfoLine("PING", s.Ping < 0 ? "-" : s.Ping + " ms", PingStyle(s.Ping));
|
GUI.Box(list, GUIContent.none, rowBox);
|
||||||
InfoLine("ADDRESS", s.Address + ":" + s.Port, rowNameStyle);
|
|
||||||
GUILayout.EndVertical();
|
|
||||||
GUILayout.Space(S(16));
|
|
||||||
|
|
||||||
GUILayout.BeginVertical(rowBox, GUILayout.ExpandWidth(true), GUILayout.MinHeight(S(150)));
|
EnsureProfileStyles();
|
||||||
GUILayout.Space(S(6));
|
float x = info.x + S(20), w = info.width - S(40), y = info.y + S(16);
|
||||||
GUILayout.Label("PLAYERS " + s.Players + " / " + s.MaxPlayers, captionStyle);
|
GUI.Label(new Rect(x, y, w, S(18)), "SERVER INFO", captionStyle);
|
||||||
profileScroll = GUILayout.BeginScrollView(profileScroll, GUILayout.Height(ListHeight(S(330f), S(130f))));
|
y += S(28);
|
||||||
|
y = ProfileRow(x, y, w, "HOSTED BY", string.IsNullOrEmpty(s.Owner) ? "unknown" : s.Owner, profileValueStyle);
|
||||||
|
y = ProfileRow(x, y, w, "VERSION", "v" + s.Version + (versionOk ? "" : " (you: v" + MSCO.ModVersion + ")"), versionOk ? profileValueStyle : badgeStyle);
|
||||||
|
y = ProfileRow(x, y, w, "STEAM", s.RequireSteam ? "Required" : "Not required", profileValueStyle);
|
||||||
|
y = ProfileRow(x, y, w, "ONLINE FOR", s.UptimeSeconds >= 0 ? FormatUptime(s.UptimeSeconds) : "-", profileValueStyle);
|
||||||
|
y = ProfileRow(x, y, w, "PING", s.Ping < 0 ? "-" : s.Ping + " ms", PingStyle(s.Ping));
|
||||||
|
y = ProfileRow(x, y, w, "ADDRESS", s.Address + ":" + s.Port, profileValueStyle);
|
||||||
|
if (s.Mods.Count > 0) ProfileRow(x, y, w, "MODS", string.Join(", ", s.Mods.ToArray()), profileWrapStyle);
|
||||||
|
|
||||||
|
// Spieler
|
||||||
|
float lx = list.x + S(20), lw = list.width - S(40), ly = list.y + S(16);
|
||||||
|
GUI.Label(new Rect(lx, ly, lw, S(18)), "PLAYERS " + s.Players + " / " + s.MaxPlayers, captionStyle);
|
||||||
|
ly += S(30);
|
||||||
|
var listArea = new Rect(lx, ly, lw, list.yMax - ly - S(14));
|
||||||
if (s.PlayerNames == null || s.PlayerNames.Count == 0)
|
if (s.PlayerNames == null || s.PlayerNames.Count == 0)
|
||||||
GUILayout.Label("Nobody online right now.", textStyle);
|
{
|
||||||
|
GUI.Label(listArea, "Nobody online right now.", profileEmptyStyle);
|
||||||
|
}
|
||||||
else
|
else
|
||||||
foreach (string name in s.PlayerNames)
|
{
|
||||||
|
float rowH = S(32);
|
||||||
|
var inner = new Rect(0, 0, lw - S(14), rowH * s.PlayerNames.Count);
|
||||||
|
profileScroll = GUI.BeginScrollView(listArea, profileScroll, inner);
|
||||||
|
for (int i = 0; i < s.PlayerNames.Count; i++)
|
||||||
{
|
{
|
||||||
Rect line = GUILayoutUtility.GetRect(S(10), S(24), GUILayout.ExpandWidth(true));
|
var line = new Rect(0, i * rowH, inner.width, rowH);
|
||||||
GUI.DrawTexture(new Rect(line.x + S(2), line.y + S(8), S(8), S(8)), texOnline);
|
if (i > 0) GUI.DrawTexture(new Rect(line.x, line.y, line.width, S(1)), texDivider);
|
||||||
GUI.Label(new Rect(line.x + S(18), line.y, line.width - S(18), line.height), name, rowNameStyle);
|
GUI.DrawTexture(new Rect(line.x + S(2), line.y + (rowH - S(8)) / 2f, S(8), S(8)), texOnline);
|
||||||
|
GUI.Label(new Rect(line.x + S(20), line.y, line.width - S(20), rowH), s.PlayerNames[i], profileValueStyle);
|
||||||
}
|
}
|
||||||
GUILayout.EndScrollView();
|
GUI.EndScrollView();
|
||||||
GUILayout.EndVertical();
|
}
|
||||||
GUILayout.EndHorizontal();
|
|
||||||
GUILayout.FlexibleSpace();
|
GUILayout.FlexibleSpace();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
GUIStyle profileValueStyle, profileWrapStyle, profileCaptionStyle, profileEmptyStyle;
|
||||||
|
|
||||||
|
void EnsureProfileStyles()
|
||||||
|
{
|
||||||
|
if (profileValueStyle != null && profileValueStyle.fontSize == P(15)) return;
|
||||||
|
profileValueStyle = new GUIStyle(rowNameStyle) { fontSize = P(15), wordWrap = false, clipping = TextClipping.Clip, alignment = TextAnchor.MiddleLeft };
|
||||||
|
profileWrapStyle = new GUIStyle(profileValueStyle) { wordWrap = true, fontSize = P(13), alignment = TextAnchor.UpperLeft };
|
||||||
|
profileWrapStyle.normal.textColor = Muted;
|
||||||
|
profileCaptionStyle = new GUIStyle(captionStyle) { alignment = TextAnchor.MiddleLeft };
|
||||||
|
profileCaptionStyle.margin = new RectOffset();
|
||||||
|
profileEmptyStyle = new GUIStyle(textStyle) { alignment = TextAnchor.MiddleCenter };
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Info-Zeile der Server-Karte: Bezeichnung links, Wert daneben, feine Linie darunter. Gibt das nächste y zurück.</summary>
|
||||||
|
float ProfileRow(float x, float y, float w, string caption, string value, GUIStyle valueStyle)
|
||||||
|
{
|
||||||
|
const float captionWidth = 120f;
|
||||||
|
float valueWidth = w - S(captionWidth);
|
||||||
|
float h = S(36);
|
||||||
|
if (valueStyle.wordWrap) h = Mathf.Max(h, valueStyle.CalcHeight(new GUIContent(value), valueWidth) + S(16));
|
||||||
|
GUI.Label(new Rect(x, y, S(captionWidth), S(36)), caption, profileCaptionStyle);
|
||||||
|
var valueRect = valueStyle.wordWrap ? new Rect(x + S(captionWidth), y + S(9), valueWidth, h - S(9)) : new Rect(x + S(captionWidth), y, valueWidth, S(36));
|
||||||
|
GUIStyle style = valueStyle;
|
||||||
|
if (!valueStyle.wordWrap && valueStyle.alignment != TextAnchor.MiddleLeft)
|
||||||
|
style = new GUIStyle(valueStyle) { alignment = TextAnchor.MiddleLeft };
|
||||||
|
GUI.Label(valueRect, value, style);
|
||||||
|
GUI.DrawTexture(new Rect(x, y + h, w, S(1)), texDivider);
|
||||||
|
return y + h + S(1);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Eine Info-Zeile: Bezeichnung links (grau), Wert rechts.</summary>
|
/// <summary>Eine Info-Zeile: Bezeichnung links (grau), Wert rechts.</summary>
|
||||||
void InfoLine(string caption, string value, GUIStyle valueStyle)
|
void InfoLine(string caption, string value, GUIStyle valueStyle)
|
||||||
{
|
{
|
||||||
@@ -808,6 +941,85 @@ namespace MSCO
|
|||||||
if (GUI.Button(new Rect(r.xMax - S(32) - S(170), r.yMax - S(28) - S(42), S(170), S(42)), "OK", primaryStyle)) mp.ClearDisconnectNotice();
|
if (GUI.Button(new Rect(r.xMax - S(32) - S(170), r.yMax - S(28) - S(42), S(170), S(42)), "OK", primaryStyle)) mp.ClearDisconnectNotice();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------- Mods
|
||||||
|
|
||||||
|
Vector2 modsScroll;
|
||||||
|
|
||||||
|
/// <summary>Server verlangt Mods, die hier fehlen: zeigen, was geladen würde, und fragen.</summary>
|
||||||
|
void DrawModsPrompt(MultiplayerManager mp)
|
||||||
|
{
|
||||||
|
if (texDim == null) texDim = Solid(new Color(0f, 0f, 0f, 0.6f));
|
||||||
|
GUI.DrawTexture(new Rect(0, 0, vw, vh), texDim);
|
||||||
|
List<MultiplayerManager.ServerModInfo> list = mp.PendingMods;
|
||||||
|
float height = S(250) + S(46) * Mathf.Min(list.Count, 6);
|
||||||
|
var r = new Rect(Mathf.Round((vw - S(620)) / 2f), Mathf.Round((vh - height) / 2f), S(620), height);
|
||||||
|
UiTheme.DrawShadow(r);
|
||||||
|
GUI.Box(r, GUIContent.none, windowBox);
|
||||||
|
GUI.DrawTexture(new Rect(r.x, r.y, r.width, S(4)), texAccent);
|
||||||
|
|
||||||
|
float x = r.x + S(32), w = r.width - S(64), y = r.y + S(26);
|
||||||
|
GUI.Label(new Rect(x, y, w, S(34)), "THIS SERVER USES MODS", titleStyle);
|
||||||
|
y += S(44);
|
||||||
|
GUI.Label(new Rect(x, y, w, S(44)), "To play here you need these mods. Mods are programs - they run on your PC. " +
|
||||||
|
"Only install them from servers you trust.", textStyle);
|
||||||
|
y += S(52);
|
||||||
|
for (int i = 0; i < list.Count && i < 6; i++)
|
||||||
|
{
|
||||||
|
MultiplayerManager.ServerModInfo m = list[i];
|
||||||
|
var row = new Rect(x, y, w, S(40));
|
||||||
|
GUI.Box(row, GUIContent.none, rowBox);
|
||||||
|
GUI.Label(new Rect(row.x + S(14), row.y, w * 0.55f, row.height), m.Name + (string.IsNullOrEmpty(m.Version) ? "" : " " + m.Version), rowNameStyle);
|
||||||
|
GUI.Label(new Rect(row.x + w * 0.55f, row.y, w * 0.45f - S(14), row.height),
|
||||||
|
(m.Size / 1024) + " KB · SHA-256 " + (m.Sha256.Length > 12 ? m.Sha256.Substring(0, 12) + "…" : m.Sha256), headerCellStyle);
|
||||||
|
y += S(46);
|
||||||
|
}
|
||||||
|
if (list.Count > 6) GUI.Label(new Rect(x, y - S(6), w, S(20)), "+ " + (list.Count - 6) + " more", textStyle);
|
||||||
|
|
||||||
|
float by = r.yMax - S(28) - S(42);
|
||||||
|
if (GUI.Button(new Rect(r.xMax - S(32) - S(220), by, S(220), S(42)), "INSTALL & JOIN", primaryStyle)) mp.AcceptServerMods();
|
||||||
|
if (GUI.Button(new Rect(r.xMax - S(32) - S(220) - S(12) - S(140), by, S(140), S(42)), "CANCEL", secondaryStyle)) mp.DeclineServerMods();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Seite "Mods": geladene MSCO-Mods, Ordner öffnen.</summary>
|
||||||
|
void DrawMods()
|
||||||
|
{
|
||||||
|
GUILayout.BeginHorizontal();
|
||||||
|
GUILayout.Label("Mods", titleStyle, GUILayout.ExpandWidth(true));
|
||||||
|
if (GUILayout.Button("OPEN FOLDER", tabStyle, GUILayout.Height(S(28)), GUILayout.Width(S(130))))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
System.IO.Directory.CreateDirectory(ModLoader.Folder);
|
||||||
|
System.Diagnostics.Process.Start("explorer.exe", "\"" + ModLoader.Folder + "\"");
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
}
|
||||||
|
GUILayout.EndHorizontal();
|
||||||
|
GUILayout.Label("Put MSCO mods (.dll) into MSCO\\Mods in the game folder and restart the game - or manage them in the MSCO Launcher. " +
|
||||||
|
"Mods from a server are downloaded when you join it (you are asked first) and only run on that server.", textStyle);
|
||||||
|
GUILayout.Space(S(12));
|
||||||
|
|
||||||
|
modsScroll = GUILayout.BeginScrollView(modsScroll, GUILayout.Height(ListHeight(S(150f), S(140f))));
|
||||||
|
if (ModLoader.All.Count == 0) GUILayout.Label("No mods installed.", textStyle);
|
||||||
|
foreach (ModLoader.LoadedMod m in ModLoader.All)
|
||||||
|
{
|
||||||
|
Rect row = GUILayoutUtility.GetRect(S(10), S(52), GUILayout.ExpandWidth(true));
|
||||||
|
var box = new Rect(row.x, row.y + S(3), row.width, row.height - S(6));
|
||||||
|
GUI.Box(box, GUIContent.none, rowBox);
|
||||||
|
GUI.DrawTexture(new Rect(box.x + S(14), box.y + (box.height - S(8)) / 2f, S(8), S(8)),
|
||||||
|
m.Error != null ? texPending : m.Active ? texOnline : texMuted);
|
||||||
|
string name = "?", version = "", author = "";
|
||||||
|
try { name = m.Mod.Name; version = m.Mod.Version; author = m.Mod.Author; } catch { }
|
||||||
|
float nx = box.x + S(32);
|
||||||
|
if (m.FromServer) nx += Pill(nx, new Rect(box.x, box.y, box.width, box.height), "SERVER", pillMutedStyle);
|
||||||
|
GUI.Label(new Rect(nx, box.y, box.width * 0.5f, box.height), name + " " + version, rowNameStyle);
|
||||||
|
string state = m.Error != null ? "turned off: " + m.Error : m.Active ? "active" : m.FromServer ? "only on its server" : "off";
|
||||||
|
GUI.Label(new Rect(box.x + box.width * 0.55f, box.y, box.width * 0.45f - S(14), box.height),
|
||||||
|
(string.IsNullOrEmpty(author) ? "" : "by " + author + " · ") + state, headerCellStyle);
|
||||||
|
}
|
||||||
|
GUILayout.EndScrollView();
|
||||||
|
}
|
||||||
|
|
||||||
bool newGameConfirm;
|
bool newGameConfirm;
|
||||||
|
|
||||||
/// <summary>Host: eigenen Spielstand weiterspielen oder ein neues Spiel anlegen (mit Sicherheitsabfrage).</summary>
|
/// <summary>Host: eigenen Spielstand weiterspielen oder ein neues Spiel anlegen (mit Sicherheitsabfrage).</summary>
|
||||||
@@ -933,8 +1145,16 @@ namespace MSCO
|
|||||||
// Fenster und Seitenleiste mit feinem Rand (Seitenleiste: ihr Rand trennt sie vom Inhalt)
|
// Fenster und Seitenleiste mit feinem Rand (Seitenleiste: ihr Rand trennt sie vom Inhalt)
|
||||||
windowBox = UiTheme.Card(Background, P(10));
|
windowBox = UiTheme.Card(Background, P(10));
|
||||||
sidebarBox = UiTheme.Card(Sidebar, P(10));
|
sidebarBox = UiTheme.Card(Sidebar, P(10));
|
||||||
rowBox = UiTheme.Panel9(Field, P(UiTheme.Radius));
|
rowBox = UiTheme.Card(Field, P(10));
|
||||||
rowHoverBox = UiTheme.Card(FieldFocus, P(UiTheme.Radius));
|
rowHoverBox = new GUIStyle(rowBox);
|
||||||
|
rowHoverBox.normal.background = UiTheme.RoundedEx(FieldFocus, FieldFocus, new Color(Accent.r, Accent.g, Accent.b, 0.45f), true, P(10), P(10) * 2 + 4);
|
||||||
|
texGlow = Glow(new Color(Accent.r, Accent.g, Accent.b, 0.16f));
|
||||||
|
scrollbarStyle = new GUIStyle();
|
||||||
|
scrollbarStyle.fixedWidth = S(8);
|
||||||
|
scrollbarStyle.margin = new RectOffset(P(6), 0, 0, 0);
|
||||||
|
scrollThumbStyle = UiTheme.Panel9(new Color(1f, 1f, 1f, 0.14f), P(3));
|
||||||
|
scrollThumbStyle.fixedWidth = S(6);
|
||||||
|
scrollThumbStyle.hover.background = UiTheme.Rounded(new Color(1f, 1f, 1f, 0.24f), P(3));
|
||||||
texDivider = Solid(new Color(1f, 1f, 1f, 0.07f));
|
texDivider = Solid(new Color(1f, 1f, 1f, 0.07f));
|
||||||
texGold = Solid(Gold);
|
texGold = Solid(Gold);
|
||||||
texBarBack = Solid(new Color(1f, 1f, 1f, 0.1f));
|
texBarBack = Solid(new Color(1f, 1f, 1f, 0.1f));
|
||||||
@@ -955,14 +1175,26 @@ namespace MSCO
|
|||||||
pillFeaturedStyle = new GUIStyle(pillStyle);
|
pillFeaturedStyle = new GUIStyle(pillStyle);
|
||||||
pillFeaturedStyle.normal.background = UiTheme.Rounded(Gold, P(4));
|
pillFeaturedStyle.normal.background = UiTheme.Rounded(Gold, P(4));
|
||||||
pillFeaturedStyle.normal.textColor = Hex(0x1a, 0x12, 0x06);
|
pillFeaturedStyle.normal.textColor = Hex(0x1a, 0x12, 0x06);
|
||||||
featuredRowBox = UiTheme.Panel9(Color.Lerp(Field, Gold, 0.16f), P(UiTheme.Radius));
|
pillDangerStyle = new GUIStyle(pillStyle);
|
||||||
|
pillDangerStyle.normal.background = UiTheme.Rounded(new Color(UiTheme.Danger.r, UiTheme.Danger.g, UiTheme.Danger.b, 0.25f), P(4));
|
||||||
|
pillDangerStyle.normal.textColor = UiTheme.DangerHover;
|
||||||
|
featuredRowBox = UiTheme.Card(Color.Lerp(Field, Gold, 0.14f), P(10));
|
||||||
|
|
||||||
chipOnStyle = Button(new Color(Accent.r, Accent.g, Accent.b, 0.22f), new Color(Accent.r, Accent.g, Accent.b, 0.32f), AccentHover, P(11), FontStyle.Bold);
|
chipOnStyle = Button(new Color(Accent.r, Accent.g, Accent.b, 0.2f), new Color(Accent.r, Accent.g, Accent.b, 0.3f), AccentHover, P(11), FontStyle.Bold, P(11), new Color(Accent.r, Accent.g, Accent.b, 0.55f));
|
||||||
chipOffStyle = Button(Field, FieldFocus, Muted, P(11), FontStyle.Bold);
|
chipOffStyle = Button(Field, FieldFocus, Muted, P(11), FontStyle.Bold, P(11), UiTheme.Border);
|
||||||
|
chipOffStyle.hover.textColor = Color.white;
|
||||||
chipOnStyle.padding = chipOffStyle.padding = new RectOffset(P(12), P(12), P(0), P(0));
|
chipOnStyle.padding = chipOffStyle.padding = new RectOffset(P(12), P(12), P(0), P(0));
|
||||||
|
|
||||||
logoStyle = Label(P(28), Color.white, FontStyle.Bold);
|
logoStyle = Label(P(28), Color.white, FontStyle.Bold);
|
||||||
taglineStyle = Label(P(10), Muted, FontStyle.Bold);
|
taglineStyle = Label(P(10), Muted, FontStyle.Bold);
|
||||||
|
betaPillStyle = UiTheme.Panel9(UiTheme.Accent, 4);
|
||||||
|
if (UiTheme.DynamicFont != null) betaPillStyle.font = UiTheme.DynamicFont;
|
||||||
|
betaPillStyle.normal.textColor = new Color(0.1f, 0.07f, 0.02f);
|
||||||
|
betaPillStyle.fontSize = P(10);
|
||||||
|
betaPillStyle.fontStyle = FontStyle.Bold;
|
||||||
|
betaPillStyle.alignment = TextAnchor.MiddleCenter;
|
||||||
|
betaNoteStyle = Label(P(11), Muted, FontStyle.Normal);
|
||||||
|
betaNoteStyle.wordWrap = true;
|
||||||
titleStyle = Label(P(24), Color.white, FontStyle.Bold);
|
titleStyle = Label(P(24), Color.white, FontStyle.Bold);
|
||||||
textStyle = Label(P(13), Muted, FontStyle.Normal);
|
textStyle = Label(P(13), Muted, FontStyle.Normal);
|
||||||
textStyle.wordWrap = true;
|
textStyle.wordWrap = true;
|
||||||
@@ -992,50 +1224,98 @@ namespace MSCO
|
|||||||
toggleStyle.fontSize = P(11);
|
toggleStyle.fontSize = P(11);
|
||||||
toggleStyle.fontStyle = FontStyle.Bold;
|
toggleStyle.fontStyle = FontStyle.Bold;
|
||||||
|
|
||||||
navStyle = Button(Sidebar, Secondary, Muted, P(14), FontStyle.Normal);
|
navStyle = Button(Sidebar, new Color(1f, 1f, 1f, 0.05f), Muted, P(14), FontStyle.Normal, P(10));
|
||||||
navStyle.hover.textColor = Color.white; // Hover: Text hell (vorher blieb er grau)
|
navStyle.hover.textColor = Color.white; // Hover: Text hell (vorher blieb er grau)
|
||||||
navStyle.alignment = TextAnchor.MiddleLeft;
|
navStyle.alignment = TextAnchor.MiddleLeft;
|
||||||
navStyle.padding = new RectOffset(P(26), P(10), P(0), P(0));
|
navStyle.padding = new RectOffset(P(22), P(10), P(0), P(0));
|
||||||
navActiveStyle = Button(Secondary, Secondary, Color.white, P(14), FontStyle.Bold);
|
navActiveStyle = Button(Color.Lerp(Sidebar, Accent, 0.12f), Color.Lerp(Sidebar, Accent, 0.16f), Color.white, P(14), FontStyle.Bold, P(10), new Color(Accent.r, Accent.g, Accent.b, 0.25f));
|
||||||
navActiveStyle.alignment = TextAnchor.MiddleLeft;
|
navActiveStyle.alignment = TextAnchor.MiddleLeft;
|
||||||
navActiveStyle.padding = new RectOffset(P(26), P(10), P(0), P(0));
|
navActiveStyle.padding = new RectOffset(P(22), P(10), P(0), P(0));
|
||||||
|
|
||||||
primaryStyle = Button(Accent, AccentHover, Color.white, P(14), FontStyle.Bold);
|
primaryStyle = Button(Accent, AccentHover, Color.white, P(14), FontStyle.Bold, P(10));
|
||||||
UiTheme.ApplyPrimaryGradient(primaryStyle);
|
primaryStyle.normal.background = primaryStyle.focused.background = UiTheme.RoundedEx(AccentHover, Accent, Color.clear, false, P(10));
|
||||||
dangerStyle = Button(UiTheme.Danger, UiTheme.DangerHover, Color.white, P(13), FontStyle.Bold);
|
primaryStyle.hover.background = primaryStyle.active.background = primaryStyle.onNormal.background =
|
||||||
secondaryStyle = Button(Secondary, SecondaryHover, Color.white, P(14), FontStyle.Bold);
|
UiTheme.RoundedEx(Color.Lerp(AccentHover, Color.white, 0.12f), AccentHover, Color.clear, false, P(10));
|
||||||
tabStyle = Button(Sidebar, Secondary, Muted, P(11), FontStyle.Bold);
|
dangerStyle = Button(UiTheme.Danger, UiTheme.DangerHover, Color.white, P(13), FontStyle.Bold, P(10));
|
||||||
|
secondaryStyle = Button(Hex(0x22, 0x22, 0x28), Hex(0x2a, 0x2a, 0x31), Color.white, P(14), FontStyle.Bold, P(10), UiTheme.Border);
|
||||||
|
tabStyle = Button(Hex(0x1b, 0x1b, 0x20), Hex(0x24, 0x24, 0x2a), Muted, P(11), FontStyle.Bold, P(10), UiTheme.Border);
|
||||||
tabStyle.hover.textColor = Color.white;
|
tabStyle.hover.textColor = Color.white;
|
||||||
|
|
||||||
fieldStyle = UiTheme.Field(P(14));
|
fieldStyle = UiTheme.Field(P(14));
|
||||||
|
fieldStyle.normal.background = UiTheme.RoundedEx(Hex(0x16, 0x16, 0x1a), Hex(0x16, 0x16, 0x1a), UiTheme.Border, true, P(10), P(10) * 2 + 4);
|
||||||
|
fieldStyle.hover.background = UiTheme.RoundedEx(Hex(0x19, 0x19, 0x1e), Hex(0x19, 0x19, 0x1e), new Color(1f, 1f, 1f, 0.14f), true, P(10), P(10) * 2 + 4);
|
||||||
|
fieldStyle.focused.background = UiTheme.RoundedEx(Hex(0x19, 0x19, 0x1e), Hex(0x19, 0x19, 0x1e), Accent, true, P(10), P(10) * 2 + 4);
|
||||||
|
fieldStyle.border = new RectOffset(P(10) + 2, P(10) + 2, P(10) + 2, P(10) + 2);
|
||||||
|
|
||||||
|
// Ankündigungen: Karte mit Innenabstand, Titel fett auf Höhe des Abzeichens, Text gut lesbar
|
||||||
|
newsBoxStyle = UiTheme.Card(Field, P(10));
|
||||||
|
newsBoxStyle.padding = new RectOffset(P(18), P(12), P(12), P(12));
|
||||||
|
newsTitleStyle = Label(P(15), Color.white, FontStyle.Bold);
|
||||||
|
newsTitleStyle.alignment = TextAnchor.MiddleLeft;
|
||||||
|
newsTitleStyle.wordWrap = true;
|
||||||
|
newsTextStyle = Label(P(13), new Color(0.86f, 0.86f, 0.9f), FontStyle.Normal);
|
||||||
|
newsTextStyle.wordWrap = true;
|
||||||
|
newsTextStyle.margin = new RectOffset(0, 0, P(6), 0);
|
||||||
|
newsCloseStyle = Label(P(18), Muted, FontStyle.Normal);
|
||||||
|
newsCloseStyle.alignment = TextAnchor.MiddleCenter;
|
||||||
|
newsCloseStyle.hover.textColor = Color.white;
|
||||||
|
texNewsInfo = Solid(Hex(0x5a, 0xa0, 0xff));
|
||||||
|
texNewsWarning = Solid(Gold);
|
||||||
|
texNewsCritical = Solid(UiTheme.Danger);
|
||||||
stylesReady = true;
|
stylesReady = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
|
|
||||||
static GUIStyle Button(Color normal, Color hover, Color text, int size, FontStyle font)
|
/// <summary>Knopf mit runden Ecken (9-Slice, siehe UiTheme), optional mit feinem Rand wie .btn auf der Website.</summary>
|
||||||
|
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);
|
||||||
// abgerundete Ecken (9-Slice), siehe UiTheme
|
if (UiTheme.DynamicFont != null) s.font = UiTheme.DynamicFont;
|
||||||
s.normal.background = UiTheme.Rounded(normal);
|
bool withBorder = border.a > 0f;
|
||||||
s.hover.background = UiTheme.Rounded(hover);
|
int h = radius * 2 + 4;
|
||||||
s.active.background = UiTheme.Rounded(hover);
|
Texture2D n = UiTheme.RoundedEx(normal, normal, border, withBorder, radius, h);
|
||||||
s.focused.background = UiTheme.Rounded(normal);
|
Texture2D o = UiTheme.RoundedEx(hover, hover, withBorder ? new Color(border.r, border.g, border.b, Mathf.Min(1f, border.a * 1.8f)) : border, withBorder, radius, h);
|
||||||
s.normal.textColor = s.hover.textColor = s.active.textColor = s.focused.textColor = text;
|
s.normal.background = n;
|
||||||
s.border = new RectOffset(UiTheme.Radius + 2, UiTheme.Radius + 2, UiTheme.Radius + 2, UiTheme.Radius + 2);
|
s.focused.background = n;
|
||||||
|
s.hover.background = o;
|
||||||
|
s.active.background = o;
|
||||||
|
s.onNormal.background = o;
|
||||||
|
s.normal.textColor = s.hover.textColor = s.active.textColor = s.focused.textColor = s.onNormal.textColor = text;
|
||||||
|
s.border = new RectOffset(radius + 2, radius + 2, radius + 2, radius + 2);
|
||||||
s.fontSize = size;
|
s.fontSize = size;
|
||||||
s.fontStyle = font;
|
s.fontStyle = font;
|
||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Weicher runder Schein (Mitte deckend, nach außen durchsichtig) - für den orangen Schimmer oben.</summary>
|
||||||
|
static Texture2D Glow(Color color)
|
||||||
|
{
|
||||||
|
const int w = 128, h = 64;
|
||||||
|
var tex = new Texture2D(w, h, TextureFormat.ARGB32, false);
|
||||||
|
tex.wrapMode = TextureWrapMode.Clamp;
|
||||||
|
tex.filterMode = FilterMode.Bilinear;
|
||||||
|
for (int y = 0; y < h; y++)
|
||||||
|
for (int x = 0; x < w; x++)
|
||||||
|
{
|
||||||
|
float dx = (x + 0.5f) / w * 2f - 1f, dy = (y + 0.5f) / h * 2f - 1f;
|
||||||
|
float d = Mathf.Clamp01(Mathf.Sqrt(dx * dx + dy * dy));
|
||||||
|
float a = 1f - d;
|
||||||
|
tex.SetPixel(x, y, new Color(color.r, color.g, color.b, color.a * a * a));
|
||||||
|
}
|
||||||
|
tex.Apply();
|
||||||
|
tex.hideFlags = HideFlags.HideAndDontSave;
|
||||||
|
return tex;
|
||||||
|
}
|
||||||
|
|
||||||
static Texture2D Solid(Color color)
|
static Texture2D Solid(Color color)
|
||||||
{
|
{
|
||||||
var tex = new Texture2D(1, 1);
|
var tex = new Texture2D(1, 1);
|
||||||
|
|||||||
+123
@@ -0,0 +1,123 @@
|
|||||||
|
# MSCO-Mods schreiben
|
||||||
|
|
||||||
|
MSCO läuft ohne MSCLoader und hat eine eigene, kleine Mod-Schnittstelle. Eine MSCO-Mod ist eine .NET-3.5-DLL mit einer
|
||||||
|
Klasse, die von `MSCO.MscoMod` erbt. MSCLoader-Mods laufen damit **nicht** (sie brauchen MSCLoader).
|
||||||
|
|
||||||
|
## Wo liegen Mods?
|
||||||
|
|
||||||
|
| Ordner | |
|
||||||
|
|---|---|
|
||||||
|
| `<Spielordner>\MSCO\Mods\*.dll` | eigene Mods – immer aktiv, werden beim Spielstart geladen (auch im Singleplayer, siehe unten) |
|
||||||
|
| `<Spielordner>\MSCO\Mods\*.dll.off` | ausgeschaltet (Launcher → *Mods*) |
|
||||||
|
| `<Spielordner>\MSCO\Mods\Servers\*.dll` | Mods von Servern – nur aktiv, solange man auf einem Server ist, der sie verlangt |
|
||||||
|
| Server: `data/mods/*.dll` | Mods, die der dedizierte Server an seine Spieler verteilt (Web-Panel → *Mods*) |
|
||||||
|
|
||||||
|
Im Spiel: Seite **Mods** im MSCO-Menü, Konsole (F7) `mods`. Im Launcher: Seite **Mods** (hinzufügen, aus-/einschalten,
|
||||||
|
entfernen).
|
||||||
|
|
||||||
|
## Singleplayer
|
||||||
|
|
||||||
|
**SINGLEPLAYER** im Launcher lädt die eigenen Mods ebenfalls: MSCO startet dann im Singleplayer-Modus
|
||||||
|
(`-msco-singleplayer`) – nur die Mods, kein MSCO-Menü, kein Multiplayer, der eigene Spielstand. `OnMenuLoad`,
|
||||||
|
`OnGameLoad`, `Update`, `OnGUI` usw. laufen wie gewohnt; `IsSingleplayer` ist `true`, `IsInSession` `false` und
|
||||||
|
`Send` tut nichts. Ohne eingeschaltete Mods (oder mit *Mods im Singleplayer* aus) startet das normale Spiel ohne MSCO.
|
||||||
|
Die F7-Konsole gibt es dort nicht – Meldungen stehen in `output_log.txt`.
|
||||||
|
|
||||||
|
## Server-Mods
|
||||||
|
|
||||||
|
Liegen Mods in `data/mods` des Servers, bekommt jeder Spieler beim Beitreten die Liste (Name, Version, Größe, SHA-256).
|
||||||
|
Fehlt ihm eine Mod, fragt das Spiel: **INSTALL & JOIN** lädt sie direkt vom Server (Prüfsumme wird kontrolliert),
|
||||||
|
**CANCEL** verlässt den Server. Danach geht es ins Spiel. Hat der Spieler dieselbe Datei schon (gleiche Prüfsumme), wird
|
||||||
|
nichts geladen.
|
||||||
|
|
||||||
|
Server mit Mods bekommen in der Serverliste automatisch den Tag **MODDED**; im Server-Browser (Spiel, Launcher, Website)
|
||||||
|
steht, welche Mods sie nutzen, und es gibt Filter *VANILLA* / *MODDED*.
|
||||||
|
|
||||||
|
> Mods sind Programme und laufen mit vollen Rechten auf dem PC der Spieler. Serverbetreiber: nur Mods anbieten, denen ihr
|
||||||
|
> vertraut. Spieler: nur auf Servern zustimmen, denen ihr vertraut.
|
||||||
|
|
||||||
|
## Mod veröffentlichen (Modliste auf msco.mbm.wtf/mods)
|
||||||
|
|
||||||
|
Im MSCO Launcher unter **Mods → Hochladen**: DLL wählen (Name, Version und Beschreibung werden aus den
|
||||||
|
Assembly-Attributen `AssemblyProduct`, `AssemblyInformationalVersion` und `AssemblyDescription` vorgeschlagen), eine
|
||||||
|
**Mod-ID** vergeben (3–40 Zeichen, `a-z 0-9 _ . -`, z.B. `showboltsize`) und hochladen. Hochladen geht mit dem
|
||||||
|
Launcher-Konto; eine Mod-ID gehört danach dir.
|
||||||
|
|
||||||
|
Jede Version wird nach dem Hochladen **automatisch geprüft** und ist erst danach öffentlich: auf
|
||||||
|
<https://msco.mbm.wtf/mods> und im Launcher unter **Mods → Durchsuchen** (ein Klick installiert, SHA-256 wird geprüft,
|
||||||
|
Updates werden angezeigt). Eine neue Version braucht dieselbe Mod-ID und eine höhere Versionsnummer; bis sie freigegeben
|
||||||
|
ist, bleibt die alte öffentlich. Den Status (wartet, öffentlich, abgelehnt mit Grund) siehst du unter **Meine Uploads**.
|
||||||
|
Höchstens 10 MB pro Datei.
|
||||||
|
|
||||||
|
Was die automatische Prüfung macht:
|
||||||
|
|
||||||
|
- **Virenscan** (ClamAV, zusätzlich VirusTotal).
|
||||||
|
- **Lesbarkeit:** Die DLL muss eine normale .NET-DLL sein, die `MSCO.dll` verwendet. Verschlüsselte/obfuskierte Mods
|
||||||
|
werden abgelehnt (siehe unten).
|
||||||
|
- **Was die Mod darf:** Verändert sie nur das Spiel, ist sie meist nach Sekunden bis Minuten öffentlich. Startet sie
|
||||||
|
Programme, nutzt das Internet, löscht Dateien, lädt oder erzeugt Code zur Laufzeit, ruft native Windows-Funktionen auf
|
||||||
|
oder entschlüsselt Daten, schaut sich zusätzlich ein **MSCO-Moderator** den Code an – das dauert dann etwas länger.
|
||||||
|
Das ist erlaubt, wenn es einen guten Grund gibt – schreib ihn in die Beschreibung.
|
||||||
|
|
||||||
|
> **Nicht verschlüsseln (obfuskieren)!** Verschlüsselte Mods (ConfuserEx, Dotfuscator, SmartAssembly …) lassen sich nicht
|
||||||
|
> prüfen und werden automatisch abgelehnt – der Grund steht unter **Meine Uploads**.
|
||||||
|
|
||||||
|
Beispiel mit Quellcode: [`examples/ShowBoltSize`](examples/ShowBoltSize) zeigt unter dem Fadenkreuz die Größe der
|
||||||
|
Schraube, auf die man schaut.
|
||||||
|
|
||||||
|
## Eine Mod bauen
|
||||||
|
|
||||||
|
Vorlage: [`examples/HelloMod`](examples/HelloMod) – Projekt (.NET Framework 3.5) mit Verweisen auf `UnityEngine.dll`
|
||||||
|
(aus `mysummercar_Data\Managed`) und `MSCO.dll` (aus `<Spielordner>\MSCO`, *Copy Local* aus).
|
||||||
|
|
||||||
|
```
|
||||||
|
msbuild examples/HelloMod/HelloMod.csproj -p:Configuration=Release -p:GamePath="C:\...\My Summer Car"
|
||||||
|
```
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using MSCO;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
public class HelloMod : MscoMod
|
||||||
|
{
|
||||||
|
public override string ID { get { return "hellomod"; } } // eindeutig: Buchstaben, Ziffern, . - _
|
||||||
|
public override string Name { get { return "Hello Mod"; } }
|
||||||
|
public override string Version { get { return "1.0.0"; } }
|
||||||
|
public override string Author { get { return "Ich"; } }
|
||||||
|
|
||||||
|
public override void OnGameLoad() { Log("Im Spiel!"); }
|
||||||
|
|
||||||
|
public override void Update()
|
||||||
|
{
|
||||||
|
if (Input.GetKeyDown(KeyCode.H)) Send(System.Text.Encoding.UTF8.GetBytes("wave"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void OnMessage(int fromPlayerId, byte[] data)
|
||||||
|
{
|
||||||
|
string name;
|
||||||
|
Log((Players.TryGetValue(fromPlayerId, out name) ? name : "?") + " winkt");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Name und Version, die Launcher und Server-Panel anzeigen, kommen aus den Assembly-Attributen
|
||||||
|
(`AssemblyProduct`, `AssemblyInformationalVersion`).
|
||||||
|
|
||||||
|
## Schnittstelle
|
||||||
|
|
||||||
|
| Mitglied | |
|
||||||
|
|---|---|
|
||||||
|
| `ID`, `Name`, `Version`, `Author` | Angaben zur Mod (`ID` und `Name` müssen überschrieben werden) |
|
||||||
|
| `OnLoad()` | einmal nach dem Laden |
|
||||||
|
| `OnMenuLoad()`, `OnGameLoad()` | Hauptmenü bzw. Spiel (Szene `GAME`) geladen |
|
||||||
|
| `Update()`, `FixedUpdate()`, `OnGUI()` | jeden Frame (wie bei Unity) |
|
||||||
|
| `OnSessionStart()`, `OnSessionEnd()` | mit Server/Host verbunden bzw. getrennt |
|
||||||
|
| `OnPlayerJoined(id, name)`, `OnPlayerLeft(id, name)` | Mitspieler kommt/geht |
|
||||||
|
| `OnMessage(fromPlayerId, data)` | Daten, die dieselbe Mod (gleiche `ID`) bei einem anderen Spieler mit `Send` geschickt hat |
|
||||||
|
| `Send(byte[] data)` | an dieselbe Mod bei allen anderen Spielern (bis 60 KB; P2P und dedizierter Server) |
|
||||||
|
| `IsInSession`, `IsHost`, `LocalPlayerId`, `Players` | Sitzung, eigene ID, Spieler (ID → Name) |
|
||||||
|
| `IsSingleplayer` | im Singleplayer gestartet (nur Mods, kein Multiplayer) |
|
||||||
|
| `Log(text)`, `LogWarning(text)` | in die MSCO-Konsole (F7) und `output_log.txt` |
|
||||||
|
|
||||||
|
Fehler in einer Mod fängt MSCO ab und schreibt sie ins Log. Wirft eine Mod ständig Fehler (50×), wird sie abgeschaltet.
|
||||||
|
Harmony 1.2 (`0Harmony.dll`) liegt neben `MSCO.dll` und kann von Mods mitbenutzt werden.
|
||||||
@@ -1,22 +1,37 @@
|
|||||||
using MSCLoader;
|
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
|
|
||||||
namespace MSCO
|
namespace MSCO
|
||||||
{
|
{
|
||||||
public class MSCO : Mod
|
/// <summary>
|
||||||
|
/// MSCO - My Summer Car Online. Läuft eigenständig (ohne MSCLoader): gestartet über Unity Doorstop, siehe Bootstrap.
|
||||||
|
/// </summary>
|
||||||
|
public static class MSCO
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Version der Mod (MSCLoader / MSC Launcher). Kleine Updates: nur die dritte Stelle erhöhen (2.8.1, 2.8.2 ...) -
|
/// Version der Mod. Kleine Updates: nur die dritte Stelle erhöhen (2.9.1, 2.9.2 ...) - sie bleiben mit
|
||||||
/// sie bleiben mit 2.8-Servern und -Mitspielern kompatibel.
|
/// Servern und Mitspielern derselben ersten zwei Stellen kompatibel.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const string ModVersion = "2.8.1";
|
public const string ModVersion = "2.10.15";
|
||||||
/// <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 und eine neue Mod-Version 2.9.0 o.ä.
|
/// auch GameServer.ModVersion im Server.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const string ProtocolVersion = "2.8";
|
public const string ProtocolVersion = "2.8";
|
||||||
|
|
||||||
/// <summary>Passt eine andere Version (Server, Host, Mitspieler) zu uns? Verglichen werden nur die ersten zwei Stellen.</summary>
|
/// <summary>
|
||||||
|
/// Sicherheitsstufe dieser Mod-Version - steigt mit jedem Sicherheitsupdate (gleiche Zählung wie
|
||||||
|
/// GameServer.SecurityLevel im Server). Als P2P-Host meldet die Mod sie den Mitspielern.
|
||||||
|
/// 1 = Oktober 2026.
|
||||||
|
/// </summary>
|
||||||
|
public const int SecurityLevel = 1;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Server und P2P-Hosts unter dieser Stufe fehlen Sicherheitsupdates: die Mod tritt ihnen nicht bei. Die
|
||||||
|
/// MSCO-Admins können das Minimum im Keymaster noch anheben (SiteStatus.RequiredServerSecurity).
|
||||||
|
/// </summary>
|
||||||
|
public const int MinServerSecurity = 1;
|
||||||
|
|
||||||
|
/// <summary>Passt eine andere Version (Server, Host, Mitspieler) zu uns? Verglichen wird die Netzwerk-Version.</summary>
|
||||||
public static bool Compatible(string other)
|
public static bool Compatible(string other)
|
||||||
{
|
{
|
||||||
return Protocol(other) == ProtocolVersion;
|
return Protocol(other) == ProtocolVersion;
|
||||||
@@ -30,78 +45,67 @@ namespace MSCO
|
|||||||
return parts.Length >= 2 ? parts[0] + "." + parts[1] : version.Trim();
|
return parts.Length >= 2 ? parts[0] + "." + parts[1] : version.Trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
public override string ID => "MSCO"; // Your (unique) mod ID
|
/// <summary>Beifahrer ein-/aussteigen (Standard P), Konsole (F7), Chat (T) - änderbar mit "bind" in der Konsole.</summary>
|
||||||
public override string Name => "MSCO"; // Your mod name
|
internal static readonly KeyBinding PassengerKey = new KeyBinding("passenger", "Ride along / get out", KeyCode.P);
|
||||||
public override string Author => "MBM"; // Name of the Author (your name)
|
internal static readonly KeyBinding ConsoleKey = new KeyBinding("console", "Open the MSCO console", KeyCode.F7);
|
||||||
public override string Version => ModVersion; // Version
|
internal static readonly KeyBinding ChatKey = new KeyBinding("chat", "Open the chat", KeyCode.T);
|
||||||
public override string Description => "Online mod by mbm.wtf"; // Short description of your mod
|
internal static readonly KeyBinding[] Keys = { PassengerKey, ConsoleKey, ChatKey };
|
||||||
public override Game SupportedGames => Game.MySummerCar;
|
}
|
||||||
// Logo in der MSCLoader-Modliste (branding/logo-128.png, in die DLL eingebettet)
|
|
||||||
public override byte[] Icon { get { return Branding.LogoPng; } set { } }
|
|
||||||
|
|
||||||
bool commandAdded;
|
/// <summary>Tastenbelegung, gespeichert in den PlayerPrefs (früher in den MSCLoader-Einstellungen).</summary>
|
||||||
|
public class KeyBinding
|
||||||
|
{
|
||||||
|
public readonly string Id, Description;
|
||||||
|
public readonly KeyCode Default;
|
||||||
|
bool loaded;
|
||||||
|
KeyCode key;
|
||||||
|
|
||||||
public override void ModSetup()
|
public KeyBinding(string id, string description, KeyCode defaultKey)
|
||||||
{
|
{
|
||||||
SetupFunction(Setup.OnMenuLoad, Mod_OnMenuLoad);
|
Id = id;
|
||||||
SetupFunction(Setup.OnLoad, Mod_OnLoad);
|
Description = description;
|
||||||
SetupFunction(Setup.PostLoad, Mod_PostLoad);
|
Default = defaultKey;
|
||||||
SetupFunction(Setup.OnSave, Mod_OnSave);
|
|
||||||
SetupFunction(Setup.OnGUI, Mod_OnGUI);
|
|
||||||
SetupFunction(Setup.Update, Mod_Update);
|
|
||||||
SetupFunction(Setup.FixedUpdate, Mod_FixedUpdate);
|
|
||||||
SetupFunction(Setup.ModSettings, Mod_Settings);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Taste zum Ein-/Aussteigen als Beifahrer (in den MSCLoader-Einstellungen änderbar).</summary>
|
string Pref { get { return "MSCO_Key_" + Id; } }
|
||||||
internal static SettingsKeybind PassengerKey;
|
|
||||||
/// <summary>MSCO-Konsole (Standard F7) und Chat (Standard T), ebenfalls änderbar.</summary>
|
|
||||||
internal static SettingsKeybind ConsoleKey, ChatKey;
|
|
||||||
|
|
||||||
private void Mod_Settings()
|
public KeyCode Key
|
||||||
{
|
{
|
||||||
// All settings should be created here.
|
get
|
||||||
// DO NOT put anything that isn't settings or keybinds in here!
|
|
||||||
PassengerKey = Keybind.Add("passenger", "Ride as passenger (toggle)", KeyCode.P);
|
|
||||||
ChatKey = Keybind.Add("chat", "Open chat", KeyCode.T);
|
|
||||||
// Nicht F8: im Pausenmenü öffnet F8 ein Test-Menü des Spiels. Neue ID, damit alte F8-Belegungen nicht übernommen werden
|
|
||||||
ConsoleKey = Keybind.Add("console_f7", "Open MSCO console", KeyCode.F7);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void Mod_OnMenuLoad()
|
|
||||||
{
|
|
||||||
MscoLog.Info("Welcome to MSCO by mbm.wtf");
|
|
||||||
// Netzwerk + Lobby leben in einem eigenen Objekt, das Szenenwechsel überlebt
|
|
||||||
MultiplayerManager.Ensure();
|
|
||||||
if (!commandAdded)
|
|
||||||
{
|
{
|
||||||
ConsoleCommand.Add(new MpCommand());
|
if (!loaded)
|
||||||
commandAdded = true;
|
{
|
||||||
|
// Erst im Spiel lesen (PlayerPrefs gehen nicht in statischen Initialisierern)
|
||||||
|
KeyCode parsed;
|
||||||
|
key = TryParse(PlayerPrefs.GetString(Pref, ""), out parsed) ? parsed : Default;
|
||||||
|
loaded = true;
|
||||||
|
}
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
set
|
||||||
|
{
|
||||||
|
key = value;
|
||||||
|
loaded = true;
|
||||||
|
PlayerPrefs.SetString(Pref, value.ToString());
|
||||||
|
PlayerPrefs.Save();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
private void Mod_OnLoad()
|
|
||||||
|
public bool GetKeybindDown()
|
||||||
{
|
{
|
||||||
// Called once, when mod is loading after game is fully loaded
|
return Key != KeyCode.None && Input.GetKeyDown(Key);
|
||||||
}
|
}
|
||||||
private void Mod_PostLoad()
|
|
||||||
|
public static bool TryParse(string text, out KeyCode key)
|
||||||
{
|
{
|
||||||
// Called once, after all mods finished OnLoad
|
key = KeyCode.None;
|
||||||
}
|
if (string.IsNullOrEmpty(text)) return false;
|
||||||
private void Mod_OnSave()
|
try
|
||||||
{
|
{
|
||||||
// Called once, when save and quit
|
key = (KeyCode)System.Enum.Parse(typeof(KeyCode), text.Trim(), true);
|
||||||
}
|
return true;
|
||||||
private void Mod_OnGUI()
|
}
|
||||||
{
|
catch { return false; }
|
||||||
// Lobby/Status zeichnet der MultiplayerManager
|
|
||||||
}
|
|
||||||
private void Mod_Update()
|
|
||||||
{
|
|
||||||
// Netzwerk läuft im MultiplayerManager
|
|
||||||
}
|
|
||||||
private void Mod_FixedUpdate()
|
|
||||||
{
|
|
||||||
// FixedUpdate is called once per fixed frame
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+61
-114
@@ -1,152 +1,99 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
|
||||||
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
|
<!--
|
||||||
|
MSCO (Unity 5 / Mono, .NET 3.5). Baut mit dem normalen .NET SDK - unter Windows und auch auf dem Mac (nur zum
|
||||||
|
Bauen/Veröffentlichen, das Spiel gibt es nur für Windows): dotnet build -c Release
|
||||||
|
Die Spiel-DLLs (UnityEngine, Assembly-CSharp, PlayMaker, cInput) kommen aus dem Spielordner. Woanders (z.B. Mac):
|
||||||
|
den Ordner "mysummercar_Data/Managed" dorthin kopieren und MSC_MANAGED (Umgebungsvariable) oder
|
||||||
|
-p:MscManaged=<ordner> setzen. Release-Builds laden neue Versionen automatisch hoch (release.ps1).
|
||||||
|
-->
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Configuration Condition=" '$(Configuration)' == '' ">Release</Configuration>
|
<TargetFramework>net35</TargetFramework>
|
||||||
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
|
|
||||||
<ProjectGuid>{CE3FFBAB-946E-9569-CCAA-9CFA2D5C11F3}</ProjectGuid>
|
|
||||||
<OutputType>Library</OutputType>
|
<OutputType>Library</OutputType>
|
||||||
<AppDesignerFolder>Properties</AppDesignerFolder>
|
|
||||||
<RootNamespace>MSCO</RootNamespace>
|
|
||||||
<AssemblyName>MSCO</AssemblyName>
|
<AssemblyName>MSCO</AssemblyName>
|
||||||
<TargetFrameworkVersion>v3.5</TargetFrameworkVersion>
|
<RootNamespace>MSCO</RootNamespace>
|
||||||
<FileAlignment>512</FileAlignment>
|
|
||||||
<TargetFrameworkProfile>Unity Full v3.5</TargetFrameworkProfile>
|
|
||||||
<LangVersion>13</LangVersion>
|
<LangVersion>13</LangVersion>
|
||||||
|
<Nullable>disable</Nullable>
|
||||||
|
<ImplicitUsings>disable</ImplicitUsings>
|
||||||
|
<!-- Versionsangaben stehen in Properties/AssemblyInfo.cs (aus MSCO.ModVersion) -->
|
||||||
|
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||||
|
<Configuration Condition=" '$(Configuration)' == '' ">Release</Configuration>
|
||||||
|
<!-- Ausgabe wie bisher: bin\Release\MSCO.dll (ohne Unterordner net35) -->
|
||||||
|
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||||
|
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
|
||||||
|
<DebugType Condition=" '$(Configuration)' == 'Release' ">none</DebugType>
|
||||||
|
<Optimize Condition=" '$(Configuration)' == 'Release' ">true</Optimize>
|
||||||
|
<ProduceReferenceAssembly>false</ProduceReferenceAssembly>
|
||||||
|
<GenerateDocumentationFile>false</GenerateDocumentationFile>
|
||||||
|
<NoWarn>$(NoWarn);CS1591;NU1702</NoWarn>
|
||||||
|
|
||||||
|
<!-- Spielordner und dessen Managed-Ordner (überschreibbar) -->
|
||||||
|
<GamePath Condition=" '$(GamePath)' == '' ">F:\SteamLibrary\steamapps\common\My Summer Car</GamePath>
|
||||||
|
<MscManaged Condition=" '$(MscManaged)' == '' and '$(MSC_MANAGED)' != '' ">$(MSC_MANAGED)</MscManaged>
|
||||||
|
<MscManaged Condition=" '$(MscManaged)' == '' ">$(GamePath)\mysummercar_Data\Managed</MscManaged>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
|
||||||
<DebugSymbols>true</DebugSymbols>
|
<ItemGroup>
|
||||||
<DebugType>full</DebugType>
|
<!-- .NET-3.5-Referenzen ohne installiertes Targeting Pack (auch auf dem Mac) -->
|
||||||
<Optimize>false</Optimize>
|
<PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.3" PrivateAssets="all" />
|
||||||
<OutputPath>bin\Debug\</OutputPath>
|
</ItemGroup>
|
||||||
<DefineConstants>DEBUG;TRACE</DefineConstants>
|
|
||||||
<ErrorReport>prompt</ErrorReport>
|
<ItemGroup>
|
||||||
<WarningLevel>4</WarningLevel>
|
<!-- Beispiel-Mods sind eigene Projekte -->
|
||||||
</PropertyGroup>
|
<Compile Remove="examples/**" />
|
||||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
|
<None Remove="examples/**" />
|
||||||
<DebugType>none</DebugType>
|
<EmbeddedResource Remove="examples/**" />
|
||||||
<Optimize>true</Optimize>
|
</ItemGroup>
|
||||||
<OutputPath>bin\Release\</OutputPath>
|
|
||||||
<DefineConstants>TRACE</DefineConstants>
|
|
||||||
<ErrorReport>prompt</ErrorReport>
|
|
||||||
<WarningLevel>4</WarningLevel>
|
|
||||||
</PropertyGroup>
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Reference Include="System" />
|
|
||||||
<Reference Include="System.Core" />
|
<Reference Include="System.Core" />
|
||||||
<Reference Include="System.Xml.Linq" />
|
<Reference Include="System.Xml.Linq" />
|
||||||
<Reference Include="System.Data.DataSetExtensions" />
|
<Reference Include="System.Data.DataSetExtensions" />
|
||||||
<Reference Include="System.Data" />
|
<Reference Include="System.Data" />
|
||||||
<Reference Include="System.Xml" />
|
<Reference Include="System.Xml" />
|
||||||
<Reference Include="UnityEngine">
|
<Reference Include="UnityEngine">
|
||||||
<HintPath>F:\SteamLibrary\steamapps\common\My Summer Car\mysummercar_Data\Managed\UnityEngine.dll</HintPath>
|
<HintPath>$(MscManaged)\UnityEngine.dll</HintPath>
|
||||||
<Private>False</Private>
|
|
||||||
</Reference>
|
|
||||||
<Reference Include="MSCLoader">
|
|
||||||
<HintPath>F:\SteamLibrary\steamapps\common\My Summer Car\mysummercar_Data\Managed\MSCLoader.dll</HintPath>
|
|
||||||
<Private>False</Private>
|
|
||||||
</Reference>
|
|
||||||
<Reference Include="Newtonsoft.Json">
|
|
||||||
<HintPath>F:\SteamLibrary\steamapps\common\My Summer Car\mysummercar_Data\Managed\Newtonsoft.Json.dll</HintPath>
|
|
||||||
<Private>False</Private>
|
<Private>False</Private>
|
||||||
</Reference>
|
</Reference>
|
||||||
<Reference Include="Assembly-CSharp-firstpass">
|
<Reference Include="Assembly-CSharp-firstpass">
|
||||||
<HintPath>F:\SteamLibrary\steamapps\common\My Summer Car\mysummercar_Data\Managed\Assembly-CSharp-firstpass.dll</HintPath>
|
<HintPath>$(MscManaged)\Assembly-CSharp-firstpass.dll</HintPath>
|
||||||
<Private>False</Private>
|
<Private>False</Private>
|
||||||
</Reference>
|
</Reference>
|
||||||
<Reference Include="Assembly-CSharp">
|
<Reference Include="Assembly-CSharp">
|
||||||
<HintPath>F:\SteamLibrary\steamapps\common\My Summer Car\mysummercar_Data\Managed\Assembly-CSharp.dll</HintPath>
|
<HintPath>$(MscManaged)\Assembly-CSharp.dll</HintPath>
|
||||||
<Private>False</Private>
|
<Private>False</Private>
|
||||||
</Reference>
|
</Reference>
|
||||||
<Reference Include="cInput">
|
<Reference Include="cInput">
|
||||||
<HintPath>F:\SteamLibrary\steamapps\common\My Summer Car\mysummercar_Data\Managed\cInput.dll</HintPath>
|
<HintPath>$(MscManaged)\cInput.dll</HintPath>
|
||||||
<Private>False</Private>
|
<Private>False</Private>
|
||||||
</Reference>
|
</Reference>
|
||||||
<Reference Include="PlayMaker">
|
<Reference Include="PlayMaker">
|
||||||
<HintPath>F:\SteamLibrary\steamapps\common\My Summer Car\mysummercar_Data\Managed\PlayMaker.dll</HintPath>
|
<HintPath>$(MscManaged)\PlayMaker.dll</HintPath>
|
||||||
<Private>False</Private>
|
<Private>False</Private>
|
||||||
</Reference>
|
</Reference>
|
||||||
<Reference Include="0Harmony">
|
<Reference Include="0Harmony">
|
||||||
<HintPath>F:\SteamLibrary\steamapps\common\My Summer Car\mysummercar_Data\Managed\0Harmony.dll</HintPath>
|
<HintPath>lib\0Harmony.dll</HintPath>
|
||||||
<Private>False</Private>
|
<Private>False</Private>
|
||||||
</Reference>
|
</Reference>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Compile Include="MoneySync.cs" />
|
<EmbeddedResource Include="menu-logo.png">
|
||||||
<Compile Include="WheelSpin.cs" />
|
<LogicalName>MSCO.menu-logo.png</LogicalName>
|
||||||
<Compile Include="DamageSync.cs" />
|
</EmbeddedResource>
|
||||||
<Compile Include="WorldVarSync.cs" />
|
|
||||||
<Compile Include="TrafficSync.cs" />
|
|
||||||
<Compile Include="HingeSync.cs" />
|
|
||||||
<Compile Include="ControlSync.cs" />
|
|
||||||
<Compile Include="Toasts.cs" />
|
|
||||||
<Compile Include="PlayerNeeds.cs" />
|
|
||||||
<Compile Include="Favorites.cs" />
|
|
||||||
<Compile Include="MpCommand.cs" />
|
|
||||||
<Compile Include="Commands.cs" />
|
|
||||||
<Compile Include="MscoLog.cs" />
|
|
||||||
<Compile Include="ItemSpawner.cs" />
|
|
||||||
<Compile Include="Cheats.cs" />
|
|
||||||
<Compile Include="InputLock.cs" />
|
|
||||||
<Compile Include="Overlays.cs" />
|
|
||||||
<Compile Include="SafetyPatches.cs" />
|
|
||||||
<Compile Include="SceneCache.cs" />
|
|
||||||
<Compile Include="UiTheme.cs" />
|
|
||||||
<Compile Include="VehicleStateSync.cs" />
|
|
||||||
<Compile Include="Branding.cs" />
|
|
||||||
<Compile Include="DiscordPresence.cs" />
|
|
||||||
<Compile Include="DoorSync.cs" />
|
|
||||||
<Compile Include="FsmSync.cs" />
|
|
||||||
<Compile Include="JoinLinks.cs" />
|
|
||||||
<Compile Include="LobbyUI.cs" />
|
|
||||||
<Compile Include="MenuHelper.cs" />
|
|
||||||
<Compile Include="MenuInputBlocker.cs" />
|
|
||||||
<Compile Include="MSCO.cs" />
|
|
||||||
<Compile Include="MultiplayerManager.cs" />
|
|
||||||
<Compile Include="NetSession.cs" />
|
|
||||||
<Compile Include="ObjectSync.cs" />
|
|
||||||
<Compile Include="Passenger.cs" />
|
|
||||||
<Compile Include="GameCharacter.cs" />
|
|
||||||
<Compile Include="SaveSync.cs" />
|
|
||||||
<Compile Include="ServerBrowser.cs" />
|
|
||||||
<Compile Include="SteamLobby.cs" />
|
|
||||||
<Compile Include="VehicleFx.cs" />
|
|
||||||
<Compile Include="WinHttp.cs" />
|
|
||||||
<Compile Include="WorldSync.cs" />
|
|
||||||
<EmbeddedResource Include="logo.png">
|
<EmbeddedResource Include="logo.png">
|
||||||
<LogicalName>MSCO.logo.png</LogicalName>
|
<LogicalName>MSCO.logo.png</LogicalName>
|
||||||
</EmbeddedResource>
|
</EmbeddedResource>
|
||||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
|
||||||
<Compile Include="RemotePlayer.cs" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
|
||||||
<PropertyGroup>
|
|
||||||
<MSCMODSFOLDER>C:\Users\nico\Documents\MySummerCar\Mods</MSCMODSFOLDER>
|
|
||||||
<MWCMODSFOLDER>NONE</MWCMODSFOLDER>
|
|
||||||
<ABFOLDER>NONE</ABFOLDER>
|
|
||||||
<PostBuildEvent>
|
|
||||||
echo .
|
|
||||||
echo "== Running post build actions for configuration $(ConfigurationName)! ==="
|
|
||||||
|
|
||||||
IF "$(ConfigurationName)" == "Release" (
|
<!-- Zum Testen: MSCO.dll direkt in den Spielordner (My Summer Car\MSCO) - läuft das Spiel, ist sie gesperrt -->
|
||||||
IF NOT "$(MSCMODSFOLDER)" == "NONE" (
|
<Target Name="CopyToGame" AfterTargets="Build" Condition="Exists('$(GamePath)\MSCO')">
|
||||||
copy "$(TargetPath)" "$(MSCMODSFOLDER)" /y
|
<Copy SourceFiles="$(TargetPath)" DestinationFolder="$(GamePath)\MSCO" ContinueOnError="true" />
|
||||||
echo "Copied dll into MSC mods folder!"
|
</Target>
|
||||||
)
|
|
||||||
IF NOT "$(MWCMODSFOLDER)" == "NONE" (
|
|
||||||
copy "$(TargetPath)" "$(MWCMODSFOLDER)" /y
|
|
||||||
echo "Copied dll into MWC mods folder!"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
if "$(ConfigurationName)" == "Debug" (
|
<!-- Release-Build: neue Version automatisch auf den Keymaster laden (Token + Signier-Schlüssel, siehe release.ps1) -->
|
||||||
copy "$(TargetPath)" "C:\Users\nico\Documents\MySummerCar\Mods" /y
|
<Target Name="MscoRelease" AfterTargets="Build" Condition="'$(Configuration)' == 'Release'">
|
||||||
copy "$(TargetDir)$(TargetName).pdb" "C:\Users\nico\Documents\MySummerCar\Mods" /y
|
<Exec Condition="'$(OS)' == 'Windows_NT'" Command="powershell -NoProfile -ExecutionPolicy Bypass -File "$(MSBuildProjectDirectory)\release.ps1" -Product mod -File "$(TargetPath)"" ContinueOnError="true" />
|
||||||
cd "C:\Users\nico\Documents\MySummerCar\Mods"
|
<Exec Condition="'$(OS)' != 'Windows_NT'" Command="pwsh -NoProfile -File "$(MSBuildProjectDirectory)/release.ps1" -Product mod -File "$(TargetPath)"" ContinueOnError="true" />
|
||||||
call "C:\Users\nico\Documents\MySummerCar\Mods\debug.bat"
|
</Target>
|
||||||
) ELSE (
|
|
||||||
copy "$(TargetPath)" "C:\Users\nico\Documents\MySummerCar\Mods\Mods" /y
|
|
||||||
)
|
|
||||||
</PostBuildEvent>
|
|
||||||
</PropertyGroup>
|
|
||||||
</Project>
|
</Project>
|
||||||
+182
@@ -0,0 +1,182 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Text;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace MSCO
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// MSCO statt "My Summer Car": Fenstertitel und -symbol, und im Hauptmenü das große Spiel-Logo durch das MSCO-Logo
|
||||||
|
/// (branding/menu-logo.png, in die DLL eingebettet) ersetzen.
|
||||||
|
/// </summary>
|
||||||
|
public static class MenuBranding
|
||||||
|
{
|
||||||
|
const string WindowTitle = "MSCO - My Summer Car Online";
|
||||||
|
const string MenuLogoResource = "MSCO.menu-logo.png";
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------- Fenster
|
||||||
|
|
||||||
|
delegate bool EnumWindowsProc(IntPtr hwnd, IntPtr lParam);
|
||||||
|
[DllImport("user32.dll")] static extern bool EnumWindows(EnumWindowsProc callback, IntPtr lParam);
|
||||||
|
[DllImport("user32.dll")] static extern uint GetWindowThreadProcessId(IntPtr hwnd, out uint processId);
|
||||||
|
[DllImport("user32.dll", CharSet = CharSet.Unicode)] static extern int GetClassNameW(IntPtr hwnd, StringBuilder name, int max);
|
||||||
|
[DllImport("user32.dll", CharSet = CharSet.Unicode)] static extern bool SetWindowTextW(IntPtr hwnd, string text);
|
||||||
|
[DllImport("user32.dll")] static extern IntPtr SendMessageW(IntPtr hwnd, uint msg, IntPtr wParam, IntPtr lParam);
|
||||||
|
[DllImport("user32.dll")] static extern IntPtr CreateIconFromResourceEx(byte[] data, uint size, bool icon, uint version, int cx, int cy, uint flags);
|
||||||
|
|
||||||
|
static readonly EnumWindowsProc enumProc = OnWindow; // Delegate festhalten (sonst räumt der GC ihn weg)
|
||||||
|
static uint processId;
|
||||||
|
static IntPtr bigIcon, smallIcon;
|
||||||
|
|
||||||
|
/// <summary>Titel und Symbol des Spielfensters auf MSCO setzen (beim Start und nach jedem Szenenwechsel).</summary>
|
||||||
|
public static void ApplyWindow()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
processId = (uint)Process.GetCurrentProcess().Id;
|
||||||
|
if (bigIcon == IntPtr.Zero && Branding.LogoPng.Length > 0)
|
||||||
|
{
|
||||||
|
// PNG-Daten direkt als Symbol (geht seit Windows Vista)
|
||||||
|
bigIcon = CreateIconFromResourceEx(Branding.LogoPng, (uint)Branding.LogoPng.Length, true, 0x00030000, 0, 0, 0);
|
||||||
|
smallIcon = CreateIconFromResourceEx(Branding.LogoPng, (uint)Branding.LogoPng.Length, true, 0x00030000, 16, 16, 0);
|
||||||
|
}
|
||||||
|
EnumWindows(enumProc, IntPtr.Zero);
|
||||||
|
}
|
||||||
|
catch (Exception e) { Bootstrap.Log("Window title/icon: " + e.Message); }
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool OnWindow(IntPtr hwnd, IntPtr lParam)
|
||||||
|
{
|
||||||
|
uint pid;
|
||||||
|
GetWindowThreadProcessId(hwnd, out pid);
|
||||||
|
if (pid != processId) return true;
|
||||||
|
var name = new StringBuilder(64);
|
||||||
|
GetClassNameW(hwnd, name, name.Capacity);
|
||||||
|
if (name.ToString() != "UnityWndClass") return true;
|
||||||
|
SetWindowTextW(hwnd, WindowTitle);
|
||||||
|
const uint WM_SETICON = 0x0080;
|
||||||
|
if (bigIcon != IntPtr.Zero) SendMessageW(hwnd, WM_SETICON, (IntPtr)1, bigIcon);
|
||||||
|
if (smallIcon != IntPtr.Zero) SendMessageW(hwnd, WM_SETICON, IntPtr.Zero, smallIcon);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------- Logo im Hauptmenü
|
||||||
|
|
||||||
|
static Texture2D source;
|
||||||
|
static readonly Dictionary<string, Texture2D> fitted = new Dictionary<string, Texture2D>();
|
||||||
|
static bool dumped;
|
||||||
|
|
||||||
|
static Texture2D Source
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (source != null) return source;
|
||||||
|
using (Stream s = Assembly.GetExecutingAssembly().GetManifestResourceStream(MenuLogoResource))
|
||||||
|
{
|
||||||
|
if (s == null) return null;
|
||||||
|
var data = new byte[s.Length];
|
||||||
|
int read = 0;
|
||||||
|
while (read < data.Length) read += s.Read(data, read, data.Length - read);
|
||||||
|
source = new Texture2D(2, 2, TextureFormat.ARGB32, false);
|
||||||
|
source.LoadImage(data);
|
||||||
|
source.hideFlags = HideFlags.HideAndDontSave;
|
||||||
|
}
|
||||||
|
return source;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Ist das eine Textur des Spiel-Logos? (Name enthält "logo", aber nicht vom Auto/Laden ...)</summary>
|
||||||
|
static bool IsGameLogo(Texture t)
|
||||||
|
{
|
||||||
|
if (t == null || t == source) return false;
|
||||||
|
string n = t.name.ToLowerInvariant();
|
||||||
|
return n.Contains("logo") && !n.Contains("msco") && !n.Contains("satsuma") && !n.Contains("teimo") && !n.Contains("amistech");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Im Hauptmenü das Spiel-Logo ersetzen. Gibt die Anzahl ersetzter Texturen zurück.</summary>
|
||||||
|
public static int ReplaceMenuLogo()
|
||||||
|
{
|
||||||
|
if (Source == null) return 0;
|
||||||
|
int replaced = 0;
|
||||||
|
var seen = new List<string>();
|
||||||
|
// Auch ausgeblendete Objekte (das Menü wechselt zwischen mehreren Logo-Objekten)
|
||||||
|
foreach (Renderer r in Resources.FindObjectsOfTypeAll<Renderer>())
|
||||||
|
{
|
||||||
|
if (r == null || r.gameObject.hideFlags != HideFlags.None) continue;
|
||||||
|
Material[] materials = r.sharedMaterials;
|
||||||
|
for (int i = 0; i < materials.Length; i++)
|
||||||
|
{
|
||||||
|
Material m = materials[i];
|
||||||
|
if (m == null || m.mainTexture == null) continue;
|
||||||
|
if (!dumped && seen.Count < 200) seen.Add(Path(r.transform) + " : " + m.mainTexture.name);
|
||||||
|
if (!IsGameLogo(m.mainTexture)) continue;
|
||||||
|
Texture original = m.mainTexture;
|
||||||
|
m.mainTexture = Fit(original.width, original.height);
|
||||||
|
Bootstrap.Log("Menu logo replaced: " + Path(r.transform) + " (" + original.name + ", " + original.width + "x" + original.height + ")");
|
||||||
|
replaced++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
foreach (SpriteRenderer sr in UnityEngine.Object.FindObjectsOfType<SpriteRenderer>())
|
||||||
|
{
|
||||||
|
if (sr == null || sr.sprite == null || !IsGameLogo(sr.sprite.texture)) continue;
|
||||||
|
Texture2D tex = Fit(sr.sprite.texture.width, sr.sprite.texture.height);
|
||||||
|
sr.sprite = Sprite.Create(tex, new Rect(0, 0, tex.width, tex.height), new Vector2(0.5f, 0.5f), sr.sprite.pixelsPerUnit);
|
||||||
|
Bootstrap.Log("Menu logo (sprite) replaced: " + Path(sr.transform));
|
||||||
|
replaced++;
|
||||||
|
}
|
||||||
|
foreach (GUITexture g in UnityEngine.Object.FindObjectsOfType<GUITexture>())
|
||||||
|
{
|
||||||
|
if (g == null || !IsGameLogo(g.texture)) continue;
|
||||||
|
g.texture = Fit(g.texture.width, g.texture.height);
|
||||||
|
Bootstrap.Log("Menu logo (GUITexture) replaced: " + Path(g.transform));
|
||||||
|
replaced++;
|
||||||
|
}
|
||||||
|
if (!dumped && replaced == 0)
|
||||||
|
{
|
||||||
|
// Zur Diagnose: welche Texturen gibt es im Menü? (nur einmal)
|
||||||
|
Bootstrap.Log("Menu logo not found. Textures in the menu:\n " + string.Join("\n ", seen.ToArray()));
|
||||||
|
}
|
||||||
|
dumped = true;
|
||||||
|
return replaced;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>MSCO-Logo mittig in eine Textur der Größe des Originals (gleiches Seitenverhältnis wie dessen Fläche).</summary>
|
||||||
|
static Texture2D Fit(int width, int height)
|
||||||
|
{
|
||||||
|
width = Mathf.Clamp(width, 64, 2048);
|
||||||
|
height = Mathf.Clamp(height, 64, 2048);
|
||||||
|
string key = width + "x" + height;
|
||||||
|
Texture2D tex;
|
||||||
|
if (fitted.TryGetValue(key, out tex) && tex != null) return tex;
|
||||||
|
|
||||||
|
tex = new Texture2D(width, height, TextureFormat.ARGB32, true);
|
||||||
|
tex.hideFlags = HideFlags.HideAndDontSave;
|
||||||
|
tex.wrapMode = TextureWrapMode.Clamp;
|
||||||
|
float scale = Mathf.Min((float)width / Source.width, (float)height / Source.height);
|
||||||
|
float w = Source.width * scale, h = Source.height * scale;
|
||||||
|
float x0 = (width - w) / 2f, y0 = (height - h) / 2f;
|
||||||
|
var pixels = new Color[width * height];
|
||||||
|
for (int y = 0; y < height; y++)
|
||||||
|
for (int x = 0; x < width; x++)
|
||||||
|
{
|
||||||
|
float u = (x - x0) / w, v = (y - y0) / h;
|
||||||
|
pixels[y * width + x] = u < 0f || u > 1f || v < 0f || v > 1f ? new Color(0, 0, 0, 0) : Source.GetPixelBilinear(u, v);
|
||||||
|
}
|
||||||
|
tex.SetPixels(pixels);
|
||||||
|
tex.Apply(true);
|
||||||
|
fitted[key] = tex;
|
||||||
|
return tex;
|
||||||
|
}
|
||||||
|
|
||||||
|
static string Path(Transform t)
|
||||||
|
{
|
||||||
|
string path = t.name;
|
||||||
|
for (Transform p = t.parent; p != null; p = p.parent) path = p.name + "/" + path;
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+26
-3
@@ -1,7 +1,6 @@
|
|||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using HutongGames.PlayMaker;
|
using HutongGames.PlayMaker;
|
||||||
using HutongGames.PlayMaker.Actions;
|
using HutongGames.PlayMaker.Actions;
|
||||||
using MSCLoader;
|
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
|
|
||||||
namespace MSCO
|
namespace MSCO
|
||||||
@@ -12,8 +11,32 @@ namespace MSCO
|
|||||||
public const string MenuScene = "MainMenu";
|
public const string MenuScene = "MainMenu";
|
||||||
public const string GameScene = "GAME";
|
public const string GameScene = "GAME";
|
||||||
|
|
||||||
public static bool InMenu { get { return Application.loadedLevelName == MenuScene; } }
|
public static bool InMenu { get { return Scene == MenuScene; } }
|
||||||
public static bool InGame { get { return Application.loadedLevelName == GameScene; } }
|
public static bool InGame { get { return Scene == GameScene; } }
|
||||||
|
|
||||||
|
static string scene;
|
||||||
|
static int sceneFrame = -1;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Aktuelle Szene, einmal pro Bild gelesen: Application.loadedLevelName erzeugt bei jedem Aufruf einen neuen String
|
||||||
|
/// (Müll für den Garbage Collector), und InGame/InMenu werden pro Bild sehr oft abgefragt.
|
||||||
|
/// </summary>
|
||||||
|
public static string Scene
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
int frame = Time.frameCount;
|
||||||
|
if (frame != sceneFrame || scene == null)
|
||||||
|
{
|
||||||
|
sceneFrame = frame;
|
||||||
|
scene = Application.loadedLevelName;
|
||||||
|
}
|
||||||
|
return scene;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Nach einem Szenenwechsel (OnLevelWasLoaded) neu lesen.</summary>
|
||||||
|
public static void SceneChanged() { scene = null; }
|
||||||
|
|
||||||
static readonly MethodInfo switchState =
|
static readonly MethodInfo switchState =
|
||||||
typeof(Fsm).GetMethod("SwitchState", BindingFlags.NonPublic | BindingFlags.Instance);
|
typeof(Fsm).GetMethod("SwitchState", BindingFlags.NonPublic | BindingFlags.Instance);
|
||||||
|
|||||||
+166
@@ -0,0 +1,166 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace MSCO
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Kleiner eigener JSON-Leser (ohne Newtonsoft.Json - das braucht System.Runtime.Serialization, das ohne MSCLoader im
|
||||||
|
/// Spiel fehlt). Ergebnis: Dictionary<string, object> (Objekt, Schlüssel ohne Groß-/Kleinschreibung),
|
||||||
|
/// List<object>, string, double, bool oder null.
|
||||||
|
/// </summary>
|
||||||
|
public static class MiniJson
|
||||||
|
{
|
||||||
|
public static object Parse(string json)
|
||||||
|
{
|
||||||
|
if (json == null) throw new FormatException("No JSON");
|
||||||
|
int i = 0;
|
||||||
|
object value = ReadValue(json, ref i);
|
||||||
|
SkipWhitespace(json, ref i);
|
||||||
|
if (i < json.Length) throw new FormatException("Unexpected text at " + i);
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------- Zugriff
|
||||||
|
|
||||||
|
public static string Str(Dictionary<string, object> o, string key, string fallback = null)
|
||||||
|
{
|
||||||
|
object v;
|
||||||
|
if (o == null || !o.TryGetValue(key, out v) || v == null) return fallback;
|
||||||
|
if (v is double) return ((double)v).ToString(CultureInfo.InvariantCulture);
|
||||||
|
return v.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int Int(Dictionary<string, object> o, string key, int fallback = 0)
|
||||||
|
{
|
||||||
|
object v;
|
||||||
|
if (o == null || !o.TryGetValue(key, out v)) return fallback;
|
||||||
|
if (v is double) return (int)(double)v;
|
||||||
|
int parsed;
|
||||||
|
return v is string && int.TryParse((string)v, NumberStyles.Integer, CultureInfo.InvariantCulture, out parsed) ? parsed : fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool Bool(Dictionary<string, object> o, string key, bool fallback = false)
|
||||||
|
{
|
||||||
|
object v;
|
||||||
|
if (o == null || !o.TryGetValue(key, out v)) return fallback;
|
||||||
|
return v is bool ? (bool)v : fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<string> Strings(Dictionary<string, object> o, string key)
|
||||||
|
{
|
||||||
|
var result = new List<string>();
|
||||||
|
object v;
|
||||||
|
if (o == null || !o.TryGetValue(key, out v)) return result;
|
||||||
|
var list = v as List<object>;
|
||||||
|
if (list != null) foreach (object item in list) if (item != null) result.Add(item.ToString());
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------- Lesen
|
||||||
|
|
||||||
|
static object ReadValue(string s, ref int i)
|
||||||
|
{
|
||||||
|
SkipWhitespace(s, ref i);
|
||||||
|
if (i >= s.Length) throw new FormatException("Unexpected end of JSON");
|
||||||
|
char c = s[i];
|
||||||
|
if (c == '{') return ReadObject(s, ref i);
|
||||||
|
if (c == '[') return ReadArray(s, ref i);
|
||||||
|
if (c == '"') return ReadString(s, ref i);
|
||||||
|
if (c == '-' || (c >= '0' && c <= '9')) return ReadNumber(s, ref i);
|
||||||
|
if (Word(s, ref i, "true")) return true;
|
||||||
|
if (Word(s, ref i, "false")) return false;
|
||||||
|
if (Word(s, ref i, "null")) return null;
|
||||||
|
throw new FormatException("Unexpected '" + c + "' at " + i);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Dictionary<string, object> ReadObject(string s, ref int i)
|
||||||
|
{
|
||||||
|
var o = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
i++; // {
|
||||||
|
SkipWhitespace(s, ref i);
|
||||||
|
if (i < s.Length && s[i] == '}') { i++; return o; }
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
SkipWhitespace(s, ref i);
|
||||||
|
if (i >= s.Length || s[i] != '"') throw new FormatException("Expected a name at " + i);
|
||||||
|
string key = ReadString(s, ref i);
|
||||||
|
SkipWhitespace(s, ref i);
|
||||||
|
if (i >= s.Length || s[i] != ':') throw new FormatException("Expected ':' at " + i);
|
||||||
|
i++;
|
||||||
|
o[key] = ReadValue(s, ref i);
|
||||||
|
SkipWhitespace(s, ref i);
|
||||||
|
if (i >= s.Length) throw new FormatException("Unexpected end of JSON");
|
||||||
|
if (s[i] == ',') { i++; continue; }
|
||||||
|
if (s[i] == '}') { i++; return o; }
|
||||||
|
throw new FormatException("Expected ',' or '}' at " + i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<object> ReadArray(string s, ref int i)
|
||||||
|
{
|
||||||
|
var list = new List<object>();
|
||||||
|
i++; // [
|
||||||
|
SkipWhitespace(s, ref i);
|
||||||
|
if (i < s.Length && s[i] == ']') { i++; return list; }
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
list.Add(ReadValue(s, ref i));
|
||||||
|
SkipWhitespace(s, ref i);
|
||||||
|
if (i >= s.Length) throw new FormatException("Unexpected end of JSON");
|
||||||
|
if (s[i] == ',') { i++; continue; }
|
||||||
|
if (s[i] == ']') { i++; return list; }
|
||||||
|
throw new FormatException("Expected ',' or ']' at " + i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static string ReadString(string s, ref int i)
|
||||||
|
{
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
i++; // "
|
||||||
|
while (i < s.Length)
|
||||||
|
{
|
||||||
|
char c = s[i++];
|
||||||
|
if (c == '"') return sb.ToString();
|
||||||
|
if (c != '\\') { sb.Append(c); continue; }
|
||||||
|
if (i >= s.Length) break;
|
||||||
|
char e = s[i++];
|
||||||
|
switch (e)
|
||||||
|
{
|
||||||
|
case 'b': sb.Append('\b'); break;
|
||||||
|
case 'f': sb.Append('\f'); break;
|
||||||
|
case 'n': sb.Append('\n'); break;
|
||||||
|
case 'r': sb.Append('\r'); break;
|
||||||
|
case 't': sb.Append('\t'); break;
|
||||||
|
case 'u':
|
||||||
|
if (i + 4 > s.Length) throw new FormatException("Bad \\u escape");
|
||||||
|
sb.Append((char)int.Parse(s.Substring(i, 4), NumberStyles.HexNumber, CultureInfo.InvariantCulture));
|
||||||
|
i += 4;
|
||||||
|
break;
|
||||||
|
default: sb.Append(e); break; // \" \\ \/
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new FormatException("Unterminated string");
|
||||||
|
}
|
||||||
|
|
||||||
|
static double ReadNumber(string s, ref int i)
|
||||||
|
{
|
||||||
|
int start = i;
|
||||||
|
while (i < s.Length && "+-0123456789.eE".IndexOf(s[i]) >= 0) i++;
|
||||||
|
return double.Parse(s.Substring(start, i - start), NumberStyles.Float, CultureInfo.InvariantCulture);
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool Word(string s, ref int i, string word)
|
||||||
|
{
|
||||||
|
if (string.CompareOrdinal(s, i, word, 0, word.Length) != 0) return false;
|
||||||
|
i += word.Length;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void SkipWhitespace(string s, ref int i)
|
||||||
|
{
|
||||||
|
while (i < s.Length && char.IsWhiteSpace(s[i])) i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace MSCO
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Basis für MSCO-Mods. Eine Mod ist eine .NET-3.5-DLL in <Spiel>\MSCO\Mods mit (mindestens) einer Klasse, die
|
||||||
|
/// von MscoMod erbt. MSCO lädt sie beim Spielstart; Mods eines Servers bekommen Spieler beim Beitreten (nach
|
||||||
|
/// Zustimmung) und sie sind nur auf diesem Server aktiv. Anleitung: MODDING.md im MSCO-Repository.
|
||||||
|
/// </summary>
|
||||||
|
public abstract class MscoMod
|
||||||
|
{
|
||||||
|
/// <summary>Eindeutige ID (Buchstaben, Ziffern, . - _). Mods mit derselben ID reden über Send/OnMessage miteinander.</summary>
|
||||||
|
public abstract string ID { get; }
|
||||||
|
public abstract string Name { get; }
|
||||||
|
public virtual string Version { get { return "1.0.0"; } }
|
||||||
|
public virtual string Author { get { return ""; } }
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------- Lebenszyklus (überschreiben nach Bedarf)
|
||||||
|
|
||||||
|
/// <summary>Einmal nach dem Laden der Mod (Hauptmenü oder - bei Server-Mods - beim Beitreten).</summary>
|
||||||
|
public virtual void OnLoad() { }
|
||||||
|
/// <summary>Hauptmenü wurde geladen.</summary>
|
||||||
|
public virtual void OnMenuLoad() { }
|
||||||
|
/// <summary>Das Spiel (Szene GAME) wurde geladen.</summary>
|
||||||
|
public virtual void OnGameLoad() { }
|
||||||
|
public virtual void Update() { }
|
||||||
|
public virtual void FixedUpdate() { }
|
||||||
|
public virtual void OnGUI() { }
|
||||||
|
|
||||||
|
/// <summary>Mit einem Server/Host verbunden (bzw. selbst P2P-Host geworden).</summary>
|
||||||
|
public virtual void OnSessionStart() { }
|
||||||
|
/// <summary>Sitzung beendet (getrennt, gekickt, Host weg ...).</summary>
|
||||||
|
public virtual void OnSessionEnd() { }
|
||||||
|
public virtual void OnPlayerJoined(int playerId, string name) { }
|
||||||
|
public virtual void OnPlayerLeft(int playerId, string name) { }
|
||||||
|
/// <summary>Daten, die dieselbe Mod bei einem anderen Spieler mit Send() geschickt hat.</summary>
|
||||||
|
public virtual void OnMessage(int fromPlayerId, byte[] data) { }
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------- Helfer
|
||||||
|
|
||||||
|
/// <summary>Daten an dieselbe Mod bei allen anderen Spielern schicken (max. 60 KB, nur in einer Sitzung).</summary>
|
||||||
|
protected bool Send(byte[] data)
|
||||||
|
{
|
||||||
|
MultiplayerManager mp = MultiplayerManager.Instance;
|
||||||
|
return mp != null && mp.SendModMessage(ID, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void Log(string text) { MscoLog.Info("[" + Name + "] " + text); }
|
||||||
|
protected void LogWarning(string text) { MscoLog.Warning("[" + Name + "] " + text); }
|
||||||
|
|
||||||
|
/// <summary>Singleplayer mit Mods (Launcher: SINGLEPLAYER) - kein MSCO-Menü, kein Multiplayer, Send() geht nicht.</summary>
|
||||||
|
protected bool IsSingleplayer { get { return Bootstrap.Singleplayer; } }
|
||||||
|
/// <summary>In einer Sitzung (Server oder P2P)?</summary>
|
||||||
|
protected bool IsInSession { get { return MultiplayerManager.Instance != null && MultiplayerManager.Instance.IsInSession; } }
|
||||||
|
/// <summary>Selbst P2P-Host?</summary>
|
||||||
|
protected bool IsHost { get { return MultiplayerManager.Instance != null && MultiplayerManager.Instance.IsHost; } }
|
||||||
|
/// <summary>Eigene Spieler-ID in der Sitzung.</summary>
|
||||||
|
protected int LocalPlayerId { get { return MultiplayerManager.Instance != null ? MultiplayerManager.Instance.LocalId : -1; } }
|
||||||
|
/// <summary>Spieler der Sitzung: ID -> Name.</summary>
|
||||||
|
protected Dictionary<int, string> Players
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
var result = new Dictionary<int, string>();
|
||||||
|
MultiplayerManager mp = MultiplayerManager.Instance;
|
||||||
|
if (mp != null) foreach (PlayerInfo p in mp.Players) result[p.Id] = p.Name;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+167
@@ -0,0 +1,167 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
|
||||||
|
namespace MSCO
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Lädt MSCO-Mods und ruft sie auf. Eigene Mods: <Spiel>\MSCO\Mods\*.dll (immer aktiv). Mods eines Servers:
|
||||||
|
/// <Spiel>\MSCO\Mods\Servers\<id>.dll - nur aktiv, solange man auf einem Server ist, der sie verlangt.
|
||||||
|
/// Fehler einer Mod werden abgefangen und protokolliert; wirft sie ständig Fehler, wird sie abgeschaltet.
|
||||||
|
/// </summary>
|
||||||
|
public static class ModLoader
|
||||||
|
{
|
||||||
|
public sealed class LoadedMod
|
||||||
|
{
|
||||||
|
public MscoMod Mod;
|
||||||
|
public string File, Sha256;
|
||||||
|
public bool FromServer, Active;
|
||||||
|
public int Errors;
|
||||||
|
public string Error;
|
||||||
|
}
|
||||||
|
|
||||||
|
const int MaxErrors = 50;
|
||||||
|
static readonly List<LoadedMod> mods = new List<LoadedMod>();
|
||||||
|
static bool localLoaded;
|
||||||
|
|
||||||
|
public static IList<LoadedMod> All { get { return mods.AsReadOnly(); } }
|
||||||
|
public static string Folder { get { return Path.Combine(Bootstrap.Folder ?? ".", "Mods"); } }
|
||||||
|
public static string ServerFolder { get { return Path.Combine(Folder, "Servers"); } }
|
||||||
|
public static string ServerFile(string id) { return Path.Combine(ServerFolder, id + ".dll"); }
|
||||||
|
|
||||||
|
/// <summary>Eigene Mods einmal beim ersten Hauptmenü laden.</summary>
|
||||||
|
public static void LoadLocal()
|
||||||
|
{
|
||||||
|
if (localLoaded) return;
|
||||||
|
localLoaded = true;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(Folder);
|
||||||
|
foreach (string file in Directory.GetFiles(Folder, "*.dll"))
|
||||||
|
Load(file, false);
|
||||||
|
if (mods.Count > 0) MscoLog.Info("[MSCO] " + mods.Count + " mod(s) loaded from " + Folder);
|
||||||
|
}
|
||||||
|
catch (Exception e) { MscoLog.Warning("[MSCO] Could not load mods: " + e.Message); }
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string Sha256Of(byte[] data)
|
||||||
|
{
|
||||||
|
using (var sha = SHA256.Create())
|
||||||
|
return BitConverter.ToString(sha.ComputeHash(data)).Replace("-", "").ToLowerInvariant();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static LoadedMod FindBySha(string sha)
|
||||||
|
{
|
||||||
|
foreach (LoadedMod m in mods) if (string.Equals(m.Sha256, sha, StringComparison.OrdinalIgnoreCase)) return m;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// DLL laden (aus den Bytes - die Datei bleibt frei, und eine neue Version einer Server-Mod kann neben der alten
|
||||||
|
/// geladen werden) und alle MscoMod-Klassen darin anlegen. Gibt die erste Mod zurück (oder null).
|
||||||
|
/// </summary>
|
||||||
|
public static LoadedMod Load(string file, bool fromServer)
|
||||||
|
{
|
||||||
|
LoadedMod first = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
byte[] data = System.IO.File.ReadAllBytes(file);
|
||||||
|
string sha = Sha256Of(data);
|
||||||
|
LoadedMod existing = FindBySha(sha);
|
||||||
|
if (existing != null) return existing;
|
||||||
|
|
||||||
|
Assembly asm = Assembly.Load(data);
|
||||||
|
Type[] types;
|
||||||
|
try { types = asm.GetTypes(); }
|
||||||
|
catch (ReflectionTypeLoadException e) { types = Array.FindAll(e.Types, t => t != null); }
|
||||||
|
foreach (Type t in types)
|
||||||
|
{
|
||||||
|
if (t.IsAbstract || !typeof(MscoMod).IsAssignableFrom(t)) continue;
|
||||||
|
MscoMod mod;
|
||||||
|
try { mod = (MscoMod)Activator.CreateInstance(t); }
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
MscoLog.Warning("[MSCO] Mod " + t.FullName + " could not be created: " + (e.InnerException ?? e).Message);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
var loaded = new LoadedMod { Mod = mod, File = file, Sha256 = sha, FromServer = fromServer, Active = !fromServer };
|
||||||
|
mods.Add(loaded);
|
||||||
|
if (first == null) first = loaded;
|
||||||
|
MscoLog.Info("[MSCO] Mod loaded: " + Safe(() => mod.Name, t.Name) + " " + Safe(() => mod.Version, "") +
|
||||||
|
(fromServer ? " (from the server)" : ""));
|
||||||
|
Call(loaded, m => m.OnLoad());
|
||||||
|
}
|
||||||
|
if (first == null) MscoLog.Warning("[MSCO] " + Path.GetFileName(file) + " contains no MSCO mod (class deriving from MscoMod)");
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
MscoLog.Warning("[MSCO] Could not load mod " + Path.GetFileName(file) + ": " + e.Message);
|
||||||
|
}
|
||||||
|
return first;
|
||||||
|
}
|
||||||
|
|
||||||
|
static string Safe(Func<string> get, string fallback)
|
||||||
|
{
|
||||||
|
try { return get() ?? fallback; } catch { return fallback; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Server-Mods für die aktuelle Sitzung einschalten (alle anderen Server-Mods aus).</summary>
|
||||||
|
public static void ActivateServerMods(ICollection<string> shas)
|
||||||
|
{
|
||||||
|
foreach (LoadedMod m in mods)
|
||||||
|
if (m.FromServer && m.Error == null) m.Active = shas != null && shas.Contains(m.Sha256);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------- Aufrufe
|
||||||
|
|
||||||
|
static void Call(LoadedMod m, Action<MscoMod> action)
|
||||||
|
{
|
||||||
|
try { action(m.Mod); }
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
m.Errors++;
|
||||||
|
if (m.Errors <= 3) MscoLog.Warning("[MSCO] Mod " + Safe(() => m.Mod.Name, "?") + ": " + e);
|
||||||
|
if (m.Errors >= MaxErrors)
|
||||||
|
{
|
||||||
|
m.Active = false;
|
||||||
|
m.Error = e.Message;
|
||||||
|
MscoLog.Warning("[MSCO] Mod " + Safe(() => m.Mod.Name, "?") + " was turned off (too many errors)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void Each(Action<MscoMod> action)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < mods.Count; i++)
|
||||||
|
if (mods[i].Active) Call(mods[i], action);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void MenuLoad() { Each(m => m.OnMenuLoad()); }
|
||||||
|
public static void GameLoad() { Each(m => m.OnGameLoad()); }
|
||||||
|
public static void Update() { if (mods.Count > 0) Each(m => m.Update()); }
|
||||||
|
public static void FixedUpdate() { if (mods.Count > 0) Each(m => m.FixedUpdate()); }
|
||||||
|
public static void OnGUI() { if (mods.Count > 0) Each(m => m.OnGUI()); }
|
||||||
|
public static void SessionStart() { Each(m => m.OnSessionStart()); }
|
||||||
|
public static void PlayerJoined(int id, string name) { Each(m => m.OnPlayerJoined(id, name)); }
|
||||||
|
public static void PlayerLeft(int id, string name) { Each(m => m.OnPlayerLeft(id, name)); }
|
||||||
|
|
||||||
|
/// <summary>Sitzung vorbei: Mods informieren, Server-Mods ausschalten.</summary>
|
||||||
|
public static void SessionEnd()
|
||||||
|
{
|
||||||
|
Each(m => m.OnSessionEnd());
|
||||||
|
ActivateServerMods(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Message(string modId, int from, byte[] data)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < mods.Count; i++)
|
||||||
|
{
|
||||||
|
LoadedMod m = mods[i];
|
||||||
|
if (!m.Active || Safe(() => m.Mod.ID, null) != modId) continue;
|
||||||
|
Call(m, mod => mod.OnMessage(from, data));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
using MSCLoader;
|
|
||||||
|
|
||||||
namespace MSCO
|
|
||||||
{
|
|
||||||
/// <summary>MSCLoader-Konsole: "mp <befehl>" - gleiche Befehle wie in der F7-Konsole.</summary>
|
|
||||||
public class MpCommand : ConsoleCommand
|
|
||||||
{
|
|
||||||
public override string Name => "mp";
|
|
||||||
public override string Help => "MSCO commands, e.g. mp help | mp connect <host[:port]> | mp save (or press F7)";
|
|
||||||
|
|
||||||
public override void Run(string[] args)
|
|
||||||
{
|
|
||||||
Commands.Execute(args, line => ModConsole.Print("[MSCO] " + line));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+1
-1
@@ -4,7 +4,7 @@ using UnityEngine;
|
|||||||
namespace MSCO
|
namespace MSCO
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Zentrales Log der Mod: alle Meldungen landen in der MSCO-Konsole (F7), nicht in der MSCLoader-Konsole.
|
/// Zentrales Log der Mod: alle Meldungen landen in der MSCO-Konsole (F7), nicht in einer Loader-Konsole.
|
||||||
/// Zusätzlich gehen sie per Debug.Log in die output_log.txt des Spiels (zur Fehlersuche), ohne dass sich die
|
/// Zusätzlich gehen sie per Debug.Log in die output_log.txt des Spiels (zur Fehlersuche), ohne dass sich die
|
||||||
/// MSCLoader-Konsole öffnet. Thread-sicher: Meldungen aus Hintergrund-Threads werden im Hauptthread übernommen.
|
/// MSCLoader-Konsole öffnet. Thread-sicher: Meldungen aus Hintergrund-Threads werden im Hauptthread übernommen.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
+417
-41
@@ -1,7 +1,6 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using HutongGames.PlayMaker;
|
using HutongGames.PlayMaker;
|
||||||
using MSCLoader;
|
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
|
|
||||||
namespace MSCO
|
namespace MSCO
|
||||||
@@ -72,6 +71,24 @@ namespace MSCO
|
|||||||
const float NeedsInterval = 10f;
|
const float NeedsInterval = 10f;
|
||||||
|
|
||||||
void ToastJoined(string name) { toasts.Add(name, "joined the game", UiTheme.Online); }
|
void ToastJoined(string name) { toasts.Add(name, "joined the game", UiTheme.Online); }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Steam-Startoptionen mit "--doorstop-mono-debug-enabled true" (vom MSCLoader-Entwicklermodus): das ganze Spiel läuft
|
||||||
|
/// mit Mono-Debugger und spürbar langsamer. Einmal deutlich melden.
|
||||||
|
/// </summary>
|
||||||
|
void WarnDebugLaunchOptions()
|
||||||
|
{
|
||||||
|
string[] args = System.Environment.GetCommandLineArgs();
|
||||||
|
int i = System.Array.FindIndex(args, a => a.Equals("--doorstop-mono-debug-enabled", System.StringComparison.OrdinalIgnoreCase));
|
||||||
|
if (i < 0 || i + 1 >= args.Length || !(args[i + 1] == "true" || args[i + 1] == "1")) return;
|
||||||
|
MscoLog.Warning("[MSCO] The game runs with the Mono debugger (Steam launch options: --doorstop-mono-debug-enabled true) - " +
|
||||||
|
"that costs FPS. In Steam: My Summer Car -> Properties -> General -> Launch options: delete everything.");
|
||||||
|
toasts.Add("Debug mode costs FPS", "Clear the Steam launch options of My Summer Car", UiTheme.Gold);
|
||||||
|
}
|
||||||
|
|
||||||
|
const string OutdatedServerText = "This server is outdated - it is missing MSCO security updates. You can join again once its owner has updated it (web panel -> Update).";
|
||||||
|
const string OutdatedHostText = "The host uses an outdated MSCO version that is missing security updates. Ask them to update " +
|
||||||
|
"MSCO (start the game once through the MSCO Launcher), then join again.";
|
||||||
void ToastLeft(string name) { toasts.Add(name, "left the game", UiTheme.Muted); }
|
void ToastLeft(string name) { toasts.Add(name, "left the game", UiTheme.Muted); }
|
||||||
readonly Overlays overlays = new Overlays();
|
readonly Overlays overlays = new Overlays();
|
||||||
bool isAdmin; // Server hat uns als Admin gemeldet
|
bool isAdmin; // Server hat uns als Admin gemeldet
|
||||||
@@ -109,6 +126,14 @@ namespace MSCO
|
|||||||
{
|
{
|
||||||
Application.runInBackground = true;
|
Application.runInBackground = true;
|
||||||
playerName = PlayerPrefs.GetString(PrefName, System.Environment.UserName);
|
playerName = PlayerPrefs.GetString(PrefName, System.Environment.UserName);
|
||||||
|
WarnDebugLaunchOptions();
|
||||||
|
// Neue Ankündigung der MSCO-Admins, während man spielt: kurz einblenden und in die Konsole (F7)
|
||||||
|
SiteStatus.NewAnnouncement += a =>
|
||||||
|
{
|
||||||
|
toasts.Add(a.Title, a.Level == "critical" ? "Important announcement" : "MSCO announcement",
|
||||||
|
a.Level == "critical" ? UiTheme.Danger : a.Level == "warning" ? UiTheme.Gold : UiTheme.Accent);
|
||||||
|
MscoLog.Info("[MSCO] Announcement: " + a.Title + (string.IsNullOrEmpty(a.Text) ? "" : " - " + a.Text));
|
||||||
|
};
|
||||||
objects.Send = SendGame;
|
objects.Send = SendGame;
|
||||||
// Einkäufe / verbrauchte Items bei uns: allen melden
|
// Einkäufe / verbrauchte Items bei uns: allen melden
|
||||||
objects.ItemAppeared = b =>
|
objects.ItemAppeared = b =>
|
||||||
@@ -193,6 +218,7 @@ namespace MSCO
|
|||||||
objects.LocalId = localId;
|
objects.LocalId = localId;
|
||||||
AddPlayer(localId, playerName, true);
|
AddPlayer(localId, playerName, true);
|
||||||
SetStatus("Hosting on port " + port + " - waiting for players...");
|
SetStatus("Hosting on port " + port + " - waiting for players...");
|
||||||
|
ModLoader.SessionStart();
|
||||||
}
|
}
|
||||||
catch (System.Exception e)
|
catch (System.Exception e)
|
||||||
{
|
{
|
||||||
@@ -339,6 +365,7 @@ namespace MSCO
|
|||||||
void MeasureFps()
|
void MeasureFps()
|
||||||
{
|
{
|
||||||
float dt = Time.unscaledDeltaTime;
|
float dt = Time.unscaledDeltaTime;
|
||||||
|
PerfStats.Frame(dt);
|
||||||
fpsFrames++;
|
fpsFrames++;
|
||||||
fpsTimer += dt;
|
fpsTimer += dt;
|
||||||
frameMsWorstWindow = Mathf.Max(frameMsWorstWindow, dt * 1000f);
|
frameMsWorstWindow = Mathf.Max(frameMsWorstWindow, dt * 1000f);
|
||||||
@@ -366,11 +393,23 @@ namespace MSCO
|
|||||||
}
|
}
|
||||||
// grün ab 50, gelb ab 30, sonst rot
|
// grün ab 50, gelb ab 30, sonst rot
|
||||||
fpsStyle.normal.textColor = fpsValue < 0f ? UiTheme.Muted : fpsValue >= 50f ? UiTheme.Online : fpsValue >= 30f ? UiTheme.Pending : UiTheme.Danger;
|
fpsStyle.normal.textColor = fpsValue < 0f ? UiTheme.Muted : fpsValue >= 50f ? UiTheme.Online : fpsValue >= 30f ? UiTheme.Pending : UiTheme.Danger;
|
||||||
var content = new GUIContent(fpsValue < 0f ? "FPS ..." : "FPS " + Mathf.RoundToInt(fpsValue));
|
int shown = fpsValue < 0f ? -1 : Mathf.RoundToInt(fpsValue);
|
||||||
float width = fpsStyle.CalcSize(content).x;
|
if (shown != fpsShown || fpsContent.text == null)
|
||||||
GUI.Box(new Rect(rightEdge - width, 8, width, 24), content, fpsStyle);
|
{
|
||||||
|
fpsShown = shown;
|
||||||
|
fpsContent.text = shown < 0 ? "FPS ..." : "FPS " + shown;
|
||||||
|
fpsWidth = fpsStyle.CalcSize(fpsContent).x;
|
||||||
|
}
|
||||||
|
GUI.Box(new Rect(rightEdge - fpsWidth, 8, fpsWidth, 24), fpsContent, fpsStyle);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Anzeige-Texte zwischengespeichert (nur bei Änderung neu gebaut, siehe DrawFps/DrawPing)
|
||||||
|
readonly GUIContent fpsContent = new GUIContent(), pingContent = new GUIContent();
|
||||||
|
int fpsShown = int.MinValue;
|
||||||
|
float fpsWidth;
|
||||||
|
string pingShown;
|
||||||
|
float pingTextWidth;
|
||||||
|
|
||||||
/// <summary>Text für Anzeige und Konsole.</summary>
|
/// <summary>Text für Anzeige und Konsole.</summary>
|
||||||
public string PingText
|
public string PingText
|
||||||
{
|
{
|
||||||
@@ -378,7 +417,10 @@ namespace MSCO
|
|||||||
{
|
{
|
||||||
if (session == null) return "not connected";
|
if (session == null) return "not connected";
|
||||||
if (session.IsHost) return "you are the host";
|
if (session.IsHost) return "you are the host";
|
||||||
return pingMs < 0f ? "measuring..." : Mathf.RoundToInt(pingMs) + " ms";
|
if (pingMs < 0f) return "measuring...";
|
||||||
|
int ms = Mathf.RoundToInt(pingMs);
|
||||||
|
if (ms != pingTextMs || pingTextCache == null) { pingTextMs = ms; pingTextCache = ms + " ms"; }
|
||||||
|
return pingTextCache;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -396,12 +438,20 @@ namespace MSCO
|
|||||||
// Farbe nach Qualität: grün < 80 ms, gelb < 150 ms, sonst rot
|
// Farbe nach Qualität: grün < 80 ms, gelb < 150 ms, sonst rot
|
||||||
Color color = pingMs < 0f ? UiTheme.Muted : pingMs < 80f ? UiTheme.Online : pingMs < 150f ? UiTheme.Pending : UiTheme.Danger;
|
Color color = pingMs < 0f ? UiTheme.Muted : pingMs < 80f ? UiTheme.Online : pingMs < 150f ? UiTheme.Pending : UiTheme.Danger;
|
||||||
pingStyle.normal.textColor = color;
|
pingStyle.normal.textColor = color;
|
||||||
var content = new GUIContent("PING " + PingText);
|
string ping = PingText; // ändert sich nur, wenn eine neue Messung da ist
|
||||||
float width = pingStyle.CalcSize(content).x;
|
if (ping != pingShown)
|
||||||
GUI.Box(new Rect(Screen.width - width - 12, 8, width, 24), content, pingStyle);
|
{
|
||||||
|
pingShown = ping;
|
||||||
|
pingContent.text = "PING " + ping;
|
||||||
|
pingTextWidth = pingStyle.CalcSize(pingContent).x;
|
||||||
|
}
|
||||||
|
float width = pingTextWidth;
|
||||||
|
GUI.Box(new Rect(Screen.width - width - 12, 8, width, 24), pingContent, pingStyle);
|
||||||
pingWidth = width + 8;
|
pingWidth = width + 8;
|
||||||
}
|
}
|
||||||
float pingWidth; // Platz, den die Ping-Anzeige belegt (FPS steht links daneben)
|
float pingWidth; // Platz, den die Ping-Anzeige belegt (FPS steht links daneben)
|
||||||
|
int pingTextMs = int.MinValue;
|
||||||
|
string pingTextCache;
|
||||||
|
|
||||||
// ---------------------------------------------------------------- Cheats (Admin-Menü)
|
// ---------------------------------------------------------------- Cheats (Admin-Menü)
|
||||||
|
|
||||||
@@ -456,6 +506,20 @@ namespace MSCO
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Admin: Satsuma neben sich holen (z.B. wenn er verschwunden oder im Boden ist).</summary>
|
/// <summary>Admin: Satsuma neben sich holen (z.B. wenn er verschwunden oder im Boden ist).</summary>
|
||||||
|
/// <summary>Admin: sich selbst zu einem Ort teleportieren (Haus, Laden ... siehe Cheats.Places).</summary>
|
||||||
|
public void TeleportToPlace(string label, string objectName)
|
||||||
|
{
|
||||||
|
if (!CanCheat || localPlayer == null) return;
|
||||||
|
Vector3 pos;
|
||||||
|
if (!Cheats.FindPlace(objectName, out pos)) { overlays.Log(label + " not found (only works in the game)"); return; }
|
||||||
|
passenger.Exit(localPlayer, playerCamera);
|
||||||
|
if (Cheats.Teleport(localPlayer, pos))
|
||||||
|
{
|
||||||
|
overlays.Log("Teleported to " + label);
|
||||||
|
AnnounceCheat("teleported to " + label);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void BringCar()
|
public void BringCar()
|
||||||
{
|
{
|
||||||
if (!CanCheat || localPlayer == null) return;
|
if (!CanCheat || localPlayer == null) return;
|
||||||
@@ -554,6 +618,25 @@ namespace MSCO
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Cheats für alle sichtbar im Chat melden.</summary>
|
/// <summary>Cheats für alle sichtbar im Chat melden.</summary>
|
||||||
|
/// <summary>Admin-Menü (Reiter VEHICLE): Änderung am Fahrzeug im Chat melden.</summary>
|
||||||
|
public void VehicleCheat(string what)
|
||||||
|
{
|
||||||
|
if (CanCheat) AnnounceCheat(what);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Admin: Uhrzeit (kind 1, Stunde) oder Wetter (kind 2: 0 klar, 1 Regen, 2 Gewitter) für alle umstellen.</summary>
|
||||||
|
public void WorldCheat(byte kind, int value)
|
||||||
|
{
|
||||||
|
if (!CanCheat) return;
|
||||||
|
Vector3 pos = localPlayer != null ? localPlayer.position : Vector3.zero;
|
||||||
|
string error = WorldAdmin.Apply(kind, value, pos);
|
||||||
|
if (error != null) { overlays.Log(error); return; }
|
||||||
|
SendModMessage(WorldAdmin.MessageId, WorldAdmin.Build(kind, value, pos));
|
||||||
|
AnnounceCheat(kind == WorldAdmin.KindHour
|
||||||
|
? "set the time to " + value.ToString("00") + ":00"
|
||||||
|
: "set the weather to " + WorldAdmin.WeatherNames[value]);
|
||||||
|
}
|
||||||
|
|
||||||
void AnnounceCheat(string what)
|
void AnnounceCheat(string what)
|
||||||
{
|
{
|
||||||
string text = OwnName() + " " + what + " (admin)";
|
string text = OwnName() + " " + what + " (admin)";
|
||||||
@@ -612,6 +695,18 @@ namespace MSCO
|
|||||||
if (host != null) JoinServer(host, port);
|
if (host != null) JoinServer(host, port);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>JOIN-Link aus dem Browser, während das Spiel schon läuft (über den Launcher, siehe JoinLinks).</summary>
|
||||||
|
void CheckJoinRequest()
|
||||||
|
{
|
||||||
|
string host;
|
||||||
|
int port;
|
||||||
|
if (!JoinLinks.TryTakeRequest(out host, out port)) return;
|
||||||
|
PendingJoinHost = host;
|
||||||
|
PendingJoinPort = port;
|
||||||
|
if (IsInSession || !MenuHelper.InMenu)
|
||||||
|
toasts.Add("Join link: " + host + ":" + port, "leave this game - you join automatically in the main menu", UiTheme.Accent);
|
||||||
|
}
|
||||||
|
|
||||||
public void CancelPendingJoin()
|
public void CancelPendingJoin()
|
||||||
{
|
{
|
||||||
PendingJoinHost = null;
|
PendingJoinHost = null;
|
||||||
@@ -686,6 +781,13 @@ namespace MSCO
|
|||||||
}
|
}
|
||||||
string name = string.IsNullOrEmpty(displayName) ? info.Name : displayName;
|
string name = string.IsNullOrEmpty(displayName) ? info.Name : displayName;
|
||||||
int max = info.MaxPlayers > 0 ? info.MaxPlayers : maxPlayers;
|
int max = info.MaxPlayers > 0 ? info.MaxPlayers : maxPlayers;
|
||||||
|
// Fehlen dem Server Sicherheitsupdates, nicht beitreten (auch nicht nach Rückfrage)
|
||||||
|
if (info.Security < SiteStatus.RequiredServerSecurity)
|
||||||
|
{
|
||||||
|
DisconnectNotice = OutdatedServerText;
|
||||||
|
SetStatus("Server " + name + " is outdated (security level " + info.Security + ", required " + SiteStatus.RequiredServerSecurity + ")");
|
||||||
|
yield break;
|
||||||
|
}
|
||||||
|
|
||||||
if (!MSCO.Compatible(info.Version))
|
if (!MSCO.Compatible(info.Version))
|
||||||
{
|
{
|
||||||
@@ -858,6 +960,7 @@ namespace MSCO
|
|||||||
objects.LocalId = localId;
|
objects.LocalId = localId;
|
||||||
AddPlayer(localId, OwnName(), true);
|
AddPlayer(localId, OwnName(), true);
|
||||||
SetStatus("Steam lobby ready - invite your friends!");
|
SetStatus("Steam lobby ready - invite your friends!");
|
||||||
|
ModLoader.SessionStart();
|
||||||
}
|
}
|
||||||
|
|
||||||
void OnSteamLobbyEntered(Steamworks.CSteamID lobby, Steamworks.CSteamID owner)
|
void OnSteamLobbyEntered(Steamworks.CSteamID lobby, Steamworks.CSteamID owner)
|
||||||
@@ -975,6 +1078,12 @@ namespace MSCO
|
|||||||
void Update()
|
void Update()
|
||||||
{
|
{
|
||||||
CheckHitch();
|
CheckHitch();
|
||||||
|
SiteStatus.Tick(); // Ankündigungen, Wartung, Mindest-Sicherheitsstufe vom Keymaster
|
||||||
|
CheckJoinRequest();
|
||||||
|
// JOIN im Server-Browser (Launcher/Website): ohne Rückfrage verbinden, sobald das Hauptmenü steht.
|
||||||
|
// Server-Mods fragen weiterhin extra nach (das sind Programme).
|
||||||
|
if (PendingJoinHost != null && !IsInSession && !checkingServer && MenuHelper.InMenu && Time.timeSinceLevelLoad > 1f)
|
||||||
|
ConfirmPendingJoin();
|
||||||
MeasureFps();
|
MeasureFps();
|
||||||
// Menü-Buttons des Spiels nicht durch unser Fenster hindurch auslösen (z.B. Changelog)
|
// Menü-Buttons des Spiels nicht durch unser Fenster hindurch auslösen (z.B. Changelog)
|
||||||
// Solange das Multiplayer-Fenster offen ist, reagiert das Spielmenü (CONTINUE, QUIT ...) gar nicht;
|
// Solange das Multiplayer-Fenster offen ist, reagiert das Spielmenü (CONTINUE, QUIT ...) gar nicht;
|
||||||
@@ -1103,7 +1212,7 @@ namespace MSCO
|
|||||||
public string DisconnectNotice { get; private set; }
|
public string DisconnectNotice { get; private set; }
|
||||||
|
|
||||||
// ---------------------------------------------------------------- Über Neuladen hinweg merken
|
// ---------------------------------------------------------------- Über Neuladen hinweg merken
|
||||||
// Beim Wechsel ins Hauptmenü lädt MSCLoader alle Mods neu - dabei ginge der Kick-Grund bzw. "bitte neu
|
// Beim Wechsel ins Hauptmenü wird die Sitzung neu aufgebaut - dabei ginge der Kick-Grund bzw. "bitte neu
|
||||||
// verbinden" verloren, bevor man das Menü überhaupt sieht. Deshalb in den PlayerPrefs sichern.
|
// verbinden" verloren, bevor man das Menü überhaupt sieht. Deshalb in den PlayerPrefs sichern.
|
||||||
const string PrefNotice = "MSCO_PendingNotice";
|
const string PrefNotice = "MSCO_PendingNotice";
|
||||||
const string PrefReconnect = "MSCO_PendingReconnect";
|
const string PrefReconnect = "MSCO_PendingReconnect";
|
||||||
@@ -1244,6 +1353,7 @@ namespace MSCO
|
|||||||
if (r.Contains("kick")) return "KICKED";
|
if (r.Contains("kick")) return "KICKED";
|
||||||
if (r.Contains("shutting down") || r.Contains("server stopped") || r.Contains("shut down")) return "SERVER CLOSED";
|
if (r.Contains("shutting down") || r.Contains("server stopped") || r.Contains("shut down")) return "SERVER CLOSED";
|
||||||
if (r.Contains("could not reconnect")) return "RECONNECT FAILED";
|
if (r.Contains("could not reconnect")) return "RECONNECT FAILED";
|
||||||
|
if (r.Contains("outdated")) return "UPDATE REQUIRED"; // Server/Host ohne Sicherheitsupdates
|
||||||
return "DISCONNECTED";
|
return "DISCONNECTED";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1313,6 +1423,10 @@ namespace MSCO
|
|||||||
connectingToServer = false;
|
connectingToServer = false;
|
||||||
readySent = false;
|
readySent = false;
|
||||||
pendingSnapshot = null;
|
pendingSnapshot = null;
|
||||||
|
PendingMods = null;
|
||||||
|
downloadingMods.Clear();
|
||||||
|
deferredStart = null;
|
||||||
|
ModLoader.SessionEnd();
|
||||||
SteamLobby.CancelAuthTicket();
|
SteamLobby.CancelAuthTicket();
|
||||||
passenger.Exit(localPlayer, playerCamera); // Beifahrer: neben das Auto stellen
|
passenger.Exit(localPlayer, playerCamera); // Beifahrer: neben das Auto stellen
|
||||||
foreach (PlayerInfo p in players.Values) DestroyAvatar(p);
|
foreach (PlayerInfo p in players.Values) DestroyAvatar(p);
|
||||||
@@ -1339,7 +1453,7 @@ namespace MSCO
|
|||||||
if (wasClientInGame)
|
if (wasClientInGame)
|
||||||
{
|
{
|
||||||
SetStatus("Disconnected from host: " + reason);
|
SetStatus("Disconnected from host: " + reason);
|
||||||
PersistPendingState(); // MSCLoader lädt die Mods im Menü neu - vorher sichern
|
PersistPendingState(); // das Menü baut alles neu auf - vorher sichern
|
||||||
Application.LoadLevel(MenuHelper.MenuScene);
|
Application.LoadLevel(MenuHelper.MenuScene);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1353,7 +1467,7 @@ namespace MSCO
|
|||||||
|| t == MsgType.Chat || t == MsgType.VarSync || t == MsgType.SpawnItem
|
|| t == MsgType.Chat || t == MsgType.VarSync || t == MsgType.SpawnItem
|
||||||
|| t == MsgType.ItemAppeared || t == MsgType.ObjDestroy || t == MsgType.VehicleDamage
|
|| t == MsgType.ItemAppeared || t == MsgType.ObjDestroy || t == MsgType.VehicleDamage
|
||||||
|| t == MsgType.TrafficStates || t == MsgType.WorldVars || t == MsgType.HingeStates
|
|| t == MsgType.TrafficStates || t == MsgType.WorldVars || t == MsgType.HingeStates
|
||||||
|| t == MsgType.VehicleControls;
|
|| t == MsgType.VehicleControls || t == MsgType.ModMessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
void OnMessage(int peerId, Message m)
|
void OnMessage(int peerId, Message m)
|
||||||
@@ -1430,6 +1544,9 @@ namespace MSCO
|
|||||||
w.Write(false); // kein dedizierter Server
|
w.Write(false); // kein dedizierter Server
|
||||||
w.Write(list.Count);
|
w.Write(list.Count);
|
||||||
foreach (PlayerInfo p in list) { w.Write(p.Id); w.Write(p.Name); }
|
foreach (PlayerInfo p in list) { w.Write(p.Id); w.Write(p.Name); }
|
||||||
|
// Unsere Sicherheitsstufe (aktuelle Mods treten veralteten Hosts nicht bei)
|
||||||
|
w.Write(MSCO.SecurityLevel);
|
||||||
|
w.Write(MSCO.ModVersion);
|
||||||
}));
|
}));
|
||||||
session.SendToAll(MsgType.PlayerJoined, NetSession.Build(w => { w.Write(peerId); w.Write(name); }), peerId);
|
session.SendToAll(MsgType.PlayerJoined, NetSession.Build(w => { w.Write(peerId); w.Write(name); }), peerId);
|
||||||
ToastJoined(name);
|
ToastJoined(name);
|
||||||
@@ -1469,13 +1586,55 @@ namespace MSCO
|
|||||||
int id = r.ReadInt32();
|
int id = r.ReadInt32();
|
||||||
AddPlayer(id, r.ReadString(), id == localId);
|
AddPlayer(id, r.ReadString(), id == localId);
|
||||||
}
|
}
|
||||||
|
// Sicherheitsstufe des Servers/Hosts (ältere schicken sie nicht = 0): darunter gleich wieder gehen
|
||||||
|
int level = r.BaseStream.Position < r.BaseStream.Length ? r.ReadInt32() : 0;
|
||||||
|
if (level < SiteStatus.RequiredServerSecurity)
|
||||||
|
{
|
||||||
|
MscoLog.Warning("[MSCO] " + (dedicated ? "Server" : "Host") + " has security level " + level + ", required " +
|
||||||
|
SiteStatus.RequiredServerSecurity + " - leaving");
|
||||||
|
serverReason = dedicated ? OutdatedServerText : OutdatedHostText;
|
||||||
|
reconnectMessage = null; // nicht immer wieder versuchen
|
||||||
|
session.Close("Outdated " + (dedicated ? "server" : "host"));
|
||||||
|
OnSessionEnded(session.CloseReason);
|
||||||
|
return;
|
||||||
|
}
|
||||||
handshakeDone = true;
|
handshakeDone = true;
|
||||||
reconnectMessage = null; // (wieder) verbunden
|
reconnectMessage = null; // (wieder) verbunden
|
||||||
reconnectAttempts = 0;
|
reconnectAttempts = 0;
|
||||||
SetStatus(dedicated ? "Connected to server - loading..." : "Connected - waiting for host to start...");
|
SetStatus(dedicated ? "Connected to server - loading..." : "Connected - waiting for host to start...");
|
||||||
|
if (PendingMods != null) SetStatus("This server uses mods - confirm to download them");
|
||||||
|
ModLoader.SessionStart();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case MsgType.ModList:
|
||||||
|
{
|
||||||
|
var list = new List<ServerModInfo>();
|
||||||
|
int n = r.ReadInt32();
|
||||||
|
for (int i = 0; i < n && i < 64; i++)
|
||||||
|
list.Add(new ServerModInfo { Id = r.ReadString(), Name = r.ReadString(), Version = r.ReadString(), Sha256 = r.ReadString(), Size = r.ReadInt32() });
|
||||||
|
ResolveServerMods(list);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case MsgType.ModFilePart:
|
||||||
|
{
|
||||||
|
string id = r.ReadString();
|
||||||
|
int total = r.ReadInt32(), offset = r.ReadInt32(), len = r.ReadInt32();
|
||||||
|
byte[] part = r.ReadBytes(len);
|
||||||
|
ServerModInfo info = downloadingMods.Find(x => x.Id == id);
|
||||||
|
if (info == null || total != info.Size || total > MaxModBytes || offset < 0 || offset + part.Length > total) break;
|
||||||
|
if (info.Buffer == null) info.Buffer = new byte[total];
|
||||||
|
System.Buffer.BlockCopy(part, 0, info.Buffer, offset, part.Length);
|
||||||
|
info.Received += part.Length;
|
||||||
|
SetStatus("Downloading mods... " + ModDownloadPercent() + "%");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case MsgType.ModFileDone:
|
||||||
|
FinishModDownload(r.ReadString());
|
||||||
|
break;
|
||||||
|
|
||||||
case MsgType.Reject:
|
case MsgType.Reject:
|
||||||
{
|
{
|
||||||
string why = r.ReadString();
|
string why = r.ReadString();
|
||||||
@@ -1549,7 +1708,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:
|
||||||
@@ -1604,34 +1768,64 @@ namespace MSCO
|
|||||||
}
|
}
|
||||||
|
|
||||||
case MsgType.StartGame:
|
case MsgType.StartGame:
|
||||||
|
{
|
||||||
if (!receivingSave) return;
|
if (!receivingSave) return;
|
||||||
receivingSave = false;
|
receivingSave = false;
|
||||||
// Später beigetreten: nach dem Laden neben den Host setzen
|
// Später beigetreten: nach dem Laden neben den Host setzen
|
||||||
teleportToHost = r.BaseStream.Length > 0 && r.ReadBoolean();
|
teleportToHost = r.BaseStream.Length > 0 && r.ReadBoolean();
|
||||||
// Server mit "neues Spiel" (Panel): kein Spielstand - hier ein neues Spiel starten (Server ab 2.9.3)
|
// Server mit "neues Spiel" (Panel): kein Spielstand - hier ein neues Spiel starten (Server ab 2.9.3)
|
||||||
bool serverNewGame = r.BaseStream.Length - r.BaseStream.Position >= 1 && r.ReadBoolean();
|
bool serverNewGame = r.BaseStream.Length - r.BaseStream.Position >= 1 && r.ReadBoolean();
|
||||||
if (serverNewGame)
|
// Fehlen noch Mods des Servers: erst nach Zustimmung + Download ins Spiel
|
||||||
|
if (PendingMods != null || ModsDownloading)
|
||||||
{
|
{
|
||||||
SetStatus("The server starts a new game - creating a fresh world...");
|
deferredStart = serverNewGame;
|
||||||
MscoLog.Info("[MSCO] Server has a new game - starting a fresh world");
|
|
||||||
MenuHelper.StartNewGame();
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
SetStatus(dedicated ? "Loading server world..." : "Loading host's game...");
|
BeginServerGame(serverNewGame);
|
||||||
MenuHelper.StartGame();
|
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Spielstand ist da: ins Spiel (bzw. bei einem neuen Server-Spiel ein neues Spiel anlegen).</summary>
|
||||||
|
void BeginServerGame(bool serverNewGame)
|
||||||
|
{
|
||||||
|
if (serverNewGame)
|
||||||
|
{
|
||||||
|
SetStatus("The server starts a new game - creating a fresh world...");
|
||||||
|
MscoLog.Info("[MSCO] Server has a new game - starting a fresh world");
|
||||||
|
MenuHelper.StartNewGame();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
SetStatus(dedicated ? "Loading server world..." : "Loading host's game...");
|
||||||
|
MenuHelper.StartGame();
|
||||||
|
}
|
||||||
|
|
||||||
void HandleGameMessage(int from, MsgType type, BinaryReader r)
|
void HandleGameMessage(int from, MsgType type, BinaryReader r)
|
||||||
{
|
{
|
||||||
if (from == localId) return;
|
if (from == localId) return;
|
||||||
|
if (type == MsgType.ModMessage)
|
||||||
|
{
|
||||||
|
string modId = r.ReadString();
|
||||||
|
int len = r.ReadInt32();
|
||||||
|
if (len < 0 || len > MaxModMessageBytes) return;
|
||||||
|
if (modId == WorldAdmin.MessageId)
|
||||||
|
{
|
||||||
|
// Admin: Uhrzeit/Wetter. Der dedizierte Server lässt das nur von Admins durch; im P2P-Spiel nur vom Host
|
||||||
|
if (dedicated || from == NetSession.HostId) WorldAdmin.Receive(r.ReadBytes(len));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (modId.StartsWith("msco.")) return; // reserviert für MSCO selbst
|
||||||
|
ModLoader.Message(modId, from, r.ReadBytes(len));
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (type == MsgType.Chat)
|
if (type == MsgType.Chat)
|
||||||
{
|
{
|
||||||
string text = r.ReadString();
|
string text = r.ReadString();
|
||||||
PlayerInfo sender;
|
PlayerInfo sender;
|
||||||
if (dedicated && from == NetSession.HostId) overlays.AddChat("", text, true); // Server/Admin-Meldung
|
if (dedicated && from == NetSession.HostId) overlays.AddChat("", text, true); // Server/Admin-Meldung
|
||||||
else if (text.StartsWith("* ")) overlays.AddChat("", text.Substring(2), true);
|
// "* ..." = Meldung (z.B. Admin-Aktion): auf dem dedizierten Server filtert der Server, im P2P-Spiel nur vom Host
|
||||||
|
else if (text.StartsWith("* ") && (dedicated || from == NetSession.HostId)) overlays.AddChat("", text.Substring(2), true);
|
||||||
else overlays.AddChat(players.TryGetValue(from, out sender) ? sender.Name : "Player " + from, text);
|
else overlays.AddChat(players.TryGetValue(from, out sender) ? sender.Name : "Player " + from, text);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1698,6 +1892,117 @@ namespace MSCO
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Spiel-Nachricht an alle anderen (Client: über den Host).</summary>
|
/// <summary>Spiel-Nachricht an alle anderen (Client: über den Host).</summary>
|
||||||
|
// ---------------------------------------------------------------- MSCO-Mods
|
||||||
|
|
||||||
|
public sealed class ServerModInfo
|
||||||
|
{
|
||||||
|
public string Id, Name, Version, Sha256;
|
||||||
|
public int Size, Received;
|
||||||
|
public byte[] Buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
const int MaxModBytes = 20 * 1024 * 1024, MaxModMessageBytes = 60000;
|
||||||
|
/// <summary>Mods, die der Server verlangt und die hier fehlen - warten auf Zustimmung des Spielers (null = keine).</summary>
|
||||||
|
public List<ServerModInfo> PendingMods { get; private set; }
|
||||||
|
readonly List<ServerModInfo> downloadingMods = new List<ServerModInfo>();
|
||||||
|
readonly List<string> serverModShas = new List<string>();
|
||||||
|
bool? deferredStart;
|
||||||
|
public bool ModsDownloading { get { return downloadingMods.Count > 0; } }
|
||||||
|
|
||||||
|
int ModDownloadPercent()
|
||||||
|
{
|
||||||
|
long total = 0, done = 0;
|
||||||
|
foreach (ServerModInfo m in downloadingMods) { total += m.Size; done += m.Received; }
|
||||||
|
return total > 0 ? (int)(done * 100 / total) : 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Liste vom Server: vorhandene Mods (gleiche Prüfsumme) einschalten, fehlende zum Bestätigen merken.</summary>
|
||||||
|
void ResolveServerMods(List<ServerModInfo> list)
|
||||||
|
{
|
||||||
|
serverModShas.Clear();
|
||||||
|
var missing = new List<ServerModInfo>();
|
||||||
|
foreach (ServerModInfo m in list)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(m.Id) || m.Id.IndexOfAny(Path.GetInvalidFileNameChars()) >= 0 || m.Id.Contains("..")) continue;
|
||||||
|
serverModShas.Add(m.Sha256);
|
||||||
|
if (ModLoader.FindBySha(m.Sha256) != null) continue;
|
||||||
|
// Schon einmal geladen (gleiche Datei auf der Platte)?
|
||||||
|
string file = ModLoader.ServerFile(m.Id);
|
||||||
|
if (File.Exists(file) && ModLoader.Sha256Of(File.ReadAllBytes(file)) == m.Sha256 && ModLoader.Load(file, true) != null) continue;
|
||||||
|
missing.Add(m);
|
||||||
|
}
|
||||||
|
ModLoader.ActivateServerMods(serverModShas);
|
||||||
|
PendingMods = missing.Count > 0 ? missing : null;
|
||||||
|
if (PendingMods != null) MscoLog.Info("[MSCO] The server uses " + list.Count + " mod(s), " + missing.Count + " missing");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Spieler stimmt zu: fehlende Mods vom Server laden.</summary>
|
||||||
|
public void AcceptServerMods()
|
||||||
|
{
|
||||||
|
if (PendingMods == null || session == null) return;
|
||||||
|
downloadingMods.Clear();
|
||||||
|
downloadingMods.AddRange(PendingMods);
|
||||||
|
PendingMods = null;
|
||||||
|
foreach (ServerModInfo m in downloadingMods)
|
||||||
|
{
|
||||||
|
string id = m.Id;
|
||||||
|
session.SendTo(NetSession.HostId, MsgType.ModFileRequest, NetSession.Build(w => w.Write(id)));
|
||||||
|
}
|
||||||
|
SetStatus("Downloading mods... 0%");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Spieler lehnt ab: ohne die Mods geht es auf diesem Server nicht.</summary>
|
||||||
|
public void DeclineServerMods()
|
||||||
|
{
|
||||||
|
PendingMods = null;
|
||||||
|
serverReason = "This server needs its mods - you declined to install them.";
|
||||||
|
reconnectMessage = null;
|
||||||
|
if (session != null) session.Close("Declined the server's mods");
|
||||||
|
OnSessionEnded("Declined the server's mods");
|
||||||
|
}
|
||||||
|
|
||||||
|
void FinishModDownload(string id)
|
||||||
|
{
|
||||||
|
ServerModInfo m = downloadingMods.Find(x => x.Id == id);
|
||||||
|
if (m == null) return;
|
||||||
|
downloadingMods.Remove(m);
|
||||||
|
if (m.Buffer == null || m.Received < m.Size || ModLoader.Sha256Of(m.Buffer) != m.Sha256)
|
||||||
|
{
|
||||||
|
MscoLog.Warning("[MSCO] Mod " + m.Name + " arrived damaged (checksum mismatch) - not loaded");
|
||||||
|
serverReason = "The mod " + m.Name + " could not be downloaded correctly - try again.";
|
||||||
|
if (session != null) session.Close("Mod download failed");
|
||||||
|
OnSessionEnded("Mod download failed");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(ModLoader.ServerFolder);
|
||||||
|
File.WriteAllBytes(ModLoader.ServerFile(m.Id), m.Buffer);
|
||||||
|
}
|
||||||
|
catch (System.Exception e) { MscoLog.Warning("[MSCO] Could not save mod " + m.Name + ": " + e.Message); }
|
||||||
|
if (ModLoader.Load(ModLoader.ServerFile(m.Id), true) == null)
|
||||||
|
MscoLog.Warning("[MSCO] Mod " + m.Name + " could not be started - see the log");
|
||||||
|
ModLoader.ActivateServerMods(serverModShas);
|
||||||
|
if (MenuHelper.InMenu) ModLoader.MenuLoad();
|
||||||
|
if (downloadingMods.Count > 0) return;
|
||||||
|
|
||||||
|
SetStatus("Mods installed - loading the world...");
|
||||||
|
if (deferredStart != null)
|
||||||
|
{
|
||||||
|
bool newGame = deferredStart.Value;
|
||||||
|
deferredStart = null;
|
||||||
|
BeginServerGame(newGame);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Für MscoMod.Send: Daten einer Mod an dieselbe Mod bei allen anderen Spielern.</summary>
|
||||||
|
public bool SendModMessage(string modId, byte[] data)
|
||||||
|
{
|
||||||
|
if (session == null || !handshakeDone || string.IsNullOrEmpty(modId) || data == null || data.Length > MaxModMessageBytes) return false;
|
||||||
|
SendGame(MsgType.ModMessage, NetSession.Build(w => { w.Write(modId); w.Write(data.Length); w.Write(data); }));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
void SendGame(MsgType type, byte[] payload)
|
void SendGame(MsgType type, byte[] payload)
|
||||||
{
|
{
|
||||||
if (session == null) return;
|
if (session == null) return;
|
||||||
@@ -1710,9 +2015,11 @@ namespace MSCO
|
|||||||
PlayerInfo AddPlayer(int id, string name, bool isLocal)
|
PlayerInfo AddPlayer(int id, string name, bool isLocal)
|
||||||
{
|
{
|
||||||
PlayerInfo p;
|
PlayerInfo p;
|
||||||
if (!players.TryGetValue(id, out p)) players[id] = p = new PlayerInfo { Id = id };
|
bool isNew = !players.TryGetValue(id, out p);
|
||||||
|
if (isNew) players[id] = p = new PlayerInfo { Id = id };
|
||||||
p.Name = name;
|
p.Name = name;
|
||||||
p.IsLocal = isLocal;
|
p.IsLocal = isLocal;
|
||||||
|
if (isNew && !isLocal) ModLoader.PlayerJoined(id, name);
|
||||||
return p;
|
return p;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1720,6 +2027,7 @@ namespace MSCO
|
|||||||
{
|
{
|
||||||
PlayerInfo p;
|
PlayerInfo p;
|
||||||
if (!players.TryGetValue(id, out p)) return;
|
if (!players.TryGetValue(id, out p)) return;
|
||||||
|
if (!p.IsLocal) ModLoader.PlayerLeft(id, p.Name);
|
||||||
DestroyAvatar(p);
|
DestroyAvatar(p);
|
||||||
objects.ReleaseAllOf(id);
|
objects.ReleaseAllOf(id);
|
||||||
players.Remove(id);
|
players.Remove(id);
|
||||||
@@ -1794,6 +2102,7 @@ namespace MSCO
|
|||||||
|
|
||||||
void OnLevelWasLoaded(int level)
|
void OnLevelWasLoaded(int level)
|
||||||
{
|
{
|
||||||
|
MenuHelper.SceneChanged();
|
||||||
menuButtonsHidden = false; // neues Hauptmenü: Knöpfe wieder ausblenden
|
menuButtonsHidden = false; // neues Hauptmenü: Knöpfe wieder ausblenden
|
||||||
nextPresenceUpdate = 0f; // Discord gleich nach dem Szenenwechsel aktualisieren
|
nextPresenceUpdate = 0f; // Discord gleich nach dem Szenenwechsel aktualisieren
|
||||||
if (MenuHelper.InGame)
|
if (MenuHelper.InGame)
|
||||||
@@ -1934,12 +2243,12 @@ namespace MSCO
|
|||||||
PartStart();
|
PartStart();
|
||||||
try { objects.Update(localPlayer, Time.deltaTime); }
|
try { objects.Update(localPlayer, Time.deltaTime); }
|
||||||
catch (System.Exception e) { ReportError("object sync", e); }
|
catch (System.Exception e) { ReportError("object sync", e); }
|
||||||
PartStart();
|
|
||||||
PartEnd("object sync");
|
PartEnd("object sync");
|
||||||
|
PartStart();
|
||||||
try { vehicleFx.Update(objects, SendGame, Time.deltaTime, IsWorldAuthority); }
|
try { vehicleFx.Update(objects, SendGame, Time.deltaTime, IsWorldAuthority); }
|
||||||
catch (System.Exception e) { ReportError("vehicle lights/sound", e); }
|
catch (System.Exception e) { ReportError("vehicle lights/sound", e); }
|
||||||
PartStart();
|
|
||||||
PartEnd("vehicle lights/sound");
|
PartEnd("vehicle lights/sound");
|
||||||
|
PartStart();
|
||||||
try { worldSync.Update(IsWorldAuthority, SendGame, Time.unscaledDeltaTime); }
|
try { worldSync.Update(IsWorldAuthority, SendGame, Time.unscaledDeltaTime); }
|
||||||
catch (System.Exception e) { ReportError("time/weather sync", e); }
|
catch (System.Exception e) { ReportError("time/weather sync", e); }
|
||||||
PartEnd("time/weather sync");
|
PartEnd("time/weather sync");
|
||||||
@@ -1962,7 +2271,7 @@ namespace MSCO
|
|||||||
|
|
||||||
// Gesamtzustand der Fahrzeuge (Tank, Öl, Verschleiß ...)
|
// Gesamtzustand der Fahrzeuge (Tank, Öl, Verschleiß ...)
|
||||||
PartStart();
|
PartStart();
|
||||||
try { if (objects.ScanComplete) vehicleState.Update(objects, localId, SendGame); }
|
try { if (objects.ScanComplete) vehicleState.Update(objects, localId, SendGame, IsWorldAuthority); }
|
||||||
catch (System.Exception e) { ReportError("vehicle state sync", e); }
|
catch (System.Exception e) { ReportError("vehicle state sync", e); }
|
||||||
PartEnd("vehicle state sync");
|
PartEnd("vehicle state sync");
|
||||||
// Unfallschäden (verbeulte Karosserie)
|
// Unfallschäden (verbeulte Karosserie)
|
||||||
@@ -2024,6 +2333,7 @@ namespace MSCO
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Abgefangene Schraub-/Montage-Aktionen verschicken
|
// Abgefangene Schraub-/Montage-Aktionen verschicken
|
||||||
|
PartStart();
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
FsmSync.Enabled = ready;
|
FsmSync.Enabled = ready;
|
||||||
@@ -2054,23 +2364,39 @@ namespace MSCO
|
|||||||
{
|
{
|
||||||
double ms = (partWatch.ElapsedTicks - partStart) * 1000.0 / System.Diagnostics.Stopwatch.Frequency;
|
double ms = (partWatch.ElapsedTicks - partStart) * 1000.0 / System.Diagnostics.Stopwatch.Frequency;
|
||||||
frameMsco += ms;
|
frameMsco += ms;
|
||||||
|
PerfStats.Add(name, ms);
|
||||||
if (ms > slowestMs) { slowestMs = ms; slowestPart = name; }
|
if (ms > slowestMs) { slowestMs = ms; slowestPart = name; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <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;
|
||||||
@@ -2143,7 +2469,7 @@ namespace MSCO
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
System.Collections.IEnumerator ApplyWorldSnapshot(byte[] snapshot)
|
System.Collections.IEnumerator ApplyWorldSnapshot(byte[] snapshot)
|
||||||
{
|
{
|
||||||
var r = new BinaryReader(new MemoryStream(snapshot));
|
var r = new PacketReader(snapshot);
|
||||||
|
|
||||||
if (r.ReadBoolean())
|
if (r.ReadBoolean())
|
||||||
{
|
{
|
||||||
@@ -2176,7 +2502,7 @@ namespace MSCO
|
|||||||
for (int i = 0; i < eventCount; i++)
|
for (int i = 0; i < eventCount; i++)
|
||||||
{
|
{
|
||||||
byte[] payload = r.ReadBytes(r.ReadInt32());
|
byte[] payload = r.ReadBytes(r.ReadInt32());
|
||||||
FsmSync.Apply(FsmEventMsg.Deserialize(new BinaryReader(new MemoryStream(payload))));
|
FsmSync.Apply(FsmEventMsg.Deserialize(new PacketReader(payload)));
|
||||||
if ((i + 1) % SnapshotEventsPerFrame == 0) yield return null; // nicht alles in einem Frame
|
if ((i + 1) % SnapshotEventsPerFrame == 0) yield return null; // nicht alles in einem Frame
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2201,7 +2527,7 @@ namespace MSCO
|
|||||||
for (int i = 0; i < spawns; i++)
|
for (int i = 0; i < spawns; i++)
|
||||||
{
|
{
|
||||||
byte[] payload = r.ReadBytes(r.ReadInt32());
|
byte[] payload = r.ReadBytes(r.ReadInt32());
|
||||||
try { if (ApplySpawn(new BinaryReader(new MemoryStream(payload)))) spawned++; }
|
try { if (ApplySpawn(new PacketReader(payload))) spawned++; }
|
||||||
catch (System.Exception e) { MscoLog.Warning("Could not spawn a saved item: " + e.Message); }
|
catch (System.Exception e) { MscoLog.Warning("Could not spawn a saved item: " + e.Message); }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2214,7 +2540,7 @@ namespace MSCO
|
|||||||
for (int i = 0; i < appeared; i++)
|
for (int i = 0; i < appeared; i++)
|
||||||
{
|
{
|
||||||
byte[] payload = r.ReadBytes(r.ReadInt32());
|
byte[] payload = r.ReadBytes(r.ReadInt32());
|
||||||
try { ApplyAppeared(new BinaryReader(new MemoryStream(payload))); }
|
try { ApplyAppeared(new PacketReader(payload)); }
|
||||||
catch (System.Exception e) { MscoLog.Warning("Could not restore a bought item: " + e.Message); }
|
catch (System.Exception e) { MscoLog.Warning("Could not restore a bought item: " + e.Message); }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2258,9 +2584,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
|
||||||
@@ -2396,6 +2743,24 @@ namespace MSCO
|
|||||||
// ---------------------------------------------------------------- GUI
|
// ---------------------------------------------------------------- GUI
|
||||||
|
|
||||||
void OnGUI()
|
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
|
||||||
|
{
|
||||||
|
skin.font = previousFont;
|
||||||
|
PartEnd("interface (OnGUI)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
string centerMessageSource, centerMessageUpper; // Großschreibung nur einmal pro Meldung (nicht jedes Frame)
|
||||||
|
|
||||||
|
void DrawGui()
|
||||||
{
|
{
|
||||||
InputLock.KeepCursorFree();
|
InputLock.KeepCursorFree();
|
||||||
if (MenuHelper.InMenu)
|
if (MenuHelper.InMenu)
|
||||||
@@ -2413,7 +2778,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2422,7 +2788,7 @@ namespace MSCO
|
|||||||
overlays.Draw(this, session != null);
|
overlays.Draw(this, session != null);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>F7 Konsole, T Chat (Tasten in den MSCLoader-Einstellungen änderbar).</summary>
|
/// <summary>F7 Konsole, T Chat (Tasten mit "bind" in der Konsole änderbar).</summary>
|
||||||
void UpdateOverlayKeys()
|
void UpdateOverlayKeys()
|
||||||
{
|
{
|
||||||
if (MSCO.ConsoleKey != null && MSCO.ConsoleKey.GetKeybindDown()) overlays.ToggleConsole();
|
if (MSCO.ConsoleKey != null && MSCO.ConsoleKey.GetKeybindDown()) overlays.ToggleConsole();
|
||||||
@@ -2433,12 +2799,22 @@ namespace MSCO
|
|||||||
InputLock.Set(overlays.AnyOpen && MenuHelper.InGame);
|
InputLock.Set(overlays.AnyOpen && MenuHelper.InGame);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
string hudText;
|
||||||
|
int hudTextKey;
|
||||||
|
|
||||||
void DrawHud()
|
void DrawHud()
|
||||||
{
|
{
|
||||||
// Statuszeile oben in der Mitte (links oben ist die Spielanzeige)
|
// Statuszeile oben in der Mitte (links oben ist die Spielanzeige)
|
||||||
string state = IsFullyConnected ? "CONNECTED" : IsPeerReady ? "SYNCING..." : "WAITING";
|
string state = IsFullyConnected ? "CONNECTED" : IsPeerReady ? "SYNCING..." : "WAITING";
|
||||||
string text = "MSCO • " + state + " • " + (dedicated ? "Server" : IsHost ? "Host" : "Client") + " • " +
|
// Text nur neu bauen, wenn sich etwas geändert hat (nicht jedes Frame neue Strings)
|
||||||
|
int key = (state.Length * 31 + players.Count) * 100003 + objects.Count + (dedicated ? 7 : IsHost ? 3 : 1) * 1000000007;
|
||||||
|
if (hudText == null || key != hudTextKey)
|
||||||
|
{
|
||||||
|
hudTextKey = key;
|
||||||
|
hudText = "MSCO BETA • " + state + " • " + (dedicated ? "Server" : IsHost ? "Host" : "Client") + " • " +
|
||||||
players.Count + " player(s) • " + objects.Count + " synced objects";
|
players.Count + " player(s) • " + objects.Count + " synced objects";
|
||||||
|
}
|
||||||
|
string text = hudText;
|
||||||
var rect = new Rect((Screen.width - 520) / 2f, 6, 520, 24);
|
var rect = new Rect((Screen.width - 520) / 2f, 6, 520, 24);
|
||||||
if (hudBox == null) hudBox = UiTheme.Panel9(new Color(0.06f, 0.06f, 0.07f, 0.85f), 8);
|
if (hudBox == null) hudBox = UiTheme.Panel9(new Color(0.06f, 0.06f, 0.07f, 0.85f), 8);
|
||||||
GUI.Box(rect, GUIContent.none, hudBox);
|
GUI.Box(rect, GUIContent.none, hudBox);
|
||||||
|
|||||||
+124
-25
@@ -52,7 +52,12 @@ namespace MSCO
|
|||||||
WorldVars = 41, // Jobs/Laden: int n, je [int Schlüssel, byte Typ (2 int, 3 bool), Wert]
|
WorldVars = 41, // Jobs/Laden: int n, je [int Schlüssel, byte Typ (2 int, 3 bool), Wert]
|
||||||
HingeStates = 42, // Autotüren/Haube/Kofferraum: int n, je [int Schlüssel, float Scharnier-Winkel]
|
HingeStates = 42, // Autotüren/Haube/Kofferraum: int n, je [int Schlüssel, float Scharnier-Winkel]
|
||||||
VehicleControls = 43, // Zündung/Motor/Licht ...: int n, je [int FSM-Schlüssel, string Zustand]
|
VehicleControls = 43, // Zündung/Motor/Licht ...: int n, je [int FSM-Schlüssel, string Zustand]
|
||||||
PlayerNeeds = 44 // Client -> Server: eigene Bedürfnisse (Hunger, Durst ...): int n, je [string Name, float Wert]
|
PlayerNeeds = 44, // Client -> Server: eigene Bedürfnisse (Hunger, Durst ...): int n, je [string Name, float Wert]
|
||||||
|
ModMessage = 45, // Nachricht einer MSCO-Mod an die anderen Spieler: string Mod-ID, int Länge, bytes
|
||||||
|
ModList = 46, // Server -> Client (vor Welcome): int n, je [string ID, string Name, string Version, string SHA-256, int Größe]
|
||||||
|
ModFileRequest = 47, // Client -> Server: string Mod-ID
|
||||||
|
ModFilePart = 48, // Server -> Client: string Mod-ID, int Gesamtgröße, int Offset, int Länge, bytes
|
||||||
|
ModFileDone = 49 // Server -> Client: string Mod-ID
|
||||||
}
|
}
|
||||||
|
|
||||||
public struct Message
|
public struct Message
|
||||||
@@ -62,7 +67,7 @@ namespace MSCO
|
|||||||
|
|
||||||
public BinaryReader Reader()
|
public BinaryReader Reader()
|
||||||
{
|
{
|
||||||
return new BinaryReader(new MemoryStream(Payload));
|
return new PacketReader(Payload);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -195,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;
|
||||||
@@ -210,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)
|
||||||
{
|
{
|
||||||
@@ -227,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
|
||||||
{
|
{
|
||||||
@@ -258,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 { }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -308,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); }
|
||||||
@@ -414,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
|
||||||
{
|
{
|
||||||
@@ -563,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>
|
||||||
|
|||||||
+11
-4
@@ -3,7 +3,6 @@ using System.Collections.Generic;
|
|||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using HutongGames.PlayMaker;
|
using HutongGames.PlayMaker;
|
||||||
using MSCLoader;
|
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
|
|
||||||
namespace MSCO
|
namespace MSCO
|
||||||
@@ -812,15 +811,23 @@ namespace MSCO
|
|||||||
return attached;
|
return attached;
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool IsAttached(SyncedBody b)
|
bool IsAttached(SyncedBody b)
|
||||||
{
|
{
|
||||||
if (b.IsVehicle || b.Rb == null) return false;
|
if (b.IsVehicle || b.Rb == null) return false;
|
||||||
// Unter einem anderen Rigidbody (z.B. am Auto eingehängt)
|
// Unter einem anderen Rigidbody (z.B. am Auto eingehängt)
|
||||||
for (Transform p = b.Rb.transform.parent; p != null; p = p.parent)
|
for (Transform p = b.Rb.transform.parent; p != null; p = p.parent)
|
||||||
if (p.GetComponent<Rigidbody>() != null) return true;
|
if (p.GetComponent<Rigidbody>() != null) return true;
|
||||||
// Per Gelenk mit etwas verbunden, das nicht der Spieler ist (die Hand hält Items nicht über Gelenke fest)
|
// Per Gelenk mit etwas verbunden, das nicht der Spieler ist (die Hand hält Items nicht über Gelenke fest).
|
||||||
|
// Läuft alle 0,5 s für jedes Teil: erst billig prüfen, ob es überhaupt ein Gelenk hat (GetComponents legt
|
||||||
|
// jedes Mal ein neues Array an - die allermeisten Teile haben keins).
|
||||||
|
if (b.Rb.GetComponent<Joint>() == null) return false;
|
||||||
|
Transform playerRoot = Head != null ? Head.root : null;
|
||||||
foreach (Joint j in b.Rb.GetComponents<Joint>())
|
foreach (Joint j in b.Rb.GetComponents<Joint>())
|
||||||
if (j != null && j.connectedBody != null && j.connectedBody.transform.root.name != "PLAYER") return true;
|
{
|
||||||
|
if (j == null || j.connectedBody == null) continue;
|
||||||
|
Transform root = j.connectedBody.transform.root;
|
||||||
|
if (playerRoot != null ? root != playerRoot : root.name != "PLAYER") return true;
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+117
@@ -0,0 +1,117 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
|
using HutongGames.PlayMaker;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace MSCO
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Konsole "oilcheck": woher läuft Öl aus? Das Spiel kennt fünf Quellen - die Motor-FSM rechnet LeakOilpan,
|
||||||
|
/// LeakOilfilter, LeakHeadgasket und LeakRockerCover aus den Teilen aus, dazu die Ablassschraube der Ölwanne
|
||||||
|
/// (Stufe 0-5 = offen, Objekt "Oil" aktiv). Der Ölfilter ist ein gekauftes Teil und steht in items.txt
|
||||||
|
/// (Installed/Tightness) - fehlt die Datei im Spielstand eines Servers, fehlt auch der Filter.
|
||||||
|
/// </summary>
|
||||||
|
public static class OilCheck
|
||||||
|
{
|
||||||
|
static readonly string[] PartWords = { "oilpan", "oilfilter", "oil filter", "headgasket", "rocker", "valvecover", "drain" };
|
||||||
|
|
||||||
|
public static List<string> Report()
|
||||||
|
{
|
||||||
|
var lines = new List<string>();
|
||||||
|
if (!MenuHelper.InGame) { lines.Add("Only in the game."); return lines; }
|
||||||
|
|
||||||
|
var fsms = Object.FindObjectsOfType<PlayMakerFSM>();
|
||||||
|
// 1. Leck-Werte der Motor-FSM
|
||||||
|
bool leakFsm = false;
|
||||||
|
foreach (PlayMakerFSM f in fsms)
|
||||||
|
{
|
||||||
|
if (f == null || f.FsmVariables.FindFsmFloat("LeakOilfilter") == null) continue;
|
||||||
|
leakFsm = true;
|
||||||
|
var parts = new List<string>();
|
||||||
|
foreach (FsmFloat v in f.FsmVariables.FloatVariables)
|
||||||
|
if (v != null && v.Name.StartsWith("Leak")) parts.Add(v.Name + "=" + Num(v.Value));
|
||||||
|
lines.Add("Engine leaks (" + f.gameObject.name + "/" + f.FsmName + ", state " + f.ActiveStateName + "): " + string.Join(" ", parts.ToArray()));
|
||||||
|
}
|
||||||
|
if (!leakFsm) lines.Add("Engine leak values: not found (engine not assembled?)");
|
||||||
|
|
||||||
|
// 2. Teile in der Database (eingebaut, festgeschraubt, kaputt, Ölstand)
|
||||||
|
foreach (PlayMakerFSM f in Resources.FindObjectsOfTypeAll<PlayMakerFSM>())
|
||||||
|
{
|
||||||
|
if (f == null || f.FsmName != "Data" || f.gameObject.hideFlags != HideFlags.None) continue;
|
||||||
|
if (f.transform.root.name != "Database" || !Matches(f.gameObject.name)) continue;
|
||||||
|
lines.Add(" " + f.gameObject.name + ": " + Values(f.FsmVariables, "Installed", "Bolted", "Damaged", "Tightness", "Oil", "Wear"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Schrauben der Ölwanne (die Ablassschraube schaltet das Leck "Oil")
|
||||||
|
foreach (PlayMakerFSM f in fsms)
|
||||||
|
{
|
||||||
|
if (f == null || f.FsmName != "Screw") continue;
|
||||||
|
string path = FsmSync.PathOf(f.transform);
|
||||||
|
if (path.ToLower().IndexOf("oilpan", System.StringComparison.Ordinal) < 0) continue;
|
||||||
|
FsmInt stage = f.FsmVariables.FindFsmInt("Stage");
|
||||||
|
lines.Add(" bolt " + Short(path) + ": stage " + (stage != null ? stage.Value.ToString() : "?") + (Switches(f) ? " (drain plug - 0-5 = open)" : ""));
|
||||||
|
}
|
||||||
|
foreach (GameObject go in Object.FindObjectsOfType<GameObject>())
|
||||||
|
if (go.name == "Oil" && go.transform.parent != null && FsmSync.PathOf(go.transform).ToLower().Contains("oilpan"))
|
||||||
|
lines.Add(" drain leak object " + Short(FsmSync.PathOf(go.transform)) + " is ACTIVE (oil runs out of the drain plug)");
|
||||||
|
|
||||||
|
// 4. Ölfilter als Gegenstand (items.txt)
|
||||||
|
int filters = 0;
|
||||||
|
foreach (PlayMakerFSM f in fsms)
|
||||||
|
{
|
||||||
|
if (f == null || !f.gameObject.name.StartsWith("oil filter")) continue;
|
||||||
|
if (f.FsmVariables.FindFsmBool("Installed") == null && f.FsmVariables.FindFsmFloat("Tightness") == null) continue;
|
||||||
|
filters++;
|
||||||
|
lines.Add(" item " + f.gameObject.name + " (" + f.FsmName + ", under " + f.transform.root.name + "): "
|
||||||
|
+ Values(f.FsmVariables, "Installed", "Tightness", "Consumed", "Dirt"));
|
||||||
|
}
|
||||||
|
if (filters == 0) lines.Add(" no oil filter item in the world - if the server's save has no items.txt, bought parts are missing");
|
||||||
|
return lines;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool Matches(string name)
|
||||||
|
{
|
||||||
|
string n = name.ToLower();
|
||||||
|
foreach (string w in PartWords) if (n.Contains(w)) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool Switches(PlayMakerFSM f)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
foreach (FsmState s in f.FsmStates)
|
||||||
|
{
|
||||||
|
if (!s.ActionsLoaded) s.LoadActions();
|
||||||
|
if (s.Actions == null) continue;
|
||||||
|
foreach (FsmStateAction a in s.Actions) if (a != null && a.GetType().Name == "ActivateGameObject") return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static string Values(FsmVariables vars, params string[] names)
|
||||||
|
{
|
||||||
|
var parts = new List<string>();
|
||||||
|
foreach (string n in names)
|
||||||
|
{
|
||||||
|
FsmBool b = vars.FindFsmBool(n);
|
||||||
|
if (b != null) { parts.Add(n + "=" + b.Value); continue; }
|
||||||
|
FsmFloat f = vars.FindFsmFloat(n);
|
||||||
|
if (f != null) { parts.Add(n + "=" + Num(f.Value)); continue; }
|
||||||
|
FsmInt i = vars.FindFsmInt(n);
|
||||||
|
if (i != null) parts.Add(n + "=" + i.Value);
|
||||||
|
}
|
||||||
|
return parts.Count == 0 ? "-" : string.Join(" ", parts.ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
static string Num(float v) { return v.ToString("0.###", CultureInfo.InvariantCulture); }
|
||||||
|
|
||||||
|
static string Short(string path)
|
||||||
|
{
|
||||||
|
string[] s = path.Split('|');
|
||||||
|
return s.Length <= 3 ? path : "…|" + string.Join("|", s, s.Length - 3, 3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+377
-112
@@ -30,8 +30,13 @@ namespace MSCO
|
|||||||
public string Name, Text;
|
public string Name, Text;
|
||||||
public bool System;
|
public bool System;
|
||||||
public float Time;
|
public float Time;
|
||||||
|
// "Name:" und seine Breite - einmal berechnet statt bei jedem OnGUI-Durchlauf (2-3x pro Bild)
|
||||||
|
public string NameLabel;
|
||||||
|
public float NameWidth = -1f;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
readonly List<ChatLine> visibleChat = new List<ChatLine>();
|
||||||
|
|
||||||
// Konsole
|
// Konsole
|
||||||
public bool ConsoleOpen { get; private set; }
|
public bool ConsoleOpen { get; private set; }
|
||||||
readonly List<string> consoleLines = new List<string>();
|
readonly List<string> consoleLines = new List<string>();
|
||||||
@@ -71,6 +76,7 @@ namespace MSCO
|
|||||||
if (ConsoleOpen)
|
if (ConsoleOpen)
|
||||||
{
|
{
|
||||||
ChatOpen = false;
|
ChatOpen = false;
|
||||||
|
AdminOpen = false; // sonst liegt die Konsole über dem Admin-Menü und schluckt dessen Klicks
|
||||||
consoleOpenedFrame = Time.frameCount;
|
consoleOpenedFrame = Time.frameCount;
|
||||||
consoleScrollToEnd = true;
|
consoleScrollToEnd = true;
|
||||||
}
|
}
|
||||||
@@ -210,7 +216,8 @@ namespace MSCO
|
|||||||
int maxLines = ChatOpen ? 12 : 6;
|
int maxLines = ChatOpen ? 12 : 6;
|
||||||
|
|
||||||
// Sichtbare Zeilen: offen = die letzten, geschlossen = nur neue (blenden aus)
|
// Sichtbare Zeilen: offen = die letzten, geschlossen = nur neue (blenden aus)
|
||||||
var visible = new List<ChatLine>();
|
var visible = visibleChat;
|
||||||
|
visible.Clear();
|
||||||
for (int i = chat.Count - 1; i >= 0 && visible.Count < maxLines; i--)
|
for (int i = chat.Count - 1; i >= 0 && visible.Count < maxLines; i--)
|
||||||
if (ChatOpen || now - chat[i].Time < ChatFadeSeconds) visible.Insert(0, chat[i]);
|
if (ChatOpen || now - chat[i].Time < ChatFadeSeconds) visible.Insert(0, chat[i]);
|
||||||
|
|
||||||
@@ -232,9 +239,14 @@ namespace MSCO
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
Vector2 size = chatName.CalcSize(new GUIContent(line.Name + ":"));
|
if (line.NameWidth < 0f)
|
||||||
GUI.Label(new Rect(rect.x + 12, y, size.x, lineHeight), line.Name + ":", chatName);
|
{
|
||||||
GUI.Label(new Rect(rect.x + 16 + size.x, y, width - 28 - size.x, lineHeight), line.Text, chatText);
|
line.NameLabel = line.Name + ":";
|
||||||
|
line.NameWidth = chatName.CalcSize(new GUIContent(line.NameLabel)).x;
|
||||||
|
}
|
||||||
|
float nameWidth = line.NameWidth;
|
||||||
|
GUI.Label(new Rect(rect.x + 12, y, nameWidth, lineHeight), line.NameLabel, chatName);
|
||||||
|
GUI.Label(new Rect(rect.x + 16 + nameWidth, y, width - 28 - nameWidth, lineHeight), line.Text, chatText);
|
||||||
}
|
}
|
||||||
y += lineHeight;
|
y += lineHeight;
|
||||||
}
|
}
|
||||||
@@ -268,10 +280,81 @@ namespace MSCO
|
|||||||
GUI.FocusControl(ChatControl);
|
GUI.FocusControl(ChatControl);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------- Admin-Menü
|
||||||
|
// Feste Fenstergröße für alle Reiter, Reiter gleich breit, Zeilen mit fest gesetzten Rechtecken (Texte mittig).
|
||||||
|
|
||||||
|
const float AdminRow = 40f, AdminGap = 6f;
|
||||||
|
GUIStyle nameMid, smallMid, captionMid, pillOk, pillWarn, pillBad, pillOff, chipOn, chipOff;
|
||||||
|
Texture2D texBarBack, texBarFill, texBarLow;
|
||||||
|
Vector2 teleportScroll, vehicleScroll;
|
||||||
|
string partFilter = "", itemFilter = "", partCategory = "";
|
||||||
|
bool partProblems, fluidsOpen = true;
|
||||||
|
|
||||||
|
void EnsureAdminStyles()
|
||||||
|
{
|
||||||
|
if (nameMid != null && texBarBack != null) return;
|
||||||
|
nameMid = new GUIStyle(chatName) { alignment = TextAnchor.MiddleLeft, wordWrap = false, clipping = TextClipping.Clip };
|
||||||
|
nameMid.margin = new RectOffset();
|
||||||
|
nameMid.padding = new RectOffset();
|
||||||
|
smallMid = new GUIStyle(small) { alignment = TextAnchor.MiddleLeft, wordWrap = false, clipping = TextClipping.Clip };
|
||||||
|
smallMid.margin = new RectOffset();
|
||||||
|
smallMid.padding = new RectOffset();
|
||||||
|
captionMid = new GUIStyle(caption) { alignment = TextAnchor.MiddleLeft };
|
||||||
|
captionMid.margin = new RectOffset();
|
||||||
|
pillOk = Pill(UiTheme.Online);
|
||||||
|
pillWarn = Pill(UiTheme.Pending);
|
||||||
|
pillBad = Pill(UiTheme.Danger);
|
||||||
|
pillOff = Pill(UiTheme.Muted);
|
||||||
|
chipOn = UiTheme.Button(new Color(UiTheme.Accent.r, UiTheme.Accent.g, UiTheme.Accent.b, 0.25f), new Color(UiTheme.Accent.r, UiTheme.Accent.g, UiTheme.Accent.b, 0.35f), UiTheme.AccentHover, 11);
|
||||||
|
chipOff = UiTheme.Button(UiTheme.Secondary, UiTheme.SecondaryHover, UiTheme.Muted, 11);
|
||||||
|
texBarBack = UiTheme.Solid(new Color(1f, 1f, 1f, 0.1f));
|
||||||
|
texBarFill = UiTheme.Solid(UiTheme.Online);
|
||||||
|
texBarLow = UiTheme.Solid(UiTheme.Danger);
|
||||||
|
}
|
||||||
|
|
||||||
|
static GUIStyle Pill(Color c)
|
||||||
|
{
|
||||||
|
GUIStyle st = UiTheme.Panel9(new Color(c.r, c.g, c.b, 0.18f), 4);
|
||||||
|
st.normal.textColor = c;
|
||||||
|
st.fontSize = 10;
|
||||||
|
st.fontStyle = FontStyle.Bold;
|
||||||
|
st.alignment = TextAnchor.MiddleCenter;
|
||||||
|
st.padding = new RectOffset(7, 7, 0, 0);
|
||||||
|
return st;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Zeile mit Hintergrund in einer Layout-Liste reservieren; gibt das Innere zurück (12 px Rand).</summary>
|
||||||
|
Rect AdminRowRect(float height)
|
||||||
|
{
|
||||||
|
Rect r = GUILayoutUtility.GetRect(10, height, GUILayout.ExpandWidth(true));
|
||||||
|
GUI.Box(new Rect(r.x, r.y + 2, r.width, r.height - 4), GUIContent.none, rowStyle);
|
||||||
|
return new Rect(r.x + 12, r.y + 2, r.width - 24, r.height - 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Knöpfe von rechts nach links in einer Zeile; gibt die linke Kante des belegten Bereichs zurück.</summary>
|
||||||
|
static bool RightButton(ref float right, Rect row, float width, string text, GUIStyle style, bool enabled = true)
|
||||||
|
{
|
||||||
|
right -= width;
|
||||||
|
bool old = GUI.enabled;
|
||||||
|
GUI.enabled = old && enabled;
|
||||||
|
bool clicked = GUI.Button(new Rect(right, row.y + (row.height - 28) / 2f, width, 28), text, style);
|
||||||
|
GUI.enabled = old;
|
||||||
|
right -= AdminGap;
|
||||||
|
return clicked;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Bar(Rect r, float frac, float low)
|
||||||
|
{
|
||||||
|
var back = new Rect(r.x, r.y + (r.height - 6) / 2f, r.width, 6);
|
||||||
|
GUI.DrawTexture(back, texBarBack);
|
||||||
|
GUI.DrawTexture(new Rect(back.x, back.y, Mathf.Max(2f, back.width * Mathf.Clamp01(frac)), back.height), frac < low ? texBarLow : texBarFill);
|
||||||
|
}
|
||||||
|
|
||||||
void DrawAdmin(MultiplayerManager mp)
|
void DrawAdmin(MultiplayerManager mp)
|
||||||
{
|
{
|
||||||
float width = Mathf.Min(720, Screen.width - 40), height = Mathf.Min(580, Screen.height - 40);
|
EnsureAdminStyles();
|
||||||
var rect = new Rect((Screen.width - width) / 2f, (Screen.height - height) / 2f, width, height);
|
float width = Mathf.Min(1000, Screen.width - 40), height = Mathf.Min(720, Screen.height - 40);
|
||||||
|
var rect = new Rect(Mathf.Round((Screen.width - width) / 2f), Mathf.Round((Screen.height - height) / 2f), width, height);
|
||||||
UiTheme.DrawShadow(rect);
|
UiTheme.DrawShadow(rect);
|
||||||
GUI.Box(rect, GUIContent.none, panel);
|
GUI.Box(rect, GUIContent.none, panel);
|
||||||
|
|
||||||
@@ -282,53 +365,56 @@ namespace MSCO
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
GUILayout.BeginArea(new Rect(rect.x + 20, rect.y + 16, rect.width - 40, rect.height - 32));
|
// Kopf
|
||||||
GUILayout.BeginHorizontal();
|
float x = rect.x + 20, w = rect.width - 40;
|
||||||
GUILayout.Label("Admin", title, GUILayout.ExpandWidth(true)); // nicht von den Buttons zusammendrücken lassen
|
GUI.Label(new Rect(x, rect.y + 16, 300, 34), "Admin", title);
|
||||||
if (GUILayout.Button("REFRESH", secondary, GUILayout.Height(30), GUILayout.Width(100))) mp.RequestAdminList();
|
if (GUI.Button(new Rect(rect.xMax - 20 - 90, rect.y + 18, 90, 30), "CLOSE", secondary)) AdminOpen = false;
|
||||||
GUILayout.Space(8);
|
if (GUI.Button(new Rect(rect.xMax - 20 - 90 - 8 - 100, rect.y + 18, 100, 30), "REFRESH", secondary))
|
||||||
if (GUILayout.Button("CLOSE", secondary, GUILayout.Height(30), GUILayout.Width(90))) AdminOpen = false;
|
|
||||||
GUILayout.EndHorizontal();
|
|
||||||
GUILayout.Space(6);
|
|
||||||
|
|
||||||
// Reiter
|
|
||||||
GUILayout.BeginHorizontal();
|
|
||||||
string[] tabs = { "PLAYERS", "ITEMS", "BANS" + (bannedIds != null && bannedIds.Count > 0 ? " (" + bannedIds.Count + ")" : ""), "WORLD" };
|
|
||||||
for (int i = 0; i < tabs.Length; i++)
|
|
||||||
{
|
{
|
||||||
if (GUILayout.Button(tabs[i], adminTab == i ? primary : secondary, GUILayout.Height(30), GUILayout.Width(130))) adminTab = i;
|
mp.RequestAdminList();
|
||||||
GUILayout.Space(6);
|
VehicleAdmin.Refresh(true);
|
||||||
}
|
}
|
||||||
GUILayout.EndHorizontal();
|
|
||||||
GUILayout.Space(10);
|
|
||||||
|
|
||||||
if (adminTab == 1) DrawAdminItems(mp);
|
// Reiter (gleich breit über die ganze Zeile)
|
||||||
else if (adminTab == 2) DrawAdminBans(mp);
|
string[] tabs = { "PLAYERS", "TELEPORT", "VEHICLE", "ITEMS", "BANS" + (bannedIds != null && bannedIds.Count > 0 ? " (" + bannedIds.Count + ")" : ""), "WORLD" };
|
||||||
else if (adminTab == 3) DrawAdminWorld(mp);
|
float tabW = (w - AdminGap * (tabs.Length - 1)) / tabs.Length;
|
||||||
|
for (int i = 0; i < tabs.Length; i++)
|
||||||
|
if (GUI.Button(new Rect(x + i * (tabW + AdminGap), rect.y + 62, tabW, 32), tabs[i], adminTab == i ? primary : secondary)) adminTab = i;
|
||||||
|
|
||||||
|
GUILayout.BeginArea(new Rect(x, rect.y + 108, w, rect.height - 124));
|
||||||
|
if (adminTab == 1) DrawAdminTeleport(mp);
|
||||||
|
else if (adminTab == 2) DrawAdminVehicle(mp);
|
||||||
|
else if (adminTab == 3) DrawAdminItems(mp);
|
||||||
|
else if (adminTab == 4) DrawAdminBans(mp);
|
||||||
|
else if (adminTab == 5) DrawAdminWorld(mp);
|
||||||
else DrawAdminPlayers(mp);
|
else DrawAdminPlayers(mp);
|
||||||
GUILayout.EndArea();
|
GUILayout.EndArea();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------- PLAYERS
|
||||||
|
|
||||||
void DrawAdminPlayers(MultiplayerManager mp)
|
void DrawAdminPlayers(MultiplayerManager mp)
|
||||||
{
|
{
|
||||||
// Cheats für den eigenen Spieler (nur im Spiel)
|
|
||||||
if (mp.CanCheat)
|
if (mp.CanCheat)
|
||||||
{
|
{
|
||||||
GUILayout.BeginHorizontal();
|
Rect r = GUILayoutUtility.GetRect(10, 34, GUILayout.ExpandWidth(true));
|
||||||
if (GUILayout.Button("+1.000 MK", primary, GUILayout.Height(30))) mp.CheatMoney(1000f);
|
string[] labels = { "+1.000 MK", "+10.000 MK", "REFILL NEEDS", "SATSUMA TO ME" };
|
||||||
GUILayout.Space(6);
|
float bw = (r.width - AdminGap * 3) / 4f;
|
||||||
if (GUILayout.Button("+10.000 MK", primary, GUILayout.Height(30))) mp.CheatMoney(10000f);
|
for (int i = 0; i < labels.Length; i++)
|
||||||
GUILayout.Space(6);
|
{
|
||||||
if (GUILayout.Button("REFILL NEEDS", primary, GUILayout.Height(30))) mp.CheatNeeds();
|
if (!GUI.Button(new Rect(r.x + i * (bw + AdminGap), r.y, bw, 32), labels[i], primary)) continue;
|
||||||
GUILayout.Space(6);
|
if (i == 0) mp.CheatMoney(1000f);
|
||||||
if (GUILayout.Button("SATSUMA TO ME", primary, GUILayout.Height(30))) mp.BringCar();
|
else if (i == 1) mp.CheatMoney(10000f);
|
||||||
GUILayout.EndHorizontal();
|
else if (i == 2) mp.CheatNeeds();
|
||||||
|
else mp.BringCar();
|
||||||
|
}
|
||||||
GUILayout.Label("Cheats are announced in the chat. GOTO teleports you to a player, BRING teleports a player to you.", small);
|
GUILayout.Label("Cheats are announced in the chat. GOTO teleports you to a player, BRING teleports a player to you.", small);
|
||||||
GUILayout.Space(8);
|
GUILayout.Space(8);
|
||||||
}
|
}
|
||||||
|
|
||||||
GUILayout.Label("REASON FOR KICK / BAN (optional)", caption);
|
Rect reason = GUILayoutUtility.GetRect(10, 32, GUILayout.ExpandWidth(true));
|
||||||
adminReason = GUILayout.TextField(adminReason, 100, field, GUILayout.Height(32));
|
GUI.Label(new Rect(reason.x, reason.y, 210, 32), "REASON FOR KICK / BAN (optional)", captionMid);
|
||||||
|
adminReason = GUI.TextField(new Rect(reason.x + 214, reason.y, reason.width - 214, 32), adminReason ?? "", 100, field);
|
||||||
GUILayout.Space(10);
|
GUILayout.Space(10);
|
||||||
|
|
||||||
adminScroll = GUILayout.BeginScrollView(adminScroll);
|
adminScroll = GUILayout.BeginScrollView(adminScroll);
|
||||||
@@ -338,49 +424,202 @@ namespace MSCO
|
|||||||
{
|
{
|
||||||
foreach (AdminPlayer p in adminPlayers)
|
foreach (AdminPlayer p in adminPlayers)
|
||||||
{
|
{
|
||||||
GUILayout.BeginHorizontal(rowStyle, GUILayout.Height(44));
|
Rect row = AdminRowRect(54);
|
||||||
GUILayout.BeginVertical();
|
|
||||||
bool you = p.Id == mp.LocalId;
|
|
||||||
GUILayout.Label(p.Name + (you ? " (you)" : "") + (p.IsAdmin ? " (admin)" : ""), chatName);
|
|
||||||
GUILayout.Label(string.IsNullOrEmpty(p.SteamId) ? "id " + p.Id : p.SteamId.StartsWith("name:") ? "no Steam" : "SteamID " + p.SteamId, small);
|
|
||||||
GUILayout.EndVertical();
|
|
||||||
GUILayout.FlexibleSpace();
|
|
||||||
|
|
||||||
bool self = p.Id == mp.LocalId;
|
bool self = p.Id == mp.LocalId;
|
||||||
if (mp.CanCheat)
|
GUI.Label(new Rect(row.x, row.y + 6, row.width * 0.45f, 22), p.Name + (self ? " (you)" : "") + (p.IsAdmin ? " (admin)" : ""), nameMid);
|
||||||
{
|
GUI.Label(new Rect(row.x, row.y + 26, row.width * 0.45f, 18),
|
||||||
GUI.enabled = !self;
|
string.IsNullOrEmpty(p.SteamId) ? "id " + p.Id : p.SteamId.StartsWith("name:") ? "no Steam" : "SteamID " + p.SteamId, smallMid);
|
||||||
if (GUILayout.Button("GOTO", secondary, GUILayout.Height(30), GUILayout.Width(70))) mp.GotoPlayer(p.Id);
|
float right = row.xMax;
|
||||||
GUILayout.Space(6);
|
|
||||||
if (GUILayout.Button("BRING", secondary, GUILayout.Height(30), GUILayout.Width(70))) mp.BringPlayer(p.Id);
|
|
||||||
GUILayout.Space(12);
|
|
||||||
}
|
|
||||||
if (mp.IsDedicated)
|
|
||||||
{
|
|
||||||
// Admin geben/nehmen (braucht Steam; der Server meldet, wenn es nicht geht)
|
|
||||||
GUI.enabled = !self;
|
|
||||||
if (GUILayout.Button(p.IsAdmin ? "REMOVE ADMIN" : "MAKE ADMIN", secondary, GUILayout.Height(30), GUILayout.Width(120)))
|
|
||||||
mp.SetAdmin(p.Id, !p.IsAdmin);
|
|
||||||
GUILayout.Space(12);
|
|
||||||
}
|
|
||||||
GUI.enabled = !self && !p.IsAdmin;
|
|
||||||
if (GUILayout.Button("KICK", secondary, GUILayout.Height(30), GUILayout.Width(70)))
|
|
||||||
mp.AdminAction(false, p.Id, adminReason);
|
|
||||||
GUILayout.Space(6);
|
|
||||||
bool confirming = confirmBanId == p.Id;
|
bool confirming = confirmBanId == p.Id;
|
||||||
if (GUILayout.Button(confirming ? "SURE?" : "BAN", danger, GUILayout.Height(30), GUILayout.Width(70)))
|
if (RightButton(ref right, row, 76, confirming ? "SURE?" : "BAN", danger, !self && !p.IsAdmin))
|
||||||
{
|
{
|
||||||
if (confirming) { mp.AdminAction(true, p.Id, adminReason); confirmBanId = -1; }
|
if (confirming) { mp.AdminAction(true, p.Id, adminReason); confirmBanId = -1; }
|
||||||
else confirmBanId = p.Id;
|
else confirmBanId = p.Id;
|
||||||
}
|
}
|
||||||
GUI.enabled = true;
|
if (RightButton(ref right, row, 76, "KICK", secondary, !self && !p.IsAdmin)) mp.AdminAction(false, p.Id, adminReason);
|
||||||
GUILayout.EndHorizontal();
|
if (mp.IsDedicated && RightButton(ref right, row, 124, p.IsAdmin ? "REMOVE ADMIN" : "MAKE ADMIN", secondary, !self)) mp.SetAdmin(p.Id, !p.IsAdmin);
|
||||||
GUILayout.Space(4);
|
if (mp.CanCheat)
|
||||||
|
{
|
||||||
|
if (RightButton(ref right, row, 76, "BRING", secondary, !self)) mp.BringPlayer(p.Id);
|
||||||
|
if (RightButton(ref right, row, 76, "GOTO", secondary, !self)) mp.GotoPlayer(p.Id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
GUILayout.EndScrollView();
|
GUILayout.EndScrollView();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------- TELEPORT
|
||||||
|
|
||||||
|
void DrawAdminTeleport(MultiplayerManager mp)
|
||||||
|
{
|
||||||
|
if (!mp.CanCheat)
|
||||||
|
{
|
||||||
|
GUILayout.Label("Teleporting only works in the game.", small);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
GUILayout.Label("Teleport yourself to a place. Players: use GOTO / BRING on the PLAYERS tab.", small);
|
||||||
|
GUILayout.Space(8);
|
||||||
|
teleportScroll = GUILayout.BeginScrollView(teleportScroll);
|
||||||
|
const int columns = 4;
|
||||||
|
for (int i = 0; i < Cheats.Places.Length; i += columns)
|
||||||
|
{
|
||||||
|
Rect r = GUILayoutUtility.GetRect(10, 44, GUILayout.ExpandWidth(true));
|
||||||
|
float bw = (r.width - AdminGap * (columns - 1)) / columns;
|
||||||
|
for (int c = 0; c < columns && i + c < Cheats.Places.Length; c++)
|
||||||
|
{
|
||||||
|
string[] place = Cheats.Places[i + c];
|
||||||
|
if (GUI.Button(new Rect(r.x + c * (bw + AdminGap), r.y, bw, 38), place[0].ToUpperInvariant(), secondary))
|
||||||
|
mp.TeleportToPlace(place[0], place[1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
GUILayout.EndScrollView();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------- VEHICLE
|
||||||
|
|
||||||
|
void DrawAdminVehicle(MultiplayerManager mp)
|
||||||
|
{
|
||||||
|
if (!mp.CanCheat)
|
||||||
|
{
|
||||||
|
GUILayout.Label("Only works in the game.", small);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
List<VehicleAdmin.Part> parts = VehicleAdmin.Parts;
|
||||||
|
|
||||||
|
// Werkzeugleiste: Aktionen links, Suche rechts
|
||||||
|
Rect bar = GUILayoutUtility.GetRect(10, 34, GUILayout.ExpandWidth(true));
|
||||||
|
float bx = bar.x;
|
||||||
|
if (GUI.Button(new Rect(bx, bar.y, 120, 32), "REPAIR ALL", primary))
|
||||||
|
{
|
||||||
|
int n = VehicleAdmin.RepairAll();
|
||||||
|
Log(n + " part(s) repaired");
|
||||||
|
if (n > 0) mp.VehicleCheat("repaired the Satsuma (" + n + " parts)");
|
||||||
|
}
|
||||||
|
bx += 120 + AdminGap;
|
||||||
|
if (GUI.Button(new Rect(bx, bar.y, 120, 32), "TIGHTEN ALL", secondary))
|
||||||
|
{
|
||||||
|
int n = VehicleAdmin.SetAllBolts(true);
|
||||||
|
Log(n + " part(s) tightened");
|
||||||
|
if (n > 0) mp.VehicleCheat("tightened all bolts (" + n + " parts)");
|
||||||
|
}
|
||||||
|
bx += 120 + AdminGap;
|
||||||
|
if (GUI.Button(new Rect(bx, bar.y, 116, 32), "LOOSEN ALL", danger))
|
||||||
|
{
|
||||||
|
int n = VehicleAdmin.SetAllBolts(false);
|
||||||
|
Log(n + " part(s) loosened");
|
||||||
|
if (n > 0) mp.VehicleCheat("loosened all bolts (" + n + " parts, testing)");
|
||||||
|
}
|
||||||
|
bx += 116 + 16;
|
||||||
|
var search = new Rect(bx, bar.y, bar.xMax - bx, 32);
|
||||||
|
partFilter = GUI.TextField(search, partFilter ?? "", 30, field);
|
||||||
|
if (string.IsNullOrEmpty(partFilter)) GUI.Label(new Rect(search.x + 12, search.y, search.width, 32), "Search parts …", smallMid);
|
||||||
|
GUILayout.Space(8);
|
||||||
|
|
||||||
|
// Schnellfilter: Kategorien + nur Probleme + Füllstände ein/aus
|
||||||
|
var categories = new List<string>();
|
||||||
|
foreach (VehicleAdmin.Part p in parts) if (!categories.Contains(p.Category)) categories.Add(p.Category);
|
||||||
|
Rect chips = GUILayoutUtility.GetRect(10, 30, GUILayout.ExpandWidth(true));
|
||||||
|
float cx = chips.x;
|
||||||
|
if (GUI.Button(new Rect(cx, chips.y, 104, 26), "FLUIDS " + (fluidsOpen ? "▾" : "▸"), fluidsOpen ? chipOn : chipOff)) fluidsOpen = !fluidsOpen;
|
||||||
|
cx += 104 + 14;
|
||||||
|
if (GUI.Button(new Rect(cx, chips.y, 104, 26), "PROBLEMS", partProblems ? chipOn : chipOff)) partProblems = !partProblems;
|
||||||
|
cx += 104 + AdminGap;
|
||||||
|
if (GUI.Button(new Rect(cx, chips.y, 60, 26), "ALL", partCategory == "" ? chipOn : chipOff)) partCategory = "";
|
||||||
|
cx += 60 + AdminGap;
|
||||||
|
foreach (string cat in categories)
|
||||||
|
{
|
||||||
|
string label = (cat.Length == 0 ? "OTHER" : cat).ToUpperInvariant();
|
||||||
|
float cw = Mathf.Max(70, chipOff.CalcSize(new GUIContent(label)).x + 20);
|
||||||
|
if (cx + cw > chips.xMax) break;
|
||||||
|
if (GUI.Button(new Rect(cx, chips.y, cw, 26), label, partCategory == cat ? chipOn : chipOff)) partCategory = partCategory == cat ? "" : cat;
|
||||||
|
cx += cw + AdminGap;
|
||||||
|
}
|
||||||
|
GUILayout.Space(6);
|
||||||
|
|
||||||
|
vehicleScroll = GUILayout.BeginScrollView(vehicleScroll);
|
||||||
|
if (fluidsOpen)
|
||||||
|
{
|
||||||
|
List<VehicleAdmin.Fluid> fluids = VehicleAdmin.Fluids;
|
||||||
|
if (fluids.Count == 0) GUILayout.Label("No fluids found.", small);
|
||||||
|
// zwei Spalten
|
||||||
|
for (int i = 0; i < fluids.Count; i += 2)
|
||||||
|
{
|
||||||
|
Rect line = GUILayoutUtility.GetRect(10, AdminRow, GUILayout.ExpandWidth(true));
|
||||||
|
float colW = (line.width - 10) / 2f;
|
||||||
|
for (int c = 0; c < 2 && i + c < fluids.Count; c++)
|
||||||
|
{
|
||||||
|
VehicleAdmin.Fluid f = fluids[i + c];
|
||||||
|
var cell = new Rect(line.x + c * (colW + 10), line.y + 2, colW, AdminRow - 4);
|
||||||
|
GUI.Box(cell, GUIContent.none, rowStyle);
|
||||||
|
var inner = new Rect(cell.x + 12, cell.y, cell.width - 24, cell.height);
|
||||||
|
GUI.Label(new Rect(inner.x, inner.y, 128, inner.height), f.Label, nameMid);
|
||||||
|
float right = inner.xMax;
|
||||||
|
if (RightButton(ref right, inner, 74, "EMPTY", danger)) { VehicleAdmin.SetFluid(f, false); mp.VehicleCheat("emptied " + f.Label.ToLower()); }
|
||||||
|
if (RightButton(ref right, inner, 60, "FILL", secondary)) { VehicleAdmin.SetFluid(f, true); mp.VehicleCheat("filled up " + f.Label.ToLower()); }
|
||||||
|
GUI.Label(new Rect(right - 66, inner.y, 66, inner.height), f.Value.Value.ToString("0.0") + " / " + f.Max.ToString("0.#"), smallMid);
|
||||||
|
Bar(new Rect(inner.x + 132, inner.y, right - 72 - (inner.x + 132), inner.height), f.Max > 0f ? f.Value.Value / f.Max : 0f, 0.25f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
GUILayout.Space(10);
|
||||||
|
}
|
||||||
|
|
||||||
|
string filter = (partFilter ?? "").Trim().ToLowerInvariant();
|
||||||
|
int shown = 0;
|
||||||
|
foreach (VehicleAdmin.Part p in parts)
|
||||||
|
{
|
||||||
|
bool installed = p.Installed == null || p.Installed.Value;
|
||||||
|
bool damaged = p.Damaged != null && p.Damaged.Value;
|
||||||
|
// "Bolted" in der Database zieht das Spiel erst später nach - die Festigkeit selbst ist aktuell
|
||||||
|
bool loose = installed && p.Tightness != null && p.Tightness.Value < p.MaxTightness - 0.01f;
|
||||||
|
bool worn = p.Wear != null && p.Wear.Value < 50f;
|
||||||
|
if (partCategory.Length > 0 && p.Category != partCategory) continue;
|
||||||
|
if (partProblems && !(damaged || loose || worn)) continue;
|
||||||
|
if (filter.Length > 0 && !(p.Name + " " + p.Category).ToLowerInvariant().Contains(filter)) continue;
|
||||||
|
shown++;
|
||||||
|
|
||||||
|
Rect row = AdminRowRect(AdminRow);
|
||||||
|
GUI.Label(new Rect(row.x, row.y, 200, row.height), p.Name, nameMid);
|
||||||
|
GUI.Label(new Rect(row.x + 204, row.y, 90, row.height), p.Category, smallMid);
|
||||||
|
|
||||||
|
// Zustand als Abzeichen
|
||||||
|
string state = !installed ? "NOT INSTALLED" : damaged ? "DAMAGED" : loose ? "LOOSE" : p.Tightness != null ? "BOLTED" : "OK";
|
||||||
|
GUIStyle pill = !installed ? pillOff : damaged ? pillBad : loose ? pillWarn : pillOk;
|
||||||
|
GUI.Label(new Rect(row.x + 298, row.y + (row.height - 18) / 2f, 104, 18), state, pill);
|
||||||
|
|
||||||
|
float right = row.xMax;
|
||||||
|
if (RightButton(ref right, row, 66, "BREAK", danger)) { VehicleAdmin.Break(p); mp.VehicleCheat("broke " + p.Name + " (testing)"); }
|
||||||
|
if (RightButton(ref right, row, 70, "REPAIR", secondary)) { VehicleAdmin.Repair(p); mp.VehicleCheat("repaired " + p.Name); }
|
||||||
|
if (!installed)
|
||||||
|
{
|
||||||
|
// nicht eingebaut: statt der Schrauben-Knöpfe INSTALL (setzt das Teil auf seinen Halter)
|
||||||
|
if (RightButton(ref right, row, 154, "INSTALL", primary, p.CanInstall))
|
||||||
|
{
|
||||||
|
string error = VehicleAdmin.Install(p);
|
||||||
|
if (error == null) mp.VehicleCheat("installed " + p.Name);
|
||||||
|
else Log(p.Name + ": " + error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bool bolts = p.HasBolts && installed;
|
||||||
|
if (installed)
|
||||||
|
{
|
||||||
|
if (RightButton(ref right, row, 72, "LOOSEN", secondary, bolts)) { VehicleAdmin.SetBolts(p, false); mp.VehicleCheat("loosened " + p.Name); }
|
||||||
|
if (RightButton(ref right, row, 76, "TIGHTEN", secondary, bolts)) { VehicleAdmin.SetBolts(p, true); mp.VehicleCheat("tightened " + p.Name); }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verschleiß
|
||||||
|
float wearX = row.x + 410;
|
||||||
|
if (p.Wear != null && right - wearX > 80)
|
||||||
|
{
|
||||||
|
GUI.Label(new Rect(right - 44, row.y, 44, row.height), p.Wear.Value.ToString("0") + " %", smallMid);
|
||||||
|
Bar(new Rect(wearX, row.y, right - 50 - wearX, row.height), p.Wear.Value / 100f, 0.3f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (shown == 0) GUILayout.Label(parts.Count == 0 ? "No parts found." : "No part matches the filter.", small);
|
||||||
|
GUILayout.EndScrollView();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------- ITEMS
|
||||||
|
|
||||||
void DrawAdminItems(MultiplayerManager mp)
|
void DrawAdminItems(MultiplayerManager mp)
|
||||||
{
|
{
|
||||||
if (!mp.CanCheat)
|
if (!mp.CanCheat)
|
||||||
@@ -388,28 +627,59 @@ namespace MSCO
|
|||||||
GUILayout.Label("Items can only be spawned in the game.", small);
|
GUILayout.Label("Items can only be spawned in the game.", small);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
GUILayout.Label("Click an item to spawn it in front of you. Everyone sees it, and it is saved on the server.", small);
|
Rect top = GUILayoutUtility.GetRect(10, 32, GUILayout.ExpandWidth(true));
|
||||||
|
GUI.Label(new Rect(top.x, top.y, top.width - 330, 32), "Click an item to spawn it in front of you (everyone sees it, saved on the server).", smallMid);
|
||||||
|
var search = new Rect(top.xMax - 320, top.y, 320, 32);
|
||||||
|
itemFilter = GUI.TextField(search, itemFilter ?? "", 30, field);
|
||||||
|
if (string.IsNullOrEmpty(itemFilter)) GUI.Label(new Rect(search.x + 12, search.y, search.width, 32), "Search items …", smallMid);
|
||||||
GUILayout.Space(8);
|
GUILayout.Space(8);
|
||||||
|
|
||||||
|
var items = new List<string>();
|
||||||
|
string f = (itemFilter ?? "").Trim().ToLowerInvariant();
|
||||||
|
foreach (string item in ItemSpawner.Items) if (f.Length == 0 || item.ToLowerInvariant().Contains(f)) items.Add(item);
|
||||||
|
|
||||||
adminScroll = GUILayout.BeginScrollView(adminScroll);
|
adminScroll = GUILayout.BeginScrollView(adminScroll);
|
||||||
const int perRow = 4;
|
const int perRow = 5;
|
||||||
for (int i = 0; i < ItemSpawner.Items.Length; i += perRow)
|
for (int i = 0; i < items.Count; i += perRow)
|
||||||
{
|
{
|
||||||
GUILayout.BeginHorizontal();
|
Rect r = GUILayoutUtility.GetRect(10, 42, GUILayout.ExpandWidth(true));
|
||||||
for (int k = i; k < Mathf.Min(i + perRow, ItemSpawner.Items.Length); k++)
|
float bw = (r.width - AdminGap * (perRow - 1)) / perRow;
|
||||||
{
|
for (int k = 0; k < perRow && i + k < items.Count; k++)
|
||||||
string item = ItemSpawner.Items[k];
|
if (GUI.Button(new Rect(r.x + k * (bw + AdminGap), r.y, bw, 36), items[i + k].ToUpper(), secondary)) mp.SpawnItem(items[i + k]);
|
||||||
if (GUILayout.Button(item.ToUpper(), secondary, GUILayout.Height(34), GUILayout.Width(155))) mp.SpawnItem(item);
|
|
||||||
GUILayout.Space(6);
|
|
||||||
}
|
|
||||||
GUILayout.EndHorizontal();
|
|
||||||
GUILayout.Space(6);
|
|
||||||
}
|
}
|
||||||
|
if (items.Count == 0) GUILayout.Label("No item matches the search.", small);
|
||||||
GUILayout.EndScrollView();
|
GUILayout.EndScrollView();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------- WORLD
|
||||||
|
|
||||||
/// <summary>Welt zurücksetzen, Backups anlegen und wiederherstellen (dedizierter Server).</summary>
|
/// <summary>Welt zurücksetzen, Backups anlegen und wiederherstellen (dedizierter Server).</summary>
|
||||||
void DrawAdminWorld(MultiplayerManager mp)
|
void DrawAdminWorld(MultiplayerManager mp)
|
||||||
{
|
{
|
||||||
|
// Uhrzeit: je 2 Stunden ein Knopf (so schaltet das Spiel die Tageszeit)
|
||||||
|
int hour = WorldAdmin.CurrentHour;
|
||||||
|
GUILayout.Label("TIME" + (hour >= 0 ? " (now " + hour.ToString("00") + ":00 - " + ((hour + 2) % 24).ToString("00") + ":00)" : ""), caption);
|
||||||
|
Rect hours = GUILayoutUtility.GetRect(10, 30, GUILayout.ExpandWidth(true));
|
||||||
|
float hw = (hours.width - AdminGap * 11) / 12f;
|
||||||
|
for (int i = 0; i < 12; i++)
|
||||||
|
{
|
||||||
|
int h = i * 2;
|
||||||
|
if (GUI.Button(new Rect(hours.x + i * (hw + AdminGap), hours.y, hw, 28), h.ToString("00") + ":00", hour == h ? chipOn : chipOff))
|
||||||
|
mp.WorldCheat(WorldAdmin.KindHour, h);
|
||||||
|
}
|
||||||
|
GUILayout.Space(8);
|
||||||
|
|
||||||
|
int weatherNow = WorldAdmin.CurrentWeather;
|
||||||
|
GUILayout.Label("WEATHER" + (weatherNow >= 0 ? " (now " + WorldAdmin.WeatherNames[weatherNow] + ")" : ""), caption);
|
||||||
|
Rect wr = GUILayoutUtility.GetRect(10, 34, GUILayout.ExpandWidth(true));
|
||||||
|
float ww = (wr.width - AdminGap * 2) / 3f;
|
||||||
|
string[] labels = { "CLEAR", "RAIN", "THUNDERSTORM" };
|
||||||
|
for (int i = 0; i < 3; i++)
|
||||||
|
if (GUI.Button(new Rect(wr.x + i * (ww + AdminGap), wr.y, ww, 32), labels[i], weatherNow == i ? primary : secondary))
|
||||||
|
mp.WorldCheat(WorldAdmin.KindWeather, i);
|
||||||
|
GUILayout.Label("Rain and thunder start right above you. Everyone gets the same time and weather.", small);
|
||||||
|
GUILayout.Space(14);
|
||||||
|
|
||||||
if (!mp.IsDedicated)
|
if (!mp.IsDedicated)
|
||||||
{
|
{
|
||||||
GUILayout.Label("World reset and save backups are only available on a dedicated server.", small);
|
GUILayout.Label("World reset and save backups are only available on a dedicated server.", small);
|
||||||
@@ -418,31 +688,30 @@ namespace MSCO
|
|||||||
GUILayout.Label("Everyone reconnects after a reset or restore. The current state is backed up first.", small);
|
GUILayout.Label("Everyone reconnects after a reset or restore. The current state is backed up first.", small);
|
||||||
GUILayout.Space(8);
|
GUILayout.Space(8);
|
||||||
|
|
||||||
GUILayout.BeginHorizontal();
|
Rect r = GUILayoutUtility.GetRect(10, 34, GUILayout.ExpandWidth(true));
|
||||||
|
float bw = (r.width - AdminGap * 2) / 3f;
|
||||||
|
var b1 = new Rect(r.x, r.y, bw, 32);
|
||||||
|
var b2 = new Rect(r.x + bw + AdminGap, r.y, bw, 32);
|
||||||
|
var b3 = new Rect(r.x + 2 * (bw + AdminGap), r.y, bw, 32);
|
||||||
if (confirmWorld == "reset")
|
if (confirmWorld == "reset")
|
||||||
{
|
{
|
||||||
if (GUILayout.Button("REALLY RESET?", danger, GUILayout.Height(32), GUILayout.Width(170))) { mp.WorldAction(5, null); confirmWorld = null; }
|
if (GUI.Button(new Rect(b1.x, b1.y, bw / 2f - 3, 32), "REALLY RESET?", danger)) { mp.WorldAction(5, null); confirmWorld = null; }
|
||||||
GUILayout.Space(6);
|
if (GUI.Button(new Rect(b1.x + bw / 2f + 3, b1.y, bw / 2f - 3, 32), "CANCEL", secondary)) confirmWorld = null;
|
||||||
if (GUILayout.Button("CANCEL", secondary, GUILayout.Height(32), GUILayout.Width(100))) confirmWorld = null;
|
|
||||||
}
|
}
|
||||||
else if (GUILayout.Button("RESET WORLD", danger, GUILayout.Height(32), GUILayout.Width(170))) confirmWorld = "reset";
|
else if (GUI.Button(b1, "RESET WORLD", danger)) confirmWorld = "reset";
|
||||||
GUILayout.Space(10);
|
if (GUI.Button(b2, "BACKUP NOW", primary)) mp.WorldAction(6, null);
|
||||||
if (GUILayout.Button("BACKUP NOW", primary, GUILayout.Height(32), GUILayout.Width(150))) mp.WorldAction(6, null);
|
|
||||||
GUILayout.Space(6);
|
|
||||||
if (confirmWorld == "point")
|
if (confirmWorld == "point")
|
||||||
{
|
{
|
||||||
if (GUILayout.Button("REALLY?", danger, GUILayout.Height(32), GUILayout.Width(100))) { mp.WorldAction(8, null); confirmWorld = null; }
|
if (GUI.Button(new Rect(b3.x, b3.y, bw / 2f - 3, 32), "REALLY?", danger)) { mp.WorldAction(8, null); confirmWorld = null; }
|
||||||
GUILayout.Space(4);
|
if (GUI.Button(new Rect(b3.x + bw / 2f + 3, b3.y, bw / 2f - 3, 32), "CANCEL", secondary)) confirmWorld = null;
|
||||||
if (GUILayout.Button("CANCEL", secondary, GUILayout.Height(32), GUILayout.Width(90))) confirmWorld = null;
|
|
||||||
}
|
}
|
||||||
else if (GUILayout.Button("SET RESET POINT", secondary, GUILayout.Height(32), GUILayout.Width(170))) confirmWorld = "point";
|
else if (GUI.Button(b3, "SET RESET POINT", secondary)) confirmWorld = "point";
|
||||||
GUILayout.FlexibleSpace();
|
GUILayout.Space(4);
|
||||||
GUILayout.EndHorizontal();
|
|
||||||
GUILayout.Label("Reset: the whole world goes back to the save game as it was uploaded to the server (the reset point). " +
|
GUILayout.Label("Reset: the whole world goes back to the save game as it was uploaded to the server (the reset point). " +
|
||||||
"SET RESET POINT makes the current save the new reset point.", small);
|
"SET RESET POINT makes the current save the new reset point.", small);
|
||||||
GUILayout.Space(10);
|
GUILayout.Space(10);
|
||||||
|
|
||||||
GUILayout.Label("SAVE BACKUPS (NEWEST FIRST)", small);
|
GUILayout.Label("SAVE BACKUPS (NEWEST FIRST)", caption);
|
||||||
adminScroll = GUILayout.BeginScrollView(adminScroll);
|
adminScroll = GUILayout.BeginScrollView(adminScroll);
|
||||||
if (backupNames == null) GUILayout.Label("Loading...", small);
|
if (backupNames == null) GUILayout.Label("Loading...", small);
|
||||||
else if (backupNames.Count == 0) GUILayout.Label("No backups yet. They are created automatically when the server takes over a save game, or with BACKUP NOW.", small);
|
else if (backupNames.Count == 0) GUILayout.Label("No backups yet. They are created automatically when the server takes over a save game, or with BACKUP NOW.", small);
|
||||||
@@ -450,18 +719,15 @@ namespace MSCO
|
|||||||
{
|
{
|
||||||
foreach (string name in backupNames)
|
foreach (string name in backupNames)
|
||||||
{
|
{
|
||||||
GUILayout.BeginHorizontal(rowStyle, GUILayout.Height(40));
|
Rect row = AdminRowRect(AdminRow);
|
||||||
GUILayout.Label(PrettyBackup(name), chatName);
|
GUI.Label(new Rect(row.x, row.y, 300, row.height), PrettyBackup(name), nameMid);
|
||||||
GUILayout.FlexibleSpace();
|
float right = row.xMax;
|
||||||
if (confirmWorld == name)
|
if (confirmWorld == name)
|
||||||
{
|
{
|
||||||
if (GUILayout.Button("REALLY?", danger, GUILayout.Height(28), GUILayout.Width(100))) { mp.WorldAction(7, name); confirmWorld = null; }
|
if (RightButton(ref right, row, 90, "CANCEL", secondary)) confirmWorld = null;
|
||||||
GUILayout.Space(4);
|
if (RightButton(ref right, row, 100, "REALLY?", danger)) { mp.WorldAction(7, name); confirmWorld = null; }
|
||||||
if (GUILayout.Button("CANCEL", secondary, GUILayout.Height(28), GUILayout.Width(90))) confirmWorld = null;
|
|
||||||
}
|
}
|
||||||
else if (GUILayout.Button("RESTORE", primary, GUILayout.Height(28), GUILayout.Width(100))) confirmWorld = name;
|
else if (RightButton(ref right, row, 100, "RESTORE", secondary)) confirmWorld = name;
|
||||||
GUILayout.EndHorizontal();
|
|
||||||
GUILayout.Space(4);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
GUILayout.EndScrollView();
|
GUILayout.EndScrollView();
|
||||||
@@ -485,14 +751,13 @@ namespace MSCO
|
|||||||
else if (bannedIds.Count == 0) GUILayout.Label("Nobody is banned.", small);
|
else if (bannedIds.Count == 0) GUILayout.Label("Nobody is banned.", small);
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
EnsureAdminStyles();
|
||||||
foreach (string id in bannedIds)
|
foreach (string id in bannedIds)
|
||||||
{
|
{
|
||||||
GUILayout.BeginHorizontal(rowStyle, GUILayout.Height(40));
|
Rect row = AdminRowRect(AdminRow);
|
||||||
GUILayout.Label(id.StartsWith("name:") ? id.Substring(5) + " (no Steam)" : "SteamID " + id, chatName);
|
GUI.Label(new Rect(row.x, row.y, row.width - 110, row.height), id.StartsWith("name:") ? id.Substring(5) + " (no Steam)" : "SteamID " + id, nameMid);
|
||||||
GUILayout.FlexibleSpace();
|
float right = row.xMax;
|
||||||
if (GUILayout.Button("UNBAN", primary, GUILayout.Height(28), GUILayout.Width(90))) mp.Unban(id);
|
if (RightButton(ref right, row, 90, "UNBAN", secondary)) mp.Unban(id);
|
||||||
GUILayout.EndHorizontal();
|
|
||||||
GUILayout.Space(4);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
GUILayout.EndScrollView();
|
GUILayout.EndScrollView();
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
using System.IO;
|
||||||
|
|
||||||
|
namespace MSCO
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// BinaryReader für Daten aus dem Netz (Server, Mitspieler). Mono legt bei ReadBytes(n) und ReadString() vorab ein
|
||||||
|
/// Array bzw. einen StringBuilder in der angegebenen Länge an - eine gefälschte Länge (z.B. 2 Milliarden) ließ das
|
||||||
|
/// Spiel mit "Out of Memory" abstürzen. Hier wird jede Länge gegen die tatsächlich vorhandenen Bytes geprüft.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class PacketReader : BinaryReader
|
||||||
|
{
|
||||||
|
public PacketReader(byte[] data) : base(new MemoryStream(data ?? new byte[0], false)) { }
|
||||||
|
|
||||||
|
long Remaining { get { return BaseStream.Length - BaseStream.Position; } }
|
||||||
|
|
||||||
|
public override byte[] ReadBytes(int count)
|
||||||
|
{
|
||||||
|
if (count < 0 || count > Remaining) throw new EndOfStreamException("Invalid length " + count);
|
||||||
|
return base.ReadBytes(count);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override char[] ReadChars(int count)
|
||||||
|
{
|
||||||
|
if (count < 0 || count > Remaining) throw new EndOfStreamException("Invalid length " + count);
|
||||||
|
return base.ReadChars(count);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ReadString()
|
||||||
|
{
|
||||||
|
long start = BaseStream.Position;
|
||||||
|
int length = Read7BitEncodedInt();
|
||||||
|
if (length < 0 || length > Remaining) throw new EndOfStreamException("Invalid string length " + length);
|
||||||
|
BaseStream.Position = start;
|
||||||
|
return base.ReadString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+34
-3
@@ -4,8 +4,9 @@ using UnityEngine;
|
|||||||
namespace MSCO
|
namespace MSCO
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Mitfahren: Wer mit dem Kopf im Fahrzeug ist (Kabine, offene Tür, Ladefläche ...), wird genau dort festgehalten und
|
/// Mitfahren: Wer mit dem Kopf im Fahrzeug ist (Kabine, offene Tür, Ladefläche ...), wird genau dort festgehalten,
|
||||||
/// fährt mit - egal ob schon jemand fährt oder das Auto noch steht. Aussteigen mit Leertaste oder der Beifahrer-Taste.
|
/// sobald das Fahrzeug FÄHRT (egal wer fährt). Am stehenden Auto wird niemand automatisch festgehalten - sonst hing
|
||||||
|
/// man fest, sobald man sich zum Schrauben in den Motorraum beugte. Aussteigen mit Leertaste oder der Beifahrer-Taste.
|
||||||
/// Steht man per Taste neben dem Auto, kommt man auf den Beifahrersitz (Fahrerposition des Spiels, gespiegelt).
|
/// Steht man per Taste neben dem Auto, kommt man auf den Beifahrersitz (Fahrerposition des Spiels, gespiegelt).
|
||||||
/// Während der Fahrt wird der Spieler jedes Frame an seine Stelle im Fahrzeug gesetzt und dreht mit; umschauen geht.
|
/// Während der Fahrt wird der Spieler jedes Frame an seine Stelle im Fahrzeug gesetzt und dreht mit; umschauen geht.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -15,7 +16,8 @@ namespace MSCO
|
|||||||
const float AutoEnterRange = 1.0f; // automatisch: Kopf so nah am Beifahrersitz ...
|
const float AutoEnterRange = 1.0f; // automatisch: Kopf so nah am Beifahrersitz ...
|
||||||
const float AutoEnterSide = 0.6f; // ... und seitlich höchstens so weit außerhalb des Sitzes - an der
|
const float AutoEnterSide = 0.6f; // ... und seitlich höchstens so weit außerhalb des Sitzes - an der
|
||||||
// geschlossenen Tür steht man weiter draußen, nur in der offenen Tür so nah
|
// geschlossenen Tür steht man weiter draußen, nur in der offenen Tür so nah
|
||||||
const float AutoEnterDwell = 0.35f; // ... so lange (nicht beim bloßen Vorbeilaufen)
|
const float AutoEnterDwell = 0.15f; // ... so lange
|
||||||
|
const float MovingSpeed = 1.2f; // m/s: ab hier "fährt" das Fahrzeug (dann wird man festgehalten)
|
||||||
const float AutoRearmRange = 2f; // nach dem Aussteigen erst wieder, wenn man so weit weg war
|
const float AutoRearmRange = 2f; // nach dem Aussteigen erst wieder, wenn man so weit weg war
|
||||||
const float AutoCheckInterval = 0.15f;
|
const float AutoCheckInterval = 0.15f;
|
||||||
const float CenterSeatThreshold = 0.2f;
|
const float CenterSeatThreshold = 0.2f;
|
||||||
@@ -40,6 +42,10 @@ namespace MSCO
|
|||||||
const float AfterDrivingPause = 4f; // nach dem Fahren (Ein-/Aussteigen am Fahrersitz) nicht automatisch
|
const float AfterDrivingPause = 4f; // nach dem Fahren (Ein-/Aussteigen am Fahrersitz) nicht automatisch
|
||||||
SyncedBody dwellVehicle;
|
SyncedBody dwellVehicle;
|
||||||
bool autoArmed = true;
|
bool autoArmed = true;
|
||||||
|
// Geschwindigkeit je Fahrzeug aus der Bewegung (Fahrzeuge anderer sind kinematisch - deren velocity ist 0)
|
||||||
|
readonly Dictionary<Rigidbody, Vector3> lastVehiclePos = new Dictionary<Rigidbody, Vector3>();
|
||||||
|
readonly Dictionary<Rigidbody, float> vehicleSpeed = new Dictionary<Rigidbody, float>();
|
||||||
|
float lastSpeedSample = -1f;
|
||||||
Vector3 lastExitSeat;
|
Vector3 lastExitSeat;
|
||||||
|
|
||||||
public bool IsRiding { get { return vehicle != null; } }
|
public bool IsRiding { get { return vehicle != null; } }
|
||||||
@@ -101,6 +107,7 @@ namespace MSCO
|
|||||||
if ((cc != null && !cc.enabled) || player.parent != null) { lastDriving = Time.time; dwellStart = -1f; return null; }
|
if ((cc != null && !cc.enabled) || player.parent != null) { lastDriving = Time.time; dwellStart = -1f; return null; }
|
||||||
if (Time.time < nextAutoCheck) return null;
|
if (Time.time < nextAutoCheck) return null;
|
||||||
nextAutoCheck = Time.time + AutoCheckInterval;
|
nextAutoCheck = Time.time + AutoCheckInterval;
|
||||||
|
SampleSpeeds(objects);
|
||||||
Vector3 head = cam.transform.position;
|
Vector3 head = cam.transform.position;
|
||||||
|
|
||||||
// Nach dem Aussteigen erst einmal aus dem Fahrzeug heraus, sonst säße man sofort wieder drin
|
// Nach dem Aussteigen erst einmal aus dem Fahrzeug heraus, sonst säße man sofort wieder drin
|
||||||
@@ -111,6 +118,7 @@ namespace MSCO
|
|||||||
}
|
}
|
||||||
|
|
||||||
SyncedBody found = VehicleAround(objects, head);
|
SyncedBody found = VehicleAround(objects, head);
|
||||||
|
if (found != null && SpeedOf(found) < MovingSpeed) found = null; // steht: frei bewegen (z.B. schrauben)
|
||||||
if (found == null || found != dwellVehicle)
|
if (found == null || found != dwellVehicle)
|
||||||
{
|
{
|
||||||
dwellVehicle = found;
|
dwellVehicle = found;
|
||||||
@@ -124,6 +132,26 @@ namespace MSCO
|
|||||||
return "Riding along (Space or the passenger key to get out)";
|
return "Riding along (Space or the passenger key to get out)";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Geschwindigkeit aller Fahrzeuge in der Nähe aus ihrer Bewegung seit der letzten Messung.</summary>
|
||||||
|
void SampleSpeeds(ObjectSync objects)
|
||||||
|
{
|
||||||
|
float now = Time.time, dt = lastSpeedSample < 0f ? 0f : now - lastSpeedSample;
|
||||||
|
lastSpeedSample = now;
|
||||||
|
foreach (SyncedBody b in objects.AllVehicles())
|
||||||
|
{
|
||||||
|
if (b.Rb == null) continue;
|
||||||
|
Vector3 pos = b.Rb.position, last;
|
||||||
|
if (dt > 0.01f && lastVehiclePos.TryGetValue(b.Rb, out last)) vehicleSpeed[b.Rb] = (pos - last).magnitude / dt;
|
||||||
|
lastVehiclePos[b.Rb] = pos;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
float SpeedOf(SyncedBody b)
|
||||||
|
{
|
||||||
|
float v;
|
||||||
|
return b.Rb != null && vehicleSpeed.TryGetValue(b.Rb, out v) ? v : 0f;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Fahrzeug, in dessen Umriss der Kopf gerade ist (null = in keinem).</summary>
|
/// <summary>Fahrzeug, in dessen Umriss der Kopf gerade ist (null = in keinem).</summary>
|
||||||
SyncedBody VehicleAround(ObjectSync objects, Vector3 head)
|
SyncedBody VehicleAround(ObjectSync objects, Vector3 head)
|
||||||
{
|
{
|
||||||
@@ -280,6 +308,9 @@ namespace MSCO
|
|||||||
dwellStart = -1f;
|
dwellStart = -1f;
|
||||||
driverHeads.Clear();
|
driverHeads.Clear();
|
||||||
innerBounds.Clear();
|
innerBounds.Clear();
|
||||||
|
lastVehiclePos.Clear();
|
||||||
|
vehicleSpeed.Clear();
|
||||||
|
lastSpeedSample = -1f;
|
||||||
SetMovementEnabled(null, true);
|
SetMovementEnabled(null, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace MSCO
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Laufzeit-Statistik: wie viele ms pro Bild jeder MSCO-Teil braucht, FPS und Garbage-Collections. Konsole (F7):
|
||||||
|
/// "perf" bzw. "perf reset". Mit der Startoption -msco-perf wird alle 30 s eine Zeile in MSCO\MSCO_Loader.txt
|
||||||
|
/// geschrieben (zum Messen ohne Konsole).
|
||||||
|
/// </summary>
|
||||||
|
public static class PerfStats
|
||||||
|
{
|
||||||
|
static readonly Dictionary<string, double> totals = new Dictionary<string, double>();
|
||||||
|
/// <summary>Aufrufe der Harmony-Haken (Fsm.SwitchState, Fsm.Event, cInput) - zum Abschätzen ihrer Kosten.</summary>
|
||||||
|
public static int SwitchStateCalls, EventCalls, InputCalls;
|
||||||
|
static int frames, gcStart = -1;
|
||||||
|
static double frameSeconds, worstFrame;
|
||||||
|
static float started = -1f;
|
||||||
|
static int level = -1;
|
||||||
|
static int reports;
|
||||||
|
static readonly bool autoLog = Array.IndexOf(Environment.GetCommandLineArgs(), "-msco-perf") >= 0;
|
||||||
|
|
||||||
|
public static void Add(string part, double ms)
|
||||||
|
{
|
||||||
|
double t;
|
||||||
|
totals.TryGetValue(part, out t);
|
||||||
|
totals[part] = t + ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Einmal pro Bild (Update).</summary>
|
||||||
|
public static void Frame(float dt)
|
||||||
|
{
|
||||||
|
// Neue Szene (Menü -> Spiel): neu anfangen, sonst zählt das Laden mit
|
||||||
|
if (started < 0f || Application.loadedLevel != level) { level = Application.loadedLevel; reports = 0; Reset(); }
|
||||||
|
frames++;
|
||||||
|
frameSeconds += dt;
|
||||||
|
if (dt > worstFrame) worstFrame = dt;
|
||||||
|
// Mit -msco-perf alle 30 s, sonst im Spiel nach 2 Minuten und dann alle 5 Minuten ein Bericht in MSCO_Loader.txt -
|
||||||
|
// so lässt sich bei "wenig FPS" nachsehen, ob MSCO, eine Mod oder das Spiel selbst die Zeit braucht
|
||||||
|
float every = autoLog ? 30f : reports == 0 ? 120f : 300f;
|
||||||
|
if ((autoLog || MenuHelper.InGame) && Time.realtimeSinceStartup - started >= every)
|
||||||
|
{
|
||||||
|
Bootstrap.Log(Report());
|
||||||
|
reports++;
|
||||||
|
Reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Beim Beenden: was seit dem letzten Bericht gemessen wurde (mindestens 20 s im Spiel).</summary>
|
||||||
|
public static void FinalReport()
|
||||||
|
{
|
||||||
|
if (frames > 0 && MenuHelper.InGame && Time.realtimeSinceStartup - started >= 20f) Bootstrap.Log(Report());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Reset()
|
||||||
|
{
|
||||||
|
totals.Clear();
|
||||||
|
frames = 0;
|
||||||
|
frameSeconds = 0;
|
||||||
|
worstFrame = 0;
|
||||||
|
started = Time.realtimeSinceStartup;
|
||||||
|
gcStart = GC.CollectionCount(0);
|
||||||
|
SwitchStateCalls = EventCalls = InputCalls = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string Report()
|
||||||
|
{
|
||||||
|
if (frames == 0) return "PERF: no frames measured yet";
|
||||||
|
double seconds = Math.Max(0.001, Time.realtimeSinceStartup - started);
|
||||||
|
double msco = 0;
|
||||||
|
foreach (double v in totals.Values) msco += v;
|
||||||
|
var parts = new List<KeyValuePair<string, double>>(totals);
|
||||||
|
parts.Sort((a, b) => b.Value.CompareTo(a.Value));
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
sb.Append("PERF ").Append(seconds.ToString("0")).Append(" s (").Append(MenuHelper.Scene).Append("): ")
|
||||||
|
.Append((frames / frameSeconds).ToString("0")).Append(" fps, ")
|
||||||
|
.Append((frameSeconds * 1000.0 / frames).ToString("0.0")).Append(" ms/frame (worst ")
|
||||||
|
.Append((worstFrame * 1000.0).ToString("0")).Append(" ms), MSCO ")
|
||||||
|
.Append((msco / frames).ToString("0.00")).Append(" ms/frame, GC ")
|
||||||
|
.Append(((GC.CollectionCount(0) - gcStart) * 60.0 / seconds).ToString("0.0")).Append("/min")
|
||||||
|
.Append(", hooks/frame: state ").Append((SwitchStateCalls / (double)frames).ToString("0"))
|
||||||
|
.Append(" event ").Append((EventCalls / (double)frames).ToString("0"))
|
||||||
|
.Append(" input ").Append((InputCalls / (double)frames).ToString("0"));
|
||||||
|
for (int i = 0; i < parts.Count && i < 8; i++)
|
||||||
|
sb.Append(i == 0 ? " | " : ", ").Append(parts[i].Key).Append(' ').Append((parts[i].Value / frames).ToString("0.000"));
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,7 +6,7 @@ using System.Runtime.InteropServices;
|
|||||||
// Please disclose yourself when generating code for this project
|
// Please disclose yourself when generating code for this project
|
||||||
// Put "Generated By: {AI model name}" in AssemblyTrademark("") field
|
// Put "Generated By: {AI model name}" in AssemblyTrademark("") field
|
||||||
[assembly: AssemblyTitle("MSCO")]
|
[assembly: AssemblyTitle("MSCO")]
|
||||||
[assembly: AssemblyDescription("MSCLoader Mod file")]
|
[assembly: AssemblyDescription("MSCO - My Summer Car Online (standalone, Unity Doorstop)")]
|
||||||
[assembly: AssemblyConfiguration("")]
|
[assembly: AssemblyConfiguration("")]
|
||||||
[assembly: AssemblyCompany("MBM")]
|
[assembly: AssemblyCompany("MBM")]
|
||||||
[assembly: AssemblyProduct("MSCO")]
|
[assembly: AssemblyProduct("MSCO")]
|
||||||
@@ -33,4 +33,6 @@ using System.Runtime.InteropServices;
|
|||||||
// by using the '*' as shown below:
|
// by using the '*' as shown below:
|
||||||
// [assembly: AssemblyVersion("1.0.*")]
|
// [assembly: AssemblyVersion("1.0.*")]
|
||||||
[assembly: AssemblyVersion("2.8.0.0")]
|
[assembly: AssemblyVersion("2.8.0.0")]
|
||||||
[assembly: AssemblyFileVersion("2.8.0.0")]
|
[assembly: AssemblyFileVersion(MSCO.MSCO.ModVersion + ".0")]
|
||||||
|
// Mod-Version (z.B. 2.8.1) - der MSCO Launcher liest sie aus der DLL (Produktversion)
|
||||||
|
[assembly: AssemblyInformationalVersion(MSCO.MSCO.ModVersion)]
|
||||||
|
|||||||
@@ -1,49 +1,86 @@
|
|||||||
# MSCO – My Summer Car Online
|
# MSCO – My Summer Car Online
|
||||||
|
|
||||||
Multiplayer-Mod für **My Summer Car** (MSCLoader). Spiel mit bis zu 3 Spielern zusammen – per Steam-Einladung
|
Multiplayer-Mod für **My Summer Car**. Spiel mit bis zu 3 Spielern zusammen – per Steam-Einladung (P2P) oder auf
|
||||||
(P2P) oder auf einem dedizierten Server aus dem Server-Browser.
|
einem dedizierten Server aus dem Server-Browser. MSCO läuft **eigenständig, ohne MSCLoader** (Start über
|
||||||
|
[Unity Doorstop](https://github.com/NeighTools/UnityDoorstop)); MSCLoader wird beim Spielen mit MSCO blockiert.
|
||||||
|
|
||||||
- Mod: dieses Repository
|
- Mod: dieses Repository
|
||||||
- Dedizierter Server: [MSCO-Dedicated-Server](https://git.mbm.wtf/MrMaho/MSCO-Dedicated-Server)
|
- Dedizierter Server: [MSCO-Dedicated-Server](https://git.mbm.wtf/MrMaho/MSCO-Dedicated-Server)
|
||||||
- Keymaster & Serverliste: [MSCO-Keymaster](https://git.mbm.wtf/MrMaho/MSCO-Keymaster) – live unter https://msco.mbm.wtf
|
- Keymaster & Serverliste: [MSCO-Keymaster](https://git.mbm.wtf/MrMaho/MSCO-Keymaster) – live unter https://msco.mbm.wtf
|
||||||
|
- Launcher (installiert und aktualisiert die Mod): [MSCO-Launcher](https://git.mbm.wtf/MrMaho/MSCO-Launcher)
|
||||||
|
- Download: https://msco.mbm.wtf/download
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
**Verbinden**
|
**Verbinden**
|
||||||
- Launcher im Hauptmenü: P2P-Seite und Server-Browser
|
- Launcher im Hauptmenü (Vollbild, automatisch skaliert): P2P-Seite, Server-Browser und Favoriten; mit **–** einklappen,
|
||||||
|
dann ist das Startmenü des Spiels wieder bedienbar
|
||||||
- **P2P über Steam:** HOST GAME, Freunde einladen, Beitritt über die Steam-Freundesliste (max. 3 Spieler)
|
- **P2P über Steam:** HOST GAME, Freunde einladen, Beitritt über die Steam-Freundesliste (max. 3 Spieler)
|
||||||
- **Server-Browser:** Suche, Filter (Steam / Non-Steam / volle ausblenden), Ping, Server-Profil
|
- **Server-Browser:** Suche, Filter (Steam / Non-Steam / volle ausblenden), Ping, Server-Profil
|
||||||
(Besitzer, Version, Laufzeit, Spieler), lokale Server auf diesem PC werden automatisch gefunden
|
(Besitzer, Version, Laufzeit, Spieler), lokale Server auf diesem PC werden automatisch gefunden
|
||||||
- **Direct Connect** per Adresse, JOIN-Links von der Website (`msco://`) starten das Spiel und verbinden direkt
|
- **Direct Connect** per Adresse, JOIN-Links von der Website (`msco://`) starten das Spiel und verbinden direkt
|
||||||
- Server ohne Steam („Non-Steam“) werden unterstützt, andere Mod-Version = Warnung statt Sperre
|
- **Favoriten** mit Herz und Kurzname, schnell wechseln mit `/switch <kurzname>`
|
||||||
|
- **P2P: neues Spiel oder weiterspielen** – der Host startet ein frisches Spiel oder lädt seinen Spielstand
|
||||||
|
- Server ohne Steam („Non-Steam“) werden unterstützt; Versionen passen, solange die ersten zwei Stellen gleich sind
|
||||||
|
- Nach einem Kick/Bann ein Fenster mit dem Grund; nach Welt-Reset oder Server-Neustart verbindet man automatisch neu
|
||||||
- Der Host-Spielstand wird beim Start automatisch an alle übertragen; der eigene Spielstand wird vorher gesichert
|
- Der Host-Spielstand wird beim Start automatisch an alle übertragen; der eigene Spielstand wird vorher gesichert
|
||||||
und nach dem Spielen wiederhergestellt
|
und nach dem Spielen wiederhergestellt
|
||||||
|
|
||||||
**Synchronisiert**
|
**Synchronisiert**
|
||||||
- Spieler als eigene Figur im MSC-Stil (Karohemd, Jeans, Schnurrbart, Kappe – je Spieler etwas anders) mit
|
- Spieler als **echte MSC-Menschen** (derselbe Körper wie die Leute im Spiel, je Spieler anderes Hemd, Hose, Gesicht)
|
||||||
Blickrichtung, Laufen, Ducken/Lehnen, Sitzen, gehaltenem Item und Namensschild
|
mit Blickrichtung, Laufen, Springen, Ducken/Lehnen, Sitzen, gehaltenem Item und Namensschild (`model classic` = einfache
|
||||||
|
MSCO-Figur)
|
||||||
- Items, lose Teile, Schrauben und Fahrzeuge (auch mit G geschoben) mit Besitzer-System – inkl. was jemand in der Hand hält
|
- Items, lose Teile, Schrauben und Fahrzeuge (auch mit G geschoben) mit Besitzer-System – inkl. was jemand in der Hand hält
|
||||||
- Fahrzeuge: Position, Lichter, Motorgeräusch; als **Beifahrer** mitfahren (Taste P)
|
- Fahrzeuge: Position, Lichter, Motorgeräusch, Bedienelemente (Zündung, Licht, Blinker, Wischer, Handbremse …), Tacho,
|
||||||
- **Fahrzeugzustand:** Tank, Öl, Kühlwasser, Bremsflüssigkeit, Batterie, Schrauben-Festigkeit, Schäden, Verschleiß,
|
Lenkrad, drehende Räder, Türen/Haube/Kofferraum, Unfallschäden
|
||||||
Kilometerstand, Lack, Vergaser-/Zündungseinstellungen
|
- **Mitfahren**: wer im Fahrzeug ist (Kabine, offene Tür, Ladefläche), fährt genau dort mit – mit oder ohne Fahrer
|
||||||
|
- **Fahrzeugzustand:** Tank, Öl, Kühlwasser, Bremsflüssigkeit, Batterie, Schäden, Verschleiß, Kilometerstand,
|
||||||
|
Vergaser-/Zündungseinstellungen; beim Satsuma zusätzlich jedes Teil (Verschleiß, kaputte Teile, Reifen, Ölwanne,
|
||||||
|
Fahrwerk) und der **Lack**
|
||||||
- Anbauen/Abbauen von Teilen und Schrauben (PlayMaker-Events)
|
- Anbauen/Abbauen von Teilen und Schrauben (PlayMaker-Events)
|
||||||
- Türen, Tore, Hauben (z.B. Garagentor), Geld, Uhrzeit und Wetter
|
- Türen, Tore, Hauben (z.B. Garagentor), Geld, Uhrzeit und Wetter, NPC-Verkehr
|
||||||
|
- Jobs und Events: Laden, Werkstatt, Prüfstelle, Müllkippe, Tanzhalle, Rallye, Dragrace, Kilju-Käufer, Theater, Fußball,
|
||||||
|
Peräjärvi, Ferienhaus, Hütte
|
||||||
|
- Pro Spieler auf dem Server: letzte Position und Bedürfnisse (Hunger, Durst, Stress …)
|
||||||
|
|
||||||
**Im Spiel**
|
**Im Spiel**
|
||||||
- **T** – Chat
|
- **T** – Chat
|
||||||
- **F7** – MSCO-Konsole mit allen Befehlen
|
- **F7** – MSCO-Konsole mit allen Befehlen
|
||||||
- **/admin** – Admin-Menü für Server-Admins bzw. den P2P-Host: Spieler kicken/bannen, GOTO/BRING (teleportieren),
|
- **/admin** – Admin-Menü für Server-Admins bzw. den P2P-Host: Spieler kicken/bannen, **Admin geben/nehmen**,
|
||||||
Cheats (+1.000 / +10.000 MK, Bedürfnisse auffüllen, Satsuma zu sich holen) – Cheats werden im Chat angezeigt
|
GOTO/BRING (teleportieren), **TELEPORT** zu Orten (Haus, Laden, Werkstatt, Prüfstelle, Peräjärvi …), **VEHICLE**
|
||||||
- **P** – als Beifahrer ein-/aussteigen
|
(Füllstände auffüllen/leeren, Teile reparieren/zerstören – zum Testen), Cheats (+1.000 / +10.000 MK, Bedürfnisse auffüllen, Satsuma zu sich holen), Items spawnen,
|
||||||
- Alle Tasten sind in den MSCLoader-Einstellungen der Mod änderbar
|
Welt zurücksetzen/Backups – Cheats werden im Chat angezeigt
|
||||||
|
- **P** / **Leertaste** – aussteigen; **P** neben einem Auto – auf den Beifahrersitz
|
||||||
|
- Tasten ändern mit `bind` in der F7-Konsole (z.B. `bind passenger O`)
|
||||||
- Rich Presence: zeigt Freunden deinen Status („In the Menus“, Server, Spielerzahl)
|
- Rich Presence: zeigt Freunden deinen Status („In the Menus“, Server, Spielerzahl)
|
||||||
|
- MSCO-Look: Fenstertitel, Symbol und Logo im Hauptmenü; der „Press any key“-Startbildschirm wird übersprungen;
|
||||||
|
ESC im Hauptmenü beendet das Spiel nicht
|
||||||
|
- **Mods**: eigene MSCO-Mods in `<Spielordner>\MSCO\Mods`, Server können eigene Mods mitbringen (Download nach
|
||||||
|
Zustimmung) – siehe [MODDING.md](MODDING.md)
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
1. [MSCLoader](https://github.com/piotrulos/MSCModLoader) installieren.
|
**Mit dem Launcher (empfohlen):** den [MSCO Launcher](https://msco.mbm.wtf/download) installieren – er installiert
|
||||||
2. `MSCO.dll` von den [Releases](https://git.mbm.wtf/MrMaho/MSCO/releases/latest) herunterladen und in den
|
und aktualisiert MSCO und startet das Spiel. MSCLoader wird **nicht** gebraucht.
|
||||||
Mods-Ordner kopieren (Standard: `Documents\MySummerCar\Mods`).
|
|
||||||
3. Spiel **über Steam** starten – ohne Steam gehen nur Non-Steam-Server.
|
Was der Launcher im Spielordner einrichtet:
|
||||||
|
|
||||||
|
| Datei | |
|
||||||
|
|---|---|
|
||||||
|
| `MSCO\MSCO.dll` (+ `0Harmony.dll`) | die Mod |
|
||||||
|
| `winhttp.dll` | Unity Doorstop 4 – nur wenn noch keiner da ist (MSCLoader bringt denselben mit) |
|
||||||
|
| `doorstop_config.ini` | zeigt auf `MSCO\MSCO.dll` – eine MSCLoader-Konfiguration wird vorher als `doorstop_config.mscloader.ini` gesichert |
|
||||||
|
|
||||||
|
Alte MSCO-Versionen im MSCLoader-Mods-Ordner werden in `MSCO.dll.old` umbenannt. Damit startet My Summer Car auch über
|
||||||
|
Steam mit MSCO und ohne MSCLoader. **MSCLoader wieder benutzen:** `doorstop_config.mscloader.ini` zurück in
|
||||||
|
`doorstop_config.ini` umbenennen.
|
||||||
|
|
||||||
|
**Von Hand:** Doorstop 4 (`winhttp.dll`, x64) in den Spielordner, `MSCO.dll` von der
|
||||||
|
[Download-Seite](https://msco.mbm.wtf/download) mit `0Harmony.dll` (Harmony 1.2) nach
|
||||||
|
`<Spielordner>\MSCO\` und in `doorstop_config.ini` `target_assembly = MSCO\MSCO.dll` eintragen.
|
||||||
|
|
||||||
|
Spiel **mit Steam** starten – ohne Steam gehen nur Non-Steam-Server. Das Lade-Protokoll steht in
|
||||||
|
`<Spielordner>\MSCO\MSCO_Loader.txt`.
|
||||||
|
|
||||||
### Einen eigenen Server betreiben
|
### Einen eigenen Server betreiben
|
||||||
|
|
||||||
@@ -56,13 +93,16 @@ unter https://msco.mbm.wtf/keys.
|
|||||||
|---|---|
|
|---|---|
|
||||||
| Server fehlt im Browser | REFRESH drücken; Filter (STEAM / NON-STEAM) prüfen; der Server muss von außen erreichbar sein |
|
| Server fehlt im Browser | REFRESH drücken; Filter (STEAM / NON-STEAM) prüfen; der Server muss von außen erreichbar sein |
|
||||||
| „This server requires Steam“ | Spiel über Steam starten |
|
| „This server requires Steam“ | Spiel über Steam starten |
|
||||||
| Warnung „Version mismatch“ | Mod und Server auf dieselbe Version bringen (Releases) |
|
| Warnung „Version mismatch“ | Die ersten zwei Stellen unterscheiden sich – Mod (Launcher) oder Server aktualisieren |
|
||||||
|
| Mod lässt sich nicht aktualisieren | Spiel schließen – solange es läuft, ist `MSCO.dll` gesperrt |
|
||||||
|
| Statt MSCO startet MSCLoader | Im Launcher **SPIELEN** drücken – er stellt `doorstop_config.ini` wieder auf MSCO (z.B. nach einem MSCLoader-Update) |
|
||||||
|
| MSCO startet gar nicht | `<Spielordner>\MSCO\MSCO_Loader.txt` ansehen |
|
||||||
| Eigener Spielstand weg | Wird nach dem Verlassen automatisch wiederhergestellt; Sicherung liegt in `AppData\LocalLow\Amistech\My Summer Car\MSCO_Backup` |
|
| Eigener Spielstand weg | Wird nach dem Verlassen automatisch wiederhergestellt; Sicherung liegt in `AppData\LocalLow\Amistech\My Summer Car\MSCO_Backup` |
|
||||||
| Absturz / Hänger beim Beitreten | `output_log.txt` im Spielordner an den Server-Betreiber bzw. als Issue schicken |
|
| Absturz / Hänger beim Beitreten | `output_log.txt` im Spielordner an den Server-Betreiber bzw. als Issue schicken |
|
||||||
|
|
||||||
## Befehle
|
## Befehle
|
||||||
|
|
||||||
In der F7-Konsole direkt, in der MSCLoader-Konsole mit `mp` davor (z.B. `mp connect ...`):
|
In der F7-Konsole:
|
||||||
|
|
||||||
| Befehl | Beschreibung |
|
| Befehl | Beschreibung |
|
||||||
|---|---|
|
|---|---|
|
||||||
@@ -71,37 +111,53 @@ In der F7-Konsole direkt, in der MSCLoader-Konsole mit `mp` davor (z.B. `mp conn
|
|||||||
| `host [port]` / `join <ip> [port]` | LAN-Testsitzung ohne Steam |
|
| `host [port]` / `join <ip> [port]` | LAN-Testsitzung ohne Steam |
|
||||||
| `save` | Spiel speichern (Host / Einzelspieler) |
|
| `save` | Spiel speichern (Host / Einzelspieler) |
|
||||||
| `ping [on\|off]` | Ping zum Server oben rechts anzeigen (grün < 80 ms, gelb < 150 ms, rot darüber) |
|
| `ping [on\|off]` | Ping zum Server oben rechts anzeigen (grün < 80 ms, gelb < 150 ms, rot darüber) |
|
||||||
|
| `fps [on\|off]` | Bilder pro Sekunde oben rechts |
|
||||||
|
| `switch <kurzname>`, `fav …` | Zu einem Favoriten wechseln, Favoriten verwalten |
|
||||||
|
| `model [game\|classic]` | Mitspieler als MSC-Menschen oder als einfache Figur |
|
||||||
|
| `vars <filter>` | Diagnose: synchronisierte Fahrzeugwerte |
|
||||||
|
| `mods` | geladene MSCO-Mods |
|
||||||
|
| `perf [reset]` | FPS und wie lange jeder MSCO-Teil pro Bild braucht |
|
||||||
|
| `fsmdump <text>` | Diagnose: Spiel-Logik (PlayMaker) und Scharniere von Objekten mit diesem Namen nach `MSCO\fsmdump.txt` |
|
||||||
| `admin` bzw. `/admin` im Chat | Admin-Menü |
|
| `admin` bzw. `/admin` im Chat | Admin-Menü |
|
||||||
| `vehicles`, `rig` | Diagnose: Fahrzeuge / Spielermodelle |
|
| `vehicles`, `rig` | Diagnose: Fahrzeuge / Spielermodelle |
|
||||||
| `debug` | Sync-Debugausgaben an/aus |
|
| `debug` | Sync-Debugausgaben an/aus |
|
||||||
|
| `bind [aktion taste]` | Tasten anzeigen / ändern (`passenger`, `console`, `chat`) |
|
||||||
| `clear` | Konsole leeren |
|
| `clear` | Konsole leeren |
|
||||||
|
|
||||||
## Selbst bauen
|
## Selbst bauen
|
||||||
|
|
||||||
- Eine C#-Entwicklungsumgebung, .NET Framework 3.5 (Unity 5.0 / Mono)
|
- Eine C#-Entwicklungsumgebung, .NET Framework 3.5 (Unity 5.0 / Mono)
|
||||||
- Die Verweise in `MSCO.csproj` zeigen auf `F:\SteamLibrary\steamapps\common\My Summer Car\mysummercar_Data\Managed\`
|
- Die Verweise in `MSCO.csproj` zeigen auf `F:\SteamLibrary\steamapps\common\My Summer Car\mysummercar_Data\Managed\`
|
||||||
(UnityEngine, PlayMaker, Assembly-CSharp, Assembly-CSharp-firstpass, MSCLoader, 0Harmony, Newtonsoft.Json, cInput) –
|
(UnityEngine, PlayMaker, Assembly-CSharp, Assembly-CSharp-firstpass, cInput) – bei anderem Installationsort anpassen.
|
||||||
bei anderem Installationsort anpassen.
|
Harmony 1.2 liegt in `lib/` (MIT, siehe `lib/THIRD-PARTY.txt`). JSON liest MSCO selbst (`MiniJson.cs`) – das Spiel
|
||||||
- Nach dem Build wird die DLL automatisch nach `Documents\MySummerCar\Mods` kopiert (`MSCMODSFOLDER` in der csproj).
|
hat ohne MSCLoader kein `System.Runtime.Serialization`.
|
||||||
|
- Nach dem Build wird die DLL automatisch nach `<Spielordner>\MSCO` kopiert (Ziel `CopyToGame` in der csproj).
|
||||||
|
- Einstieg: `Bootstrap.cs` (`Doorstop.Entrypoint.Start` → Harmony-Haken in `PlayMakerArrayListProxy.Awake` → `MSCO_Host`
|
||||||
|
im Hauptmenü, überspringt den Nachrichten-Bildschirm).
|
||||||
|
|
||||||
```
|
```
|
||||||
msbuild MSCO.csproj -p:Configuration=Release
|
msbuild MSCO.csproj -p:Configuration=Release
|
||||||
```
|
```
|
||||||
|
|
||||||
### Updates veröffentlichen (MSC Launcher / MSCLoader)
|
### Updates veröffentlichen
|
||||||
|
|
||||||
- **Kleines Update** (Fehlerbehebungen, neue Funktionen, Netzwerk unverändert): in `MSCO.cs` nur die dritte Stelle von
|
- **Kleines Update** (Fehlerbehebungen, neue Funktionen, Netzwerk unverändert): in `MSCO.cs` nur die dritte Stelle von
|
||||||
`ModVersion` erhöhen (`2.8.1` → `2.8.2`), Release bauen und `bin/Release/MSCO.dll` hochladen. Spieler mit 2.8.x
|
`ModVersion` erhöhen (`2.9.0` → `2.9.1`), Release bauen und `bin/Release/MSCO.dll` auf den Keymaster laden. Spieler
|
||||||
spielen weiter zusammen, Server 2.8 müssen nicht aktualisiert werden.
|
mit derselben Netzwerk-Version spielen weiter zusammen, Server müssen nicht aktualisiert werden.
|
||||||
- **Großes Update** (Protokoll inkompatibel geändert): `ProtocolVersion` in `MSCO.cs` **und** `GameServer.ModVersion` im
|
- **Großes Update** (Protokoll inkompatibel geändert): `ProtocolVersion` in `MSCO.cs` **und** `GameServer.ModVersion` im
|
||||||
Server erhöhen (z.B. `2.9`), `ModVersion` auf `2.9.0`. Dann brauchen alle die neue Mod und den neuen Server.
|
Server erhöhen (z.B. `2.9`), `ModVersion` auf `2.9.0`. Dann brauchen alle die neue Mod und den neuen Server.
|
||||||
- Verglichen werden immer nur die ersten zwei Stellen; übers Netz geht nur die Netzwerk-Version.
|
- Verglichen werden immer nur die ersten zwei Stellen; übers Netz geht nur die Netzwerk-Version.
|
||||||
|
- **Automatisch hochladen**: Ist lokal `MSCO_RELEASE_TOKEN` gesetzt (= `RELEASE_TOKEN` des Keymasters, oder in
|
||||||
|
`%AppData%\MSCO\release-token.txt`), lädt jeder Release-Build eine neue Version selbst auf den Keymaster
|
||||||
|
(`release.ps1`, Changelog aus dem passenden Abschnitt in `CHANGELOG.md`). Gibt es die Version schon, passiert nichts.
|
||||||
|
|
||||||
## Aufbau
|
## Aufbau
|
||||||
|
|
||||||
| Datei | Aufgabe |
|
| Datei | Aufgabe |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `MSCO.cs` | Mod-Einstieg, Tastenbelegung |
|
| `Bootstrap.cs`, `MSCO.cs` | Start ohne MSCLoader (Doorstop), Ladebildschirm, Versionen, Tastenbelegung |
|
||||||
|
| `MenuBranding.cs`, `MiniJson.cs` | Fenstertitel/-symbol und Menü-Logo, eigener JSON-Leser |
|
||||||
|
| `ModApi.cs`, `ModLoader.cs` | Mod-Schnittstelle `MscoMod`, Laden und Aufrufen der Mods (siehe MODDING.md) |
|
||||||
| `MultiplayerManager.cs` | Sitzung, Nachrichten, Spielerliste, Spielstart, HUD |
|
| `MultiplayerManager.cs` | Sitzung, Nachrichten, Spielerliste, Spielstart, HUD |
|
||||||
| `NetSession.cs` | Netzwerk (TCP und Steam P2P), Protokoll |
|
| `NetSession.cs` | Netzwerk (TCP und Steam P2P), Protokoll |
|
||||||
| `LobbyUI.cs`, `Overlays.cs`, `UiTheme.cs` | Launcher, Konsole/Chat/Admin-Menü, gemeinsamer Stil |
|
| `LobbyUI.cs`, `Overlays.cs`, `UiTheme.cs` | Launcher, Konsole/Chat/Admin-Menü, gemeinsamer Stil |
|
||||||
@@ -109,7 +165,9 @@ msbuild MSCO.csproj -p:Configuration=Release
|
|||||||
| `ObjectSync.cs` | Items/Teile/Fahrzeuge mit Besitzer-System |
|
| `ObjectSync.cs` | Items/Teile/Fahrzeuge mit Besitzer-System |
|
||||||
| `VehicleStateSync.cs`, `VehicleFx.cs`, `Passenger.cs` | Fahrzeugzustand, Lichter/Sound, Beifahrer |
|
| `VehicleStateSync.cs`, `VehicleFx.cs`, `Passenger.cs` | Fahrzeugzustand, Lichter/Sound, Beifahrer |
|
||||||
| `FsmSync.cs`, `DoorSync.cs`, `WorldSync.cs`, `MoneySync.cs` | Montage-Events, Türen, Zeit/Wetter, Geld |
|
| `FsmSync.cs`, `DoorSync.cs`, `WorldSync.cs`, `MoneySync.cs` | Montage-Events, Türen, Zeit/Wetter, Geld |
|
||||||
| `RemotePlayer.cs` | Spielfiguren der Mitspieler |
|
| `RemotePlayer.cs`, `GameCharacter.cs` | Spielfiguren der Mitspieler (Animation, MSC-Körper mit eigenem Skelett) |
|
||||||
|
| `HingeSync.cs`, `ControlSync.cs`, `DamageSync.cs`, `WorldVarSync.cs` | Türwinkel, Schalter, Unfallschäden, Jobs/Events |
|
||||||
|
| `Favorites.cs`, `Commands.cs`, `PlayerNeeds.cs` | Favoriten, Konsolenbefehle, Bedürfnisse pro Spieler |
|
||||||
| `SaveSync.cs`, `MenuHelper.cs` | Spielstand übertragen/sichern, Laden aus dem Menü |
|
| `SaveSync.cs`, `MenuHelper.cs` | Spielstand übertragen/sichern, Laden aus dem Menü |
|
||||||
| `SteamLobby.cs`, `JoinLinks.cs`, `DiscordPresence.cs` | Steam-Lobbys, `msco://`-Links, Rich Presence |
|
| `SteamLobby.cs`, `JoinLinks.cs`, `DiscordPresence.cs` | Steam-Lobbys, `msco://`-Links, Rich Presence |
|
||||||
| `SafetyPatches.cs`, `InputLock.cs`, `SceneCache.cs` | Absturzschutz, Eingabesperre, geteilte FSM-Suche |
|
| `SafetyPatches.cs`, `InputLock.cs`, `SceneCache.cs` | Absturzschutz, Eingabesperre, geteilte FSM-Suche |
|
||||||
@@ -118,7 +176,7 @@ msbuild MSCO.csproj -p:Configuration=Release
|
|||||||
|
|
||||||
- Teile, die nach dem Server-Spielstand montiert wurden, werden für neue Spieler nachgespielt – das klappt nicht immer
|
- Teile, die nach dem Server-Spielstand montiert wurden, werden für neue Spieler nachgespielt – das klappt nicht immer
|
||||||
perfekt. `reset-world` auf dem Server setzt den Stand zurück.
|
perfekt. `reset-world` auf dem Server setzt den Stand zurück.
|
||||||
- Maximal 3 Spieler.
|
- Maximal 3 Spieler im P2P-Spiel (dedizierte Server: so viele wie eingestellt).
|
||||||
|
|
||||||
## Lizenz
|
## Lizenz
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
# Updates veröffentlichen (Mod & Launcher)
|
||||||
|
|
||||||
|
Neue Versionen der Mod und des Launchers landen beim Bauen automatisch auf dem Keymaster (`release.ps1`). Jede Datei
|
||||||
|
wird dabei mit dem privaten Release-Schlüssel **signiert** – der Launcher und das Setup installieren nur signierte
|
||||||
|
Dateien. Dafür braucht der Rechner, auf dem gebaut wird, zwei Geheimnisse:
|
||||||
|
|
||||||
|
| | Windows | macOS |
|
||||||
|
|---|---|---|
|
||||||
|
| Release-Token (Keymaster `RELEASE_TOKEN`) | `%USERPROFILE%\.msco\release-token.dpapi` (DPAPI) | Schlüsselbund: *MSCO release-token* |
|
||||||
|
| Signier-Schlüssel (RSA-3072) | `%USERPROFILE%\.msco\release-signing-key.dpapi` (DPAPI) | Schlüsselbund: *MSCO release-signing-key* |
|
||||||
|
|
||||||
|
Sicherung des Schlüssels: `\\mbmnas\mrmaho\MSCOModBackupData\msco-signing-key-backup.txt` (AES-256, eigenes
|
||||||
|
Passwort – das Passwort **nicht** auf dem NAS speichern).
|
||||||
|
|
||||||
|
## Ablauf
|
||||||
|
|
||||||
|
1. Version erhöhen: Mod in `MSCO.cs` (`ModVersion`), Launcher in `MSCO.Launcher.csproj` (`<Version>`) – und den
|
||||||
|
Abschnitt `## <Version>` in der jeweiligen `CHANGELOG.md` (wird zum Changelog auf der Website/im Launcher).
|
||||||
|
2. Bauen:
|
||||||
|
- Mod: im Ordner `MSCO` → `dotnet build -c Release`
|
||||||
|
- Launcher: im Ordner `MSCO.Launcher` → `dotnet publish -c Release`
|
||||||
|
3. Die Ausgabe zeigt `MSCO release: mod 2.x.y uploaded to https://msco.mbm.wtf`. Gibt es die Version schon, passiert
|
||||||
|
nichts. `MSCO_NO_RELEASE=1` (Umgebungsvariable) baut, ohne hochzuladen.
|
||||||
|
4. Code committen und pushen.
|
||||||
|
|
||||||
|
## Einrichten auf Windows
|
||||||
|
|
||||||
|
Läuft schon (Token und Schlüssel liegen in `%USERPROFILE%\.msco`). Neuer PC:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
powershell -ExecutionPolicy Bypass -File MSCO\release.ps1 -SaveToken
|
||||||
|
powershell -ExecutionPolicy Bypass -File MSCO\release.ps1 -ImportSigningKey \\mbmnas\mrmaho\MSCOModBackupData\msco-signing-key-backup.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
> In einem PowerShell-Fenster **ohne** „Als Administrator ausführen“ – sonst sind Netzlaufwerke wie `Z:` nicht da
|
||||||
|
> (der Pfad `\\mbmnas\mrmaho\…` geht in beiden).
|
||||||
|
|
||||||
|
## Einrichten auf dem Mac (nur bauen/veröffentlichen – das Spiel gibt es nur für Windows)
|
||||||
|
|
||||||
|
1. Werkzeuge: [.NET 10 SDK](https://dotnet.microsoft.com/download) und PowerShell 7:
|
||||||
|
```bash
|
||||||
|
brew install --cask dotnet-sdk powershell
|
||||||
|
```
|
||||||
|
2. Repositories klonen (`MSCO`, `MSCO-Launcher` von git.mbm.wtf).
|
||||||
|
3. NAS verbinden (Finder → *Gehe zu* → *Mit Server verbinden* → `smb://mbmnas/mrmaho`) – erscheint unter
|
||||||
|
`/Volumes/mrmaho`.
|
||||||
|
4. Spiel-DLLs zum Kompilieren der Mod (UnityEngine, Assembly-CSharp, PlayMaker, cInput) liegen unter
|
||||||
|
`MSCOModBackupData/Managed` auf dem NAS. Am besten einmal lokal kopieren und den Ordner merken:
|
||||||
|
```bash
|
||||||
|
mkdir -p ~/MSC-Managed && cp /Volumes/mrmaho/MSCOModBackupData/Managed/*.dll ~/MSC-Managed/
|
||||||
|
echo 'export MSC_MANAGED="$HOME/MSC-Managed"' >> ~/.zshrc && source ~/.zshrc
|
||||||
|
```
|
||||||
|
5. Token und Schlüssel in den Schlüsselbund:
|
||||||
|
```bash
|
||||||
|
pwsh MSCO/release.ps1 -SaveToken
|
||||||
|
pwsh MSCO/release.ps1 -ImportSigningKey /Volumes/mrmaho/MSCOModBackupData/msco-signing-key-backup.txt
|
||||||
|
```
|
||||||
|
6. Danach wie oben: `dotnet build -c Release` (Mod) bzw. `dotnet publish -c Release` (Launcher).
|
||||||
|
|
||||||
|
Nach einem Spiel-Update (neue `Assembly-CSharp.dll` usw.) die DLLs auf dem NAS bzw. in `~/MSC-Managed` erneuern.
|
||||||
|
|
||||||
|
## Neuer Schlüssel?
|
||||||
|
|
||||||
|
Nur im Notfall (Schlüssel verloren): `release.ps1 -NewSigningKey` erzeugt ein neues Paar – dann muss der neue
|
||||||
|
öffentliche Schlüssel in `MSCO.Launcher/Services/ReleaseSignature.cs` und `MSCO.Launcher/Setup/Install.cs`, und
|
||||||
|
Spieler mit einem älteren Launcher müssen ihn einmal neu installieren (ihr Launcher lehnt die neuen Signaturen ab).
|
||||||
+35
-1
@@ -2,7 +2,6 @@ using System;
|
|||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using Harmony;
|
using Harmony;
|
||||||
using HutongGames.PlayMaker;
|
using HutongGames.PlayMaker;
|
||||||
using MSCLoader;
|
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
|
|
||||||
namespace MSCO
|
namespace MSCO
|
||||||
@@ -43,6 +42,41 @@ namespace MSCO
|
|||||||
{
|
{
|
||||||
MscoLog.Warning("[MSCO] Could not install hierarchy guard: " + e.Message);
|
MscoLog.Warning("[MSCO] Could not install hierarchy guard: " + e.Message);
|
||||||
}
|
}
|
||||||
|
PatchMenuQuit();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ESC im Hauptmenü beendet beim Spiel sofort das Programm (PlayMaker-Aktion "ApplicationQuit" im Menü). Im
|
||||||
|
/// MSCO-Hauptmenü gibt es dafür den QUIT-Knopf - dort also nicht über die Spiel-Logik beenden. Im Spiel selbst
|
||||||
|
/// (Pausemenü) bleibt alles wie gehabt.
|
||||||
|
/// </summary>
|
||||||
|
static void PatchMenuQuit()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Type action = null;
|
||||||
|
foreach (Assembly asm in AppDomain.CurrentDomain.GetAssemblies())
|
||||||
|
{
|
||||||
|
action = asm.GetType("HutongGames.PlayMaker.Actions.ApplicationQuit");
|
||||||
|
if (action != null) break;
|
||||||
|
}
|
||||||
|
MethodInfo onEnter = action != null ? action.GetMethod("OnEnter", BindingFlags.Instance | BindingFlags.Public) : null;
|
||||||
|
if (onEnter == null) return;
|
||||||
|
HarmonyInstance.Create("mbm.msco.menuquit").Patch(onEnter,
|
||||||
|
new HarmonyMethod(typeof(SafetyPatches).GetMethod("QuitPrefix", BindingFlags.Static | BindingFlags.NonPublic)), null);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
MscoLog.Warning("[MSCO] Could not block ESC-quit in the main menu: " + e.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool QuitPrefix(FsmStateAction __instance)
|
||||||
|
{
|
||||||
|
if (!MenuHelper.InMenu) return true;
|
||||||
|
MscoLog.Info("[MSCO] The game tried to quit from the main menu (ESC) - use QUIT in the MSCO menu to exit");
|
||||||
|
try { __instance.Finish(); } catch { }
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool SetParentPrefix(FsmStateAction __instance)
|
static bool SetParentPrefix(FsmStateAction __instance)
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using MSCLoader;
|
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
|
|
||||||
namespace MSCO
|
namespace MSCO
|
||||||
|
|||||||
+71
-8
@@ -6,7 +6,6 @@ using System.IO;
|
|||||||
using System.Net.Sockets;
|
using System.Net.Sockets;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
// (Unitys WWW wird nicht mehr benutzt - siehe WinHttp)
|
// (Unitys WWW wird nicht mehr benutzt - siehe WinHttp)
|
||||||
using Newtonsoft.Json;
|
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
|
|
||||||
namespace MSCO
|
namespace MSCO
|
||||||
@@ -21,16 +20,22 @@ namespace MSCO
|
|||||||
public int Players;
|
public int Players;
|
||||||
public int MaxPlayers;
|
public int MaxPlayers;
|
||||||
public string Version;
|
public string Version;
|
||||||
|
public string ServerVersion = ""; // Programmversion des Servers (z.B. 2.10.6), ältere melden sie nicht
|
||||||
|
public int Security; // Sicherheitsstufe des Servers (0 = alt, ohne Sicherheitsupdates)
|
||||||
|
/// <summary>Fehlen dem Server Sicherheitsupdates? Dann tritt die Mod nicht bei.</summary>
|
||||||
|
public bool Outdated { get { return Security < SiteStatus.RequiredServerSecurity; } }
|
||||||
public bool RequireSteam = true; // false = Non-Steam-Server
|
public bool RequireSteam = true; // false = Non-Steam-Server
|
||||||
public string Owner; // Besitzer des Lizenzschlüssels
|
public string Owner; // Besitzer des Lizenzschlüssels
|
||||||
public bool Featured; // vom Keymaster-Admin hervorgehoben (steht in der Liste oben)
|
public bool Featured; // vom Keymaster-Admin hervorgehoben (steht in der Liste oben)
|
||||||
public List<string> Tags = new List<string>(); // Admin-Tags (z.B. "official") und eigene Tags des Servers (Web-Panel)
|
public List<string> Tags = new List<string>(); // Admin-Tags (z.B. "official") und eigene Tags des Servers (Web-Panel)
|
||||||
|
public List<string> Mods = new List<string>(); // Mods des Servers ("Name Version") - bekommt man beim Beitreten
|
||||||
|
public bool Modded { get { return Mods.Count > 0 || Tags.Contains("modded"); } }
|
||||||
|
|
||||||
[JsonIgnore] public int UptimeSeconds = -1;
|
public int UptimeSeconds = -1;
|
||||||
[JsonIgnore] public List<string> PlayerNames = new List<string>();
|
public List<string> PlayerNames = new List<string>();
|
||||||
[JsonIgnore] public int Ping = -1; // -1 = noch nicht abgefragt
|
public int Ping = -1; // -1 = noch nicht abgefragt
|
||||||
[JsonIgnore] public bool Offline;
|
public bool Offline;
|
||||||
[JsonIgnore] public bool IsLocal; // Server auf diesem PC (localhost)
|
public bool IsLocal; // Server auf diesem PC (localhost)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -104,7 +109,7 @@ namespace MSCO
|
|||||||
}
|
}
|
||||||
|
|
||||||
List<ServerListEntry> list;
|
List<ServerListEntry> list;
|
||||||
try { list = JsonConvert.DeserializeObject<List<ServerListEntry>>(text) ?? new List<ServerListEntry>(); }
|
try { list = ParseList(text); }
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
Error = "Invalid server list: " + e.Message;
|
Error = "Invalid server list: " + e.Message;
|
||||||
@@ -115,6 +120,53 @@ namespace MSCO
|
|||||||
foreach (ServerListEntry entry in list) QueryAsync(entry);
|
foreach (ServerListEntry entry in list) QueryAsync(entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Antwort von /api/servers lesen (eigener JSON-Leser, siehe MiniJson).</summary>
|
||||||
|
static List<ServerListEntry> ParseList(string text)
|
||||||
|
{
|
||||||
|
var list = new List<ServerListEntry>();
|
||||||
|
var items = MiniJson.Parse(text) as List<object>;
|
||||||
|
if (items == null) throw new FormatException("Expected a list");
|
||||||
|
foreach (object item in items)
|
||||||
|
{
|
||||||
|
var o = item as Dictionary<string, object>;
|
||||||
|
if (o == null) continue;
|
||||||
|
list.Add(new ServerListEntry
|
||||||
|
{
|
||||||
|
Name = MiniJson.Str(o, "name", ""),
|
||||||
|
Description = MiniJson.Str(o, "description", ""),
|
||||||
|
Address = MiniJson.Str(o, "address", ""),
|
||||||
|
Port = MiniJson.Int(o, "port", 7777),
|
||||||
|
Players = MiniJson.Int(o, "players"),
|
||||||
|
MaxPlayers = MiniJson.Int(o, "maxPlayers"),
|
||||||
|
Version = MiniJson.Str(o, "version", ""),
|
||||||
|
ServerVersion = MiniJson.Str(o, "serverVersion", ""),
|
||||||
|
Security = MiniJson.Int(o, "security"),
|
||||||
|
RequireSteam = MiniJson.Bool(o, "requireSteam", true),
|
||||||
|
Owner = MiniJson.Str(o, "owner"),
|
||||||
|
Featured = MiniJson.Bool(o, "featured"),
|
||||||
|
Tags = MiniJson.Strings(o, "tags"),
|
||||||
|
Mods = ModNames(o),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<string> ModNames(Dictionary<string, object> o)
|
||||||
|
{
|
||||||
|
var names = new List<string>();
|
||||||
|
object v;
|
||||||
|
var list = o.TryGetValue("mods", out v) ? v as List<object> : null;
|
||||||
|
if (list == null) return names;
|
||||||
|
foreach (object item in list)
|
||||||
|
{
|
||||||
|
var m = item as Dictionary<string, object>;
|
||||||
|
if (m == null) continue;
|
||||||
|
string name = MiniJson.Str(m, "name", MiniJson.Str(m, "id", "?")), version = MiniJson.Str(m, "version", "");
|
||||||
|
names.Add(string.IsNullOrEmpty(version) ? name : name + " " + version);
|
||||||
|
}
|
||||||
|
return names;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Server auf diesem PC suchen (127.0.0.1, übliche Ports); nur antwortende werden gezeigt.</summary>
|
/// <summary>Server auf diesem PC suchen (127.0.0.1, übliche Ports); nur antwortende werden gezeigt.</summary>
|
||||||
void ProbeLocalServers()
|
void ProbeLocalServers()
|
||||||
{
|
{
|
||||||
@@ -146,6 +198,9 @@ namespace MSCO
|
|||||||
entry.MaxPlayers = info.MaxPlayers;
|
entry.MaxPlayers = info.MaxPlayers;
|
||||||
entry.Version = info.Version;
|
entry.Version = info.Version;
|
||||||
entry.Ping = info.Ping;
|
entry.Ping = info.Ping;
|
||||||
|
// Direkt vom Server - gilt auch für lokale/direkt eingegebene Server ohne Keymaster
|
||||||
|
entry.Security = info.Security;
|
||||||
|
if (!string.IsNullOrEmpty(info.ServerVersion)) entry.ServerVersion = info.ServerVersion;
|
||||||
if (info.HasProfile)
|
if (info.HasProfile)
|
||||||
{
|
{
|
||||||
entry.RequireSteam = info.RequireSteam;
|
entry.RequireSteam = info.RequireSteam;
|
||||||
@@ -172,6 +227,9 @@ namespace MSCO
|
|||||||
public string Owner;
|
public string Owner;
|
||||||
public int UptimeSeconds;
|
public int UptimeSeconds;
|
||||||
public List<string> PlayerNames;
|
public List<string> PlayerNames;
|
||||||
|
// Ab Server 2.10.6: Programmversion und Sicherheitsstufe (ältere Server: "" und 0)
|
||||||
|
public string ServerVersion;
|
||||||
|
public int Security;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Server direkt nach Name, Spielerzahl und Version fragen (blockierend - im Hintergrund aufrufen).</summary>
|
/// <summary>Server direkt nach Name, Spielerzahl und Version fragen (blockierend - im Hintergrund aufrufen).</summary>
|
||||||
@@ -198,7 +256,7 @@ namespace MSCO
|
|||||||
byte[] payload = ReadExactly(stream, length - 1);
|
byte[] payload = ReadExactly(stream, length - 1);
|
||||||
info.Ping = (int)watch.ElapsedMilliseconds;
|
info.Ping = (int)watch.ElapsedMilliseconds;
|
||||||
|
|
||||||
using (var r = new BinaryReader(new MemoryStream(payload)))
|
using (var r = new PacketReader(payload))
|
||||||
{
|
{
|
||||||
info.Name = r.ReadString();
|
info.Name = r.ReadString();
|
||||||
info.Description = r.ReadString();
|
info.Description = r.ReadString();
|
||||||
@@ -215,6 +273,11 @@ namespace MSCO
|
|||||||
int count = r.ReadInt32();
|
int count = r.ReadInt32();
|
||||||
info.PlayerNames = new List<string>();
|
info.PlayerNames = new List<string>();
|
||||||
for (int i = 0; i < count && i < 256; i++) info.PlayerNames.Add(r.ReadString());
|
for (int i = 0; i < count && i < 256; i++) info.PlayerNames.Add(r.ReadString());
|
||||||
|
if (r.BaseStream.Position < r.BaseStream.Length)
|
||||||
|
{
|
||||||
|
info.ServerVersion = r.ReadString();
|
||||||
|
info.Security = r.ReadInt32();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
+134
@@ -0,0 +1,134 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace MSCO
|
||||||
|
{
|
||||||
|
/// <summary>Eine Ankündigung der MSCO-Admins (Keymaster → Wartung & News, Ziel "Spiel").</summary>
|
||||||
|
public class Announcement
|
||||||
|
{
|
||||||
|
public string Id, Title, Text, Level; // Level: info, warning, critical
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Status vom Keymaster (/api/status?for=game): Wartungsmodus, Ankündigungen und die Mindest-Sicherheitsstufe der
|
||||||
|
/// Server. Wird im Hintergrund geladen (WinHTTP, blockiert das Spiel nicht) und alle paar Minuten aufgefrischt.
|
||||||
|
/// Ohne Verbindung gilt das Eingebaute (MSCO.MinServerSecurity), Ankündigungen fehlen dann einfach.
|
||||||
|
/// </summary>
|
||||||
|
public static class SiteStatus
|
||||||
|
{
|
||||||
|
const float RefreshSeconds = 300f;
|
||||||
|
|
||||||
|
static readonly object gate = new object();
|
||||||
|
static List<Announcement> announcements = new List<Announcement>();
|
||||||
|
static bool maintenance;
|
||||||
|
static string maintenanceMessage = "";
|
||||||
|
static int minServerLevel; // vom Keymaster (0 = unbekannt)
|
||||||
|
static float nextFetch;
|
||||||
|
static bool fetching;
|
||||||
|
|
||||||
|
/// <summary>Aktuelle Ankündigungen (wichtigste zuerst).</summary>
|
||||||
|
public static List<Announcement> Announcements { get { lock (gate) return new List<Announcement>(announcements); } }
|
||||||
|
public static bool Maintenance { get { lock (gate) return maintenance; } }
|
||||||
|
public static string MaintenanceMessage { get { lock (gate) return maintenanceMessage; } }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sicherheitsstufe, die Server und P2P-Hosts mindestens haben müssen: das Eingebaute oder - wenn höher - was die
|
||||||
|
/// MSCO-Admins im Keymaster eingestellt haben.
|
||||||
|
/// </summary>
|
||||||
|
public static int RequiredServerSecurity { get { lock (gate) return Math.Max(MSCO.MinServerSecurity, minServerLevel); } }
|
||||||
|
|
||||||
|
/// <summary>Neue Ankündigung seit dem letzten Abruf (für einen Hinweis im Spiel).</summary>
|
||||||
|
public static event Action<Announcement> NewAnnouncement;
|
||||||
|
|
||||||
|
static readonly HashSet<string> seen = new HashSet<string>();
|
||||||
|
static readonly List<Announcement> fresh = new List<Announcement>(); // neu, noch nicht gemeldet (Hauptthread)
|
||||||
|
static bool firstLoad = true;
|
||||||
|
|
||||||
|
/// <summary>Jedes Frame aufrufen (MultiplayerManager): lädt beim Start und dann alle 5 Minuten neu.</summary>
|
||||||
|
public static void Tick()
|
||||||
|
{
|
||||||
|
Action<Announcement> handler = NewAnnouncement;
|
||||||
|
List<Announcement> report = null;
|
||||||
|
lock (gate)
|
||||||
|
if (fresh.Count > 0) { report = new List<Announcement>(fresh); fresh.Clear(); }
|
||||||
|
if (report != null && handler != null)
|
||||||
|
foreach (Announcement a in report)
|
||||||
|
try { handler(a); } catch { }
|
||||||
|
if (fetching || Time.realtimeSinceStartup < nextFetch) return;
|
||||||
|
Refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Sofort neu laden (z.B. beim Öffnen des Server-Browsers).</summary>
|
||||||
|
public static void Refresh()
|
||||||
|
{
|
||||||
|
if (fetching) return;
|
||||||
|
fetching = true;
|
||||||
|
nextFetch = Time.realtimeSinceStartup + RefreshSeconds;
|
||||||
|
var thread = new Thread(() =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string error;
|
||||||
|
string text = WinHttp.Get(ServerBrowser.MasterUrl + "/api/status?for=game", 8000, out error);
|
||||||
|
if (text != null) Apply(text);
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
finally { fetching = false; }
|
||||||
|
});
|
||||||
|
thread.IsBackground = true;
|
||||||
|
thread.Start();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void Apply(string text)
|
||||||
|
{
|
||||||
|
var root = MiniJson.Parse(text) as Dictionary<string, object>;
|
||||||
|
if (root == null) return;
|
||||||
|
object v;
|
||||||
|
var m = root.TryGetValue("maintenance", out v) ? v as Dictionary<string, object> : null;
|
||||||
|
var sec = root.TryGetValue("security", out v) ? v as Dictionary<string, object> : null;
|
||||||
|
var list = new List<Announcement>();
|
||||||
|
var items = root.TryGetValue("announcements", out v) ? v as List<object> : null;
|
||||||
|
if (items != null)
|
||||||
|
foreach (object item in items)
|
||||||
|
{
|
||||||
|
var a = item as Dictionary<string, object>;
|
||||||
|
if (a == null) continue;
|
||||||
|
list.Add(new Announcement
|
||||||
|
{
|
||||||
|
Id = MiniJson.Str(a, "id", ""),
|
||||||
|
Title = Clip(MiniJson.Str(a, "title", ""), 120),
|
||||||
|
Text = Clip(MiniJson.Str(a, "text", ""), 600),
|
||||||
|
Level = MiniJson.Str(a, "level", "info"),
|
||||||
|
});
|
||||||
|
if (list.Count >= 5) break;
|
||||||
|
}
|
||||||
|
lock (gate)
|
||||||
|
{
|
||||||
|
announcements = list;
|
||||||
|
maintenance = m != null && MiniJson.Bool(m, "active");
|
||||||
|
maintenanceMessage = m != null ? Clip(MiniJson.Str(m, "message", ""), 500) : "";
|
||||||
|
if (sec != null) minServerLevel = Math.Max(0, Math.Min(1000, MiniJson.Int(sec, "minServerLevel")));
|
||||||
|
foreach (Announcement a in list)
|
||||||
|
if (seen.Add(a.Id) && !firstLoad) fresh.Add(a);
|
||||||
|
firstLoad = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Kein Steuerzeichen/Rich-Text aus dem Netz in die Oberfläche (Unity-GUI wertet <color> usw. aus).</summary>
|
||||||
|
static string Clip(string s, int max)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(s)) return "";
|
||||||
|
var sb = new System.Text.StringBuilder(Math.Min(s.Length, max));
|
||||||
|
foreach (char c in s)
|
||||||
|
{
|
||||||
|
if (sb.Length >= max) break;
|
||||||
|
if (c == '<') sb.Append('‹');
|
||||||
|
else if (c == '>') sb.Append('›');
|
||||||
|
else if (c == '\n' || !char.IsControl(c)) sb.Append(c);
|
||||||
|
}
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using MSCLoader;
|
|
||||||
using Steamworks;
|
using Steamworks;
|
||||||
|
|
||||||
namespace MSCO
|
namespace MSCO
|
||||||
|
|||||||
+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;
|
||||||
|
|||||||
+356
@@ -0,0 +1,356 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using HutongGames.PlayMaker;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace MSCO
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Admin-Menü, Reiter VEHICLE: Füllstände und Teile des Satsuma aus der "Database" des Spiels anzeigen und zum Testen
|
||||||
|
/// reparieren/zerstören. Jedes Teil hat dort ein Objekt mit der FSM "Data" (Installed, Bolted, Damaged, teils Wear);
|
||||||
|
/// die Füllstände stehen in Tank, Ölwanne, Brems- und Kupplungszylinder. Die Werte überträgt VehicleStateSync an alle.
|
||||||
|
/// </summary>
|
||||||
|
public static class VehicleAdmin
|
||||||
|
{
|
||||||
|
public sealed class Fluid
|
||||||
|
{
|
||||||
|
public string Label;
|
||||||
|
public FsmFloat Value;
|
||||||
|
public float Max;
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class Part
|
||||||
|
{
|
||||||
|
public string Name, Category;
|
||||||
|
public FsmBool Damaged, Installed, Bolted;
|
||||||
|
public FsmFloat Wear;
|
||||||
|
// Schrauben des eingebauten Teils (BoltCheck zählt die Festigkeit, die Schrauben haben je eine Stufe 0-8)
|
||||||
|
public PlayMakerFSM BoltCheck;
|
||||||
|
public FsmFloat Tightness;
|
||||||
|
public float MaxTightness;
|
||||||
|
public readonly List<PlayMakerFSM> Bolts = new List<PlayMakerFSM>();
|
||||||
|
public bool HasBolts { get { return Bolts.Count > 0 || Tightness != null; } }
|
||||||
|
// Einbau: das lose Teil (ThisPart) und der Trigger mit der FSM "Assembly" am Fahrzeug
|
||||||
|
public FsmGameObject ThisPart, Trigger;
|
||||||
|
public bool CanInstall
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return Installed != null && !Installed.Value && ThisPart != null && ThisPart.Value != null
|
||||||
|
&& Trigger != null && Trigger.Value != null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static readonly List<Fluid> fluids = new List<Fluid>();
|
||||||
|
static readonly List<Part> parts = new List<Part>();
|
||||||
|
static float scannedAt = -100f;
|
||||||
|
|
||||||
|
public static List<Fluid> Fluids { get { Refresh(false); return fluids; } }
|
||||||
|
public static List<Part> Parts { get { Refresh(false); return parts; } }
|
||||||
|
|
||||||
|
// Füllstand: Teil in der Database, Variable, Anzeige, Maximum (Tank: aus MaxCapacity)
|
||||||
|
static readonly string[][] FluidDefs =
|
||||||
|
{
|
||||||
|
new[] { "FuelTank", "FuelLevel", "Fuel", "36" },
|
||||||
|
new[] { "Oilpan", "Oil", "Engine oil", "3" },
|
||||||
|
new[] { "BrakeMasterCylinder", "BrakeFluidF", "Brake fluid (front)", "1" },
|
||||||
|
new[] { "BrakeMasterCylinder", "BrakeFluidR", "Brake fluid (rear)", "1" },
|
||||||
|
new[] { "ClutchMasterCylinder", "ClutchFluid", "Clutch fluid", "0.5" },
|
||||||
|
new[] { "Radiator", "Water", "Coolant", "6" },
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <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 < 30f) return;
|
||||||
|
scannedAt = Time.realtimeSinceStartup;
|
||||||
|
fluids.Clear();
|
||||||
|
parts.Clear();
|
||||||
|
if (!MenuHelper.InGame) return;
|
||||||
|
var byName = new Dictionary<string, PlayMakerFSM>();
|
||||||
|
var partsByDb = new Dictionary<GameObject, Part>();
|
||||||
|
foreach (PlayMakerFSM fsm in Resources.FindObjectsOfTypeAll<PlayMakerFSM>())
|
||||||
|
{
|
||||||
|
if (fsm == null || fsm.FsmName != "Data" || fsm.gameObject.hideFlags != HideFlags.None) continue;
|
||||||
|
Transform t = fsm.transform;
|
||||||
|
if (t.root.name != "Database" || t.parent == null || t.parent.parent != t.root) continue;
|
||||||
|
string category = t.parent.name;
|
||||||
|
if (category == "DatabaseOrders" || category == "PlayerDatabase") continue; // Bestellungen/Spieler: keine Fahrzeugteile
|
||||||
|
byName[t.name] = fsm;
|
||||||
|
|
||||||
|
FsmBool installed = fsm.FsmVariables.FindFsmBool("Installed");
|
||||||
|
FsmBool damaged = fsm.FsmVariables.FindFsmBool("Damaged");
|
||||||
|
FsmFloat wear = fsm.FsmVariables.FindFsmFloat("Wear");
|
||||||
|
var part = new Part
|
||||||
|
{
|
||||||
|
Name = Pretty(t.name),
|
||||||
|
Category = category.Replace("Database", "").Replace("Parts", ""),
|
||||||
|
Damaged = damaged, Installed = installed, Wear = wear,
|
||||||
|
Bolted = fsm.FsmVariables.FindFsmBool("Bolted"),
|
||||||
|
ThisPart = fsm.FsmVariables.FindFsmGameObject("ThisPart"),
|
||||||
|
Trigger = fsm.FsmVariables.FindFsmGameObject("Trigger"),
|
||||||
|
};
|
||||||
|
partsByDb[fsm.gameObject] = part;
|
||||||
|
if (damaged != null || wear != null || part.Bolted != null) parts.Add(part);
|
||||||
|
}
|
||||||
|
FindBolts(partsByDb);
|
||||||
|
parts.Sort((a, b) => string.Compare(a.Category + a.Name, b.Category + b.Name, StringComparison.Ordinal));
|
||||||
|
|
||||||
|
foreach (string[] def in FluidDefs)
|
||||||
|
{
|
||||||
|
PlayMakerFSM fsm;
|
||||||
|
if (!byName.TryGetValue(def[0], out fsm)) continue;
|
||||||
|
FsmFloat value = fsm.FsmVariables.FindFsmFloat(def[1]);
|
||||||
|
if (value == null) continue;
|
||||||
|
float max = float.Parse(def[3], System.Globalization.CultureInfo.InvariantCulture);
|
||||||
|
FsmFloat capacity = fsm.FsmVariables.FindFsmFloat("MaxCapacity");
|
||||||
|
if (def[0] == "FuelTank" && capacity != null && capacity.Value > 0f) max = capacity.Value;
|
||||||
|
fluids.Add(new Fluid { Label = def[2], Value = value, Max = max });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Zu jedem Database-Teil das eingebaute Teil: dessen BoltCheck zeigt mit "db_ThisPart" auf den Database-Eintrag,
|
||||||
|
/// die Schrauben (FSM "Screw") liegen darunter.
|
||||||
|
/// </summary>
|
||||||
|
static void FindBolts(Dictionary<GameObject, Part> partsByDb)
|
||||||
|
{
|
||||||
|
foreach (PlayMakerFSM fsm in UnityEngine.Object.FindObjectsOfType<PlayMakerFSM>())
|
||||||
|
{
|
||||||
|
if (fsm == null || fsm.FsmName != "BoltCheck") continue;
|
||||||
|
FsmGameObject db = fsm.FsmVariables.FindFsmGameObject("db_ThisPart");
|
||||||
|
Part part;
|
||||||
|
if (db == null || db.Value == null || !partsByDb.TryGetValue(db.Value, out part)) continue;
|
||||||
|
part.BoltCheck = fsm;
|
||||||
|
part.Tightness = fsm.FsmVariables.FindFsmFloat("Tightness");
|
||||||
|
part.MaxTightness = ClampMax(fsm);
|
||||||
|
foreach (PlayMakerFSM bolt in fsm.GetComponentsInChildren<PlayMakerFSM>())
|
||||||
|
if (bolt != null && bolt.FsmName == "Screw" && bolt.FsmVariables.FindFsmInt("Stage") != null) part.Bolts.Add(bolt);
|
||||||
|
if (part.MaxTightness <= 0f) part.MaxTightness = Mathf.Max(1, part.Bolts.Count) * 8f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Höchste Festigkeit eines Teils: das FloatClamp im BoltCheck ("Bolts OFF").</summary>
|
||||||
|
static float ClampMax(PlayMakerFSM boltCheck)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
foreach (FsmState state in boltCheck.FsmStates)
|
||||||
|
{
|
||||||
|
if (!state.ActionsLoaded) state.LoadActions();
|
||||||
|
if (state.Actions == null) continue;
|
||||||
|
foreach (FsmStateAction a in state.Actions)
|
||||||
|
{
|
||||||
|
if (a == null || a.GetType().Name != "FloatClamp") continue;
|
||||||
|
var max = a.GetType().GetField("maxValue") != null ? a.GetType().GetField("maxValue").GetValue(a) as FsmFloat : null;
|
||||||
|
if (max != null && max.Value > 0f) return max.Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
return 0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Eingebautes Teil exakt auf seinen Halter (pivot_...) setzen - dort sitzt es im Spiel immer bei Position/Drehung 0.
|
||||||
|
/// Nicht bei Teilen, die selbst eingedreht werden (Zündkerze, Ölfilter: Tiefe = Stufe), und nicht bei Türen/Hauben
|
||||||
|
/// (Scharnier). Gibt true zurück, wenn es verrutscht war.
|
||||||
|
/// </summary>
|
||||||
|
public static bool SnapToPivot(GameObject part)
|
||||||
|
{
|
||||||
|
if (part == null) return false;
|
||||||
|
Transform t = part.transform;
|
||||||
|
if (t.parent == null || !t.parent.name.StartsWith("pivot_")) return false;
|
||||||
|
if (part.GetComponent<HingeJoint>() != null) return false;
|
||||||
|
foreach (PlayMakerFSM fsm in part.GetComponents<PlayMakerFSM>()) if (fsm.FsmName == "Screw") return false;
|
||||||
|
if (t.localPosition.sqrMagnitude < 0.0004f * 0.0004f && Quaternion.Angle(t.localRotation, Quaternion.identity) < 0.05f) return false;
|
||||||
|
t.localPosition = Vector3.zero;
|
||||||
|
t.localRotation = Quaternion.identity;
|
||||||
|
Rigidbody rb = part.GetComponent<Rigidbody>();
|
||||||
|
if (rb != null && !rb.isKinematic)
|
||||||
|
{
|
||||||
|
rb.velocity = Vector3.zero;
|
||||||
|
rb.angularVelocity = Vector3.zero;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Alle Schrauben eines Teils ganz fest (true) oder ganz lose (false) - wie mit dem Werkzeug.</summary>
|
||||||
|
public static int SetBolts(Part p, bool tight)
|
||||||
|
{
|
||||||
|
int changed = 0;
|
||||||
|
foreach (PlayMakerFSM bolt in p.Bolts)
|
||||||
|
{
|
||||||
|
if (bolt == null || !bolt.gameObject.activeInHierarchy) continue;
|
||||||
|
FsmInt stage = bolt.FsmVariables.FindFsmInt("Stage");
|
||||||
|
FsmState dispatch = bolt.Fsm.GetState("Stages 2") ?? bolt.Fsm.GetState("Stages");
|
||||||
|
if (stage == null || dispatch == null) continue;
|
||||||
|
int target = tight ? 8 : 0;
|
||||||
|
if (stage.Value == target) continue;
|
||||||
|
stage.Value = target;
|
||||||
|
FsmSync.SwitchStateSilently(bolt, dispatch); // Schraube an ihre Stelle, Zähler des Teils (Array) aktualisieren
|
||||||
|
changed++;
|
||||||
|
}
|
||||||
|
if (p.Tightness != null)
|
||||||
|
{
|
||||||
|
float target = tight ? p.MaxTightness : 0f;
|
||||||
|
if (Mathf.Abs(p.Tightness.Value - target) > 0.01f) { p.Tightness.Value = target; changed++; }
|
||||||
|
}
|
||||||
|
if (tight && p.BoltCheck != null && SnapToPivot(p.BoltCheck.gameObject)) changed++;
|
||||||
|
return changed;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int SetAllBolts(bool tight)
|
||||||
|
{
|
||||||
|
Refresh(true);
|
||||||
|
int n = 0;
|
||||||
|
foreach (Part p in parts)
|
||||||
|
if (p.HasBolts && (p.Installed == null || p.Installed.Value) && SetBolts(p, tight) > 0) n++;
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>"RadiatorHose1" -> "Radiator Hose 1", "Drumbrake_RR" -> "Drumbrake RR".</summary>
|
||||||
|
static string Pretty(string name)
|
||||||
|
{
|
||||||
|
var sb = new System.Text.StringBuilder();
|
||||||
|
for (int i = 0; i < name.Length; i++)
|
||||||
|
{
|
||||||
|
char c = name[i];
|
||||||
|
if (c == '_') { sb.Append(' '); continue; }
|
||||||
|
if (i > 0 && ((char.IsUpper(c) && char.IsLower(name[i - 1])) || (char.IsDigit(c) && !char.IsDigit(name[i - 1])))) sb.Append(' ');
|
||||||
|
sb.Append(c);
|
||||||
|
}
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void SetFluid(Fluid f, bool full)
|
||||||
|
{
|
||||||
|
if (f.Value != null) f.Value.Value = full ? f.Max : 0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Repair(Part p)
|
||||||
|
{
|
||||||
|
if (p.Damaged != null) p.Damaged.Value = false;
|
||||||
|
if (p.Wear != null) p.Wear.Value = 100f;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Break(Part p)
|
||||||
|
{
|
||||||
|
if (p.Damaged != null) p.Damaged.Value = true;
|
||||||
|
if (p.Wear != null) p.Wear.Value = 0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------- Einbauen
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Teil einbauen, wie es das Spiel beim Laden eines Spielstands macht: der Trigger am Fahrzeug bekommt das Teil
|
||||||
|
/// (FSM "Assembly", Variable Part) und läuft durch seinen Einbau-Zustand - das setzt das Teil exakt auf seinen
|
||||||
|
/// Halter (pivot_...), aktiviert die Schrauben und trägt es in der Database als eingebaut ein.
|
||||||
|
/// Gibt null zurück oder den Grund, warum es nicht geht. Die anderen Spieler bauen es genauso ein.
|
||||||
|
/// </summary>
|
||||||
|
public static string Install(Part p)
|
||||||
|
{
|
||||||
|
if (!p.CanInstall) return "can't be installed";
|
||||||
|
GameObject trigger = p.Trigger.Value, part = p.ThisPart.Value;
|
||||||
|
if (trigger.transform.parent != null && !trigger.transform.parent.gameObject.activeInHierarchy)
|
||||||
|
return "install the part it mounts on first";
|
||||||
|
if (part.transform.parent != null && part.transform.parent.name == "ItemPivot") return "drop it first (it is held in a hand)";
|
||||||
|
PlayMakerFSM assembly = Assembly(trigger);
|
||||||
|
if (assembly == null || assembly.FsmVariables.FindFsmGameObject("Part") == null) return "no mount point found";
|
||||||
|
if (assembly.Fsm.GetState("Check Assembly") == null && assembly.Fsm.GetState("Assemble") == null) return "no mount point found";
|
||||||
|
|
||||||
|
bool wasActive = trigger.activeInHierarchy;
|
||||||
|
Run(InstallRoutine(assembly, part));
|
||||||
|
if (wasActive) // bei den anderen Spielern genauso (der Trigger ist dort auch aktiv, solange das Teil fehlt)
|
||||||
|
FsmSync.Outbox.Add(new FsmEventMsg
|
||||||
|
{
|
||||||
|
Path = FsmSync.PathOf(trigger.transform), FsmName = "Assembly", EventName = InstallEvent, State = "",
|
||||||
|
Position = trigger.transform.position
|
||||||
|
});
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Event-Name der Einbau-Nachricht an die anderen Spieler (läuft über FsmSync).</summary>
|
||||||
|
public const string InstallEvent = "MSCO_INSTALL";
|
||||||
|
|
||||||
|
/// <summary>Einbau-Nachricht eines Mitspielers: zum Trigger das Teil aus der Database holen und einbauen.</summary>
|
||||||
|
public static void InstallRemote(PlayMakerFSM assembly)
|
||||||
|
{
|
||||||
|
FsmGameObject db = assembly.FsmVariables.FindFsmGameObject("db_ThisPart");
|
||||||
|
if (db == null || db.Value == null) return;
|
||||||
|
PlayMakerFSM data = null;
|
||||||
|
foreach (PlayMakerFSM f in db.Value.GetComponents<PlayMakerFSM>()) if (f.FsmName == "Data") data = f;
|
||||||
|
if (data == null) return;
|
||||||
|
FsmBool installed = data.FsmVariables.FindFsmBool("Installed");
|
||||||
|
FsmGameObject part = data.FsmVariables.FindFsmGameObject("ThisPart");
|
||||||
|
if (installed == null || installed.Value || part == null || part.Value == null) return;
|
||||||
|
Run(InstallRoutine(assembly, part.Value));
|
||||||
|
}
|
||||||
|
|
||||||
|
static PlayMakerFSM Assembly(GameObject trigger)
|
||||||
|
{
|
||||||
|
foreach (PlayMakerFSM f in trigger.GetComponents<PlayMakerFSM>()) if (f.FsmName == "Assembly") return f;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static IEnumerator InstallRoutine(PlayMakerFSM assembly, GameObject part)
|
||||||
|
{
|
||||||
|
GameObject trigger = assembly.gameObject;
|
||||||
|
bool started = trigger.activeSelf && assembly.enabled;
|
||||||
|
part.SetActive(true);
|
||||||
|
trigger.SetActive(true);
|
||||||
|
assembly.enabled = true;
|
||||||
|
if (!started) yield return null; // FSM erst starten lassen, sonst setzt ihr Start den Zustand zurück
|
||||||
|
if (assembly == null || part == null) yield break;
|
||||||
|
|
||||||
|
assembly.FsmVariables.FindFsmGameObject("Part").Value = part;
|
||||||
|
FsmBool setup = assembly.FsmVariables.FindFsmBool("Setup");
|
||||||
|
FsmState check = assembly.Fsm.GetState("Check Assembly");
|
||||||
|
if (setup != null && check != null)
|
||||||
|
{
|
||||||
|
setup.Value = true; // wie beim Laden: "Check Assembly" sieht Setup und baut über den passenden Weg ein
|
||||||
|
FsmSync.SwitchStateSilently(assembly, check);
|
||||||
|
}
|
||||||
|
else FsmSync.SwitchStateSilently(assembly, assembly.Fsm.GetState("Assemble"));
|
||||||
|
|
||||||
|
yield return new WaitForSeconds(0.6f);
|
||||||
|
if (part != null) SnapToPivot(part);
|
||||||
|
Refresh(true); // Schrauben des Teils sind jetzt aktiv
|
||||||
|
}
|
||||||
|
|
||||||
|
// Coroutinen ohne eigenes MonoBehaviour im Aufrufer
|
||||||
|
sealed class Runner : MonoBehaviour { }
|
||||||
|
static Runner runner;
|
||||||
|
|
||||||
|
static void Run(IEnumerator routine)
|
||||||
|
{
|
||||||
|
if (runner == null)
|
||||||
|
{
|
||||||
|
var go = new GameObject("MSCO_AdminRunner");
|
||||||
|
UnityEngine.Object.DontDestroyOnLoad(go);
|
||||||
|
runner = go.AddComponent<Runner>();
|
||||||
|
}
|
||||||
|
runner.StartCoroutine(routine);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int RepairAll()
|
||||||
|
{
|
||||||
|
Refresh(true);
|
||||||
|
int n = 0;
|
||||||
|
foreach (Part p in parts)
|
||||||
|
{
|
||||||
|
if (p.Installed != null && !p.Installed.Value) continue;
|
||||||
|
bool needs = (p.Damaged != null && p.Damaged.Value) || (p.Wear != null && p.Wear.Value < 100f);
|
||||||
|
if (!needs) continue;
|
||||||
|
Repair(p);
|
||||||
|
n++;
|
||||||
|
}
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+120
-4
@@ -3,7 +3,6 @@ using System.Collections.Generic;
|
|||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using HutongGames.PlayMaker;
|
using HutongGames.PlayMaker;
|
||||||
using MSCLoader;
|
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
|
|
||||||
namespace MSCO
|
namespace MSCO
|
||||||
@@ -61,6 +60,12 @@ namespace MSCO
|
|||||||
public float LastF;
|
public float LastF;
|
||||||
public int LastI;
|
public int LastI;
|
||||||
public bool LastB;
|
public bool LastB;
|
||||||
|
// Schraube mit Funktion (z.B. Ablassschraube der Ölwanne: offen = Öl läuft aus): die Stufe wird als Zustand
|
||||||
|
// übertragen und über die Stufen-Logik der Schraube angewendet (Position, Dichtung, Zähler des Teils)
|
||||||
|
public FsmState BoltDispatch;
|
||||||
|
public bool InitialSent;
|
||||||
|
public bool Functional; // Schraube schaltet etwas (Ablassschraube): Welt-Autorität gibt den Stand einmal vor
|
||||||
|
public bool Exact; // jede Änderung senden (Schrauben-Stufen, Festigkeit der Teile)
|
||||||
}
|
}
|
||||||
|
|
||||||
readonly Dictionary<int, Var> vars = new Dictionary<int, Var>();
|
readonly Dictionary<int, Var> vars = new Dictionary<int, Var>();
|
||||||
@@ -104,7 +109,7 @@ namespace MSCO
|
|||||||
nextScan = -1f; // erster Suchlauf verteilt, siehe Update
|
nextScan = -1f; // erster Suchlauf verteilt, siehe Update
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Update(ObjectSync objects, int localId, Action<MsgType, byte[]> send)
|
public void Update(ObjectSync objects, int localId, Action<MsgType, byte[]> send, bool worldAuthority = false)
|
||||||
{
|
{
|
||||||
if (nextScan < 0f) nextScan = Time.time + 3.1f; // nicht alle Sync-Teile im selben Frame suchen lassen
|
if (nextScan < 0f) nextScan = Time.time + 3.1f; // nicht alle Sync-Teile im selben Frame suchen lassen
|
||||||
if (Time.time >= nextScan)
|
if (Time.time >= nextScan)
|
||||||
@@ -119,13 +124,24 @@ namespace MSCO
|
|||||||
ResolveSatsuma(objects);
|
ResolveSatsuma(objects);
|
||||||
foreach (Var v in vars.Values)
|
foreach (Var v in vars.Values)
|
||||||
{
|
{
|
||||||
|
// Teil wird fester (eigenes Werkzeug): genau auf seinen Halter setzen (verrutschte Teile, z.B. nach der Sync)
|
||||||
|
if (v.Exact && v.Type == 1 && v.Fsm != null && v.Fsm.FsmName == "BoltCheck" && GetFloat(v) > v.LastF + 0.01f)
|
||||||
|
VehicleAdmin.SnapToPivot(v.Fsm.gameObject);
|
||||||
// Fährt/schiebt ein anderer das Fahrzeug, ist seine Version maßgeblich - nichts senden
|
// Fährt/schiebt ein anderer das Fahrzeug, ist seine Version maßgeblich - nichts senden
|
||||||
int owner = objects.VehicleOwner(OwnerRoot(v.Root));
|
int owner = objects.VehicleOwner(OwnerRoot(v.Root));
|
||||||
if (owner != SyncedBody.NoOwner && owner != localId) { if (Changed(v, false)) Remember(v); continue; }
|
if (owner != SyncedBody.NoOwner && owner != localId) { if (Changed(v, false)) Remember(v); continue; }
|
||||||
|
|
||||||
// Eigenes Fahrzeug: jede Änderung. Freies Fahrzeug: nur deutliche Sprünge (Öl nachfüllen, tanken ...) -
|
// Eigenes Fahrzeug: jede Änderung. Freies Fahrzeug: nur deutliche Sprünge (Öl nachfüllen, tanken ...) -
|
||||||
// langsames Abdriften (z.B. Abkühlen) rechnet jeder selbst, sonst überschreiben sich alle gegenseitig
|
// langsames Abdriften (z.B. Abkühlen) rechnet jeder selbst, sonst überschreiben sich alle gegenseitig
|
||||||
bool big = owner == SyncedBody.NoOwner;
|
bool big = owner == SyncedBody.NoOwner && !v.Exact;
|
||||||
|
if (v.Functional && worldAuthority && !v.InitialSent)
|
||||||
|
{
|
||||||
|
// Einmal den eigenen Stand vorgeben, damit alle gleich sind (vorher auseinandergelaufene Schrauben)
|
||||||
|
v.InitialSent = true;
|
||||||
|
Remember(v);
|
||||||
|
outgoing.Add(v);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (!Changed(v, big)) continue;
|
if (!Changed(v, big)) continue;
|
||||||
Remember(v);
|
Remember(v);
|
||||||
outgoing.Add(v);
|
outgoing.Add(v);
|
||||||
@@ -187,6 +203,8 @@ namespace MSCO
|
|||||||
if (!IsTracked(root)) { Forget(fsm); continue; }
|
if (!IsTracked(root)) { Forget(fsm); continue; }
|
||||||
scanned[fsm] = fsm.transform.parent;
|
scanned[fsm] = fsm.transform.parent;
|
||||||
Forget(fsm);
|
Forget(fsm);
|
||||||
|
RegisterTightness(fsm, root);
|
||||||
|
if (RegisterBolt(fsm, root)) continue;
|
||||||
// FSMs, die Größen/Positionen setzen (z.B. Füllstands-Anzeigen), NICHT anfassen: ein übertragener Wert
|
// FSMs, die Größen/Positionen setzen (z.B. Füllstands-Anzeigen), NICHT anfassen: ein übertragener Wert
|
||||||
// kann dort eine Größe 0 ergeben - Unity stürzt dann in der Physik ab ("cleaning the mesh failed")
|
// kann dort eine Größe 0 ergeben - Unity stürzt dann in der Physik ab ("cleaning the mesh failed")
|
||||||
if (TouchesTransform(fsm)) continue;
|
if (TouchesTransform(fsm)) continue;
|
||||||
@@ -206,6 +224,91 @@ namespace MSCO
|
|||||||
if (vars.Count != before) MscoLog.Info("[MSCO] Vehicle state: " + vars.Count + " values synced");
|
if (vars.Count != before) MscoLog.Info("[MSCO] Vehicle state: " + vars.Count + " values synced");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Schrauben, deren Stufen etwas ein-/ausschalten (ActivateGameObject - z.B. die Ablassschraube: Stufe 0-5 = das
|
||||||
|
/// Leck "Oil" ist an). Sonst laufen Schrauben über FsmSync (Ereignisse): ging eins verloren, war die Ablassschraube
|
||||||
|
/// bei einem offen und beim anderen zu - bei ihm lief dann endlos Öl aus (der Ölstand kam ja ständig nach).
|
||||||
|
/// </summary>
|
||||||
|
/// <summary>
|
||||||
|
/// Festigkeit eines Teils (BoltCheck bzw. bei aufgeschraubten Teilen wie dem Ölfilter "Use"): zählt das Werkzeug
|
||||||
|
/// beim Schrauben hoch - beim Mitspieler drehte sich bisher nur die Schraube, das Teil blieb "lose" bzw. "fest".
|
||||||
|
/// </summary>
|
||||||
|
void RegisterTightness(PlayMakerFSM fsm, string root)
|
||||||
|
{
|
||||||
|
if (fsm.FsmName != "BoltCheck" && fsm.FsmName != "Use") return;
|
||||||
|
FsmFloat tightness = fsm.FsmVariables.FindFsmFloat("Tightness");
|
||||||
|
if (tightness == null) return;
|
||||||
|
if (fsm.FsmName == "Use" && fsm.gameObject.GetComponents<PlayMakerFSM>().Length > 0)
|
||||||
|
{
|
||||||
|
bool screwed = false;
|
||||||
|
foreach (PlayMakerFSM other in fsm.gameObject.GetComponents<PlayMakerFSM>()) if (other.FsmName == "Screw") screwed = true;
|
||||||
|
if (!screwed) return;
|
||||||
|
}
|
||||||
|
string name = FsmSync.PathOf(fsm.transform) + "|" + fsm.FsmName + "|Tightness";
|
||||||
|
int key = Hash(name);
|
||||||
|
Var existing;
|
||||||
|
if (vars.TryGetValue(key, out existing) && existing.Variable == tightness) return;
|
||||||
|
var v = new Var { Key = key, Name = name, Root = root, Variable = tightness, Type = 1, Fsm = fsm, Exact = true };
|
||||||
|
Remember(v);
|
||||||
|
vars[key] = v;
|
||||||
|
List<int> keys;
|
||||||
|
if (!keysByFsm.TryGetValue(fsm, out keys)) keysByFsm[fsm] = keys = new List<int>();
|
||||||
|
keys.Add(key);
|
||||||
|
byte[] stored;
|
||||||
|
if (pending.TryGetValue(key, out stored))
|
||||||
|
{
|
||||||
|
pending.Remove(key);
|
||||||
|
byte[] value = new byte[stored.Length - 1];
|
||||||
|
Array.Copy(stored, 1, value, 0, value.Length);
|
||||||
|
Apply(v, stored[0], value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Jede Schraube (FSM "Screw" mit Stufe 0-8): Stufe als Zustand übertragen, angewendet über die Stufen-Logik der
|
||||||
|
/// Schraube. Schrauben, die etwas schalten (Ablassschraube), gibt die Welt-Autorität nach dem Beitreten einmal vor.
|
||||||
|
/// </summary>
|
||||||
|
bool RegisterBolt(PlayMakerFSM fsm, string root)
|
||||||
|
{
|
||||||
|
if (fsm.FsmName != "Screw") return false;
|
||||||
|
FsmInt stage = fsm.FsmVariables.FindFsmInt("Stage");
|
||||||
|
if (stage == null) return false;
|
||||||
|
FsmState dispatch = fsm.Fsm.GetState("Stages 2") ?? fsm.Fsm.GetState("Stages");
|
||||||
|
if (dispatch == null) return false;
|
||||||
|
bool switches = false;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
foreach (FsmState state in fsm.FsmStates)
|
||||||
|
{
|
||||||
|
if (!state.ActionsLoaded) state.LoadActions();
|
||||||
|
if (state.Actions == null) continue;
|
||||||
|
foreach (FsmStateAction a in state.Actions)
|
||||||
|
if (a != null && a.GetType().Name == "ActivateGameObject") { switches = true; break; }
|
||||||
|
if (switches) break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch { return false; }
|
||||||
|
string name = FsmSync.PathOf(fsm.transform) + "|" + fsm.FsmName + "|Stage";
|
||||||
|
int key = Hash(name);
|
||||||
|
Var existing;
|
||||||
|
if (vars.TryGetValue(key, out existing) && existing.Variable == stage) return true;
|
||||||
|
var v = new Var { Key = key, Name = name, Root = root, Variable = stage, Type = 2, Fsm = fsm, BoltDispatch = dispatch, Functional = switches, Exact = true };
|
||||||
|
Remember(v);
|
||||||
|
vars[key] = v;
|
||||||
|
List<int> keys;
|
||||||
|
if (!keysByFsm.TryGetValue(fsm, out keys)) keysByFsm[fsm] = keys = new List<int>();
|
||||||
|
keys.Add(key);
|
||||||
|
byte[] stored;
|
||||||
|
if (pending.TryGetValue(key, out stored))
|
||||||
|
{
|
||||||
|
pending.Remove(key);
|
||||||
|
byte[] value = new byte[stored.Length - 1];
|
||||||
|
Array.Copy(stored, 1, value, 0, value.Length);
|
||||||
|
Apply(v, stored[0], value);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
static readonly string[] TransformActionWords = { "Scale", "Position", "Rotate", "Rotation", "Translate", "Parent" };
|
static readonly string[] TransformActionWords = { "Scale", "Position", "Rotate", "Rotation", "Translate", "Parent" };
|
||||||
|
|
||||||
static bool TouchesTransform(PlayMakerFSM fsm)
|
static bool TouchesTransform(PlayMakerFSM fsm)
|
||||||
@@ -368,9 +471,22 @@ namespace MSCO
|
|||||||
case 1:
|
case 1:
|
||||||
float f = BitConverter.ToSingle(value, 0);
|
float f = BitConverter.ToSingle(value, 0);
|
||||||
if (float.IsNaN(f) || float.IsInfinity(f)) return; // nie ungültige Werte übernehmen
|
if (float.IsNaN(f) || float.IsInfinity(f)) return; // nie ungültige Werte übernehmen
|
||||||
|
bool tighter = v.Exact && v.Fsm != null && v.Fsm.FsmName == "BoltCheck" && f > GetFloat(v) + 0.01f;
|
||||||
SetFloat(v, f);
|
SetFloat(v, f);
|
||||||
|
if (tighter) VehicleAdmin.SnapToPivot(v.Fsm.gameObject); // vom Mitspieler festgezogen: Teil auf den Halter
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
int n = BitConverter.ToInt32(value, 0);
|
||||||
|
if (v.BoltDispatch != null)
|
||||||
|
{
|
||||||
|
if (n < 0 || n > 8 || ((FsmInt)v.Variable).Value == n) { Remember(v); return; }
|
||||||
|
((FsmInt)v.Variable).Value = n;
|
||||||
|
if (v.Fsm != null && v.Fsm.gameObject.activeInHierarchy) FsmSync.SwitchStateSilently(v.Fsm, v.BoltDispatch);
|
||||||
|
if (FsmSync.Debug) MscoLog.Info("[MSCO] << BOLT " + v.Name + " -> " + n);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
((FsmInt)v.Variable).Value = n;
|
||||||
break;
|
break;
|
||||||
case 2: ((FsmInt)v.Variable).Value = BitConverter.ToInt32(value, 0); break;
|
|
||||||
default: ((FsmBool)v.Variable).Value = value[0] != 0; break;
|
default: ((FsmBool)v.Variable).Value = value[0] != 0; break;
|
||||||
}
|
}
|
||||||
Remember(v); // nicht als eigene Änderung zurückschicken
|
Remember(v); // nicht als eigene Änderung zurückschicken
|
||||||
|
|||||||
+217
@@ -0,0 +1,217 @@
|
|||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using HutongGames.PlayMaker;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace MSCO
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Admin-Menü, Reiter WORLD: Uhrzeit und Wetter umstellen. Beides läuft im Spiel über PlayMaker:
|
||||||
|
/// - Uhr: die Sonnen-FSM hat je 2 Stunden einen Zustand ("00-02" ... "22-00") und die Zahl "Time"; beim Laden
|
||||||
|
/// setzt das Spiel "Time" und wählt in "State 1" den passenden Zustand - genau so stellen wir die Zeit.
|
||||||
|
/// - Wetter: die FSM "Weather" (Wolkenfront) mit den Zuständen "No weather", "Rain" und "Thunder"; bei Regen
|
||||||
|
/// wird die Front über den Admin gesetzt, damit es dort auch regnet.
|
||||||
|
/// Die anderen Spieler bekommen den Befehl über eine MSCO-Nachricht und führen ihn genauso aus.
|
||||||
|
/// </summary>
|
||||||
|
public static class WorldAdmin
|
||||||
|
{
|
||||||
|
public const string MessageId = "msco.world";
|
||||||
|
public const byte KindHour = 1, KindWeather = 2;
|
||||||
|
|
||||||
|
public static readonly string[] WeatherStates = { "No weather", "Rain", "Thunder" };
|
||||||
|
public static readonly string[] WeatherEvents = { "NORAIN", "RAIN", "THUNDER" };
|
||||||
|
public static readonly string[] WeatherNames = { "clear", "rain", "thunder" };
|
||||||
|
|
||||||
|
static PlayMakerFSM sun, weather;
|
||||||
|
static float searchedAt = -100f;
|
||||||
|
|
||||||
|
static void Find()
|
||||||
|
{
|
||||||
|
if (sun != null && weather != null) 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(60f))
|
||||||
|
{
|
||||||
|
if (f == null) continue;
|
||||||
|
if (sun == null && f.FsmVariables.FindFsmInt("Time") != null && f.Fsm.GetState("12-14") != null
|
||||||
|
&& f.Fsm.GetState("22-00") != null && f.Fsm.GetState("State 1") != null && f.Fsm.GetState("Next day") != null)
|
||||||
|
sun = f;
|
||||||
|
if (weather == null && f.FsmName == "Weather" && f.Fsm.GetState("No weather") != null && f.Fsm.GetState("Rain") != null)
|
||||||
|
weather = f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------- Anzeige
|
||||||
|
|
||||||
|
/// <summary>Aktueller 2-Stunden-Block der Uhr (Startstunde) oder -1.</summary>
|
||||||
|
public static int CurrentHour
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
Find();
|
||||||
|
string s = sun != null ? sun.ActiveStateName : null;
|
||||||
|
int h;
|
||||||
|
if (s != null && s.Length == 5 && s[2] == '-' && int.TryParse(s.Substring(0, 2), out h)) return h;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Aktuelles Wetter (0 klar, 1 Regen, 2 Gewitter) oder -1.</summary>
|
||||||
|
public static int CurrentWeather
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
Find();
|
||||||
|
if (weather == null) return -1;
|
||||||
|
FsmInt type = weather.FsmVariables.FindFsmInt("WeatherType");
|
||||||
|
if (type == null) return -1;
|
||||||
|
FsmState check = weather.Fsm.GetState("State 2");
|
||||||
|
for (int i = 0; i < WeatherEvents.Length; i++)
|
||||||
|
{
|
||||||
|
int value;
|
||||||
|
if (check != null && IntCompareValue(check, WeatherEvents[i], out value) && value == type.Value) return i;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------- Ausführen
|
||||||
|
|
||||||
|
/// <summary>Befehl lokal ausführen. Gibt null zurück oder einen Fehlertext.</summary>
|
||||||
|
public static string Apply(byte kind, int value, Vector3 position)
|
||||||
|
{
|
||||||
|
Find();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (kind == KindHour) return SetHour(value);
|
||||||
|
if (kind == KindWeather) return SetWeather(value, position);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
MscoLog.Error("[MSCO] world admin: " + e);
|
||||||
|
return "failed: " + e.Message;
|
||||||
|
}
|
||||||
|
return "unknown command";
|
||||||
|
}
|
||||||
|
|
||||||
|
static string SetHour(int hour)
|
||||||
|
{
|
||||||
|
if (sun == null) return "Clock not found.";
|
||||||
|
int start = ((hour % 24 + 24) % 24) / 2 * 2;
|
||||||
|
string block = start.ToString("00") + "-" + ((start + 2) % 24).ToString("00");
|
||||||
|
FsmState target = sun.Fsm.GetState(block);
|
||||||
|
if (target == null) return "Clock state " + block + " not found.";
|
||||||
|
|
||||||
|
// wie beim Laden: "Time" setzen, "State 1" wählt daraus den Zustand (und stellt Licht/Farben ein)
|
||||||
|
FsmState dispatch = sun.Fsm.GetState("State 1");
|
||||||
|
int timeValue;
|
||||||
|
if (dispatch != null && IntCompareValue(dispatch, block, out timeValue))
|
||||||
|
{
|
||||||
|
sun.FsmVariables.FindFsmInt("Time").Value = timeValue;
|
||||||
|
FsmSync.SwitchStateSilently(sun, dispatch);
|
||||||
|
}
|
||||||
|
else FsmSync.SwitchStateSilently(sun, target);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static string SetWeather(int kind, Vector3 position)
|
||||||
|
{
|
||||||
|
if (weather == null) return "Weather not found.";
|
||||||
|
if (kind < 0 || kind >= WeatherStates.Length) return "Unknown weather.";
|
||||||
|
FsmState state = weather.Fsm.GetState(WeatherStates[kind]);
|
||||||
|
if (state == null) return "Weather state not found.";
|
||||||
|
FsmSync.SwitchStateSilently(weather, state);
|
||||||
|
if (kind > 0) MoveFrontOver(position);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Wolkenfront über eine Stelle setzen: "Move clouds" setzt jedes Bild die Position aus PosX/PosZ - also diese
|
||||||
|
/// beiden so wählen, dass die Front über der Stelle steht (Koordinatenraum aus der SetPosition-Aktion).
|
||||||
|
/// </summary>
|
||||||
|
static void MoveFrontOver(Vector3 position)
|
||||||
|
{
|
||||||
|
FsmState move = weather.Fsm.GetState("Move clouds");
|
||||||
|
if (move == null) return;
|
||||||
|
if (!move.ActionsLoaded) move.LoadActions();
|
||||||
|
if (move.Actions == null) return;
|
||||||
|
foreach (FsmStateAction a in move.Actions)
|
||||||
|
{
|
||||||
|
if (a == null || a.GetType().Name != "SetPosition") continue;
|
||||||
|
Type t = a.GetType();
|
||||||
|
var owner = Field(t, a, "gameObject") as FsmOwnerDefault;
|
||||||
|
var x = Field(t, a, "x") as FsmFloat;
|
||||||
|
var z = Field(t, a, "z") as FsmFloat;
|
||||||
|
object space = Field(t, a, "space");
|
||||||
|
GameObject go = owner != null ? weather.Fsm.GetOwnerDefaultTarget(owner) : null;
|
||||||
|
if (go == null) continue;
|
||||||
|
|
||||||
|
bool local = space is Space && (Space)space == Space.Self && go.transform.parent != null;
|
||||||
|
Vector3 p = local ? go.transform.parent.InverseTransformPoint(position) : position;
|
||||||
|
SetVar(x, p.x);
|
||||||
|
SetVar(z, p.z);
|
||||||
|
go.transform.position = new Vector3(position.x, go.transform.position.y, position.z);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void SetVar(FsmFloat field, float value)
|
||||||
|
{
|
||||||
|
if (field == null || field.IsNone || string.IsNullOrEmpty(field.Name)) return;
|
||||||
|
FsmFloat v = weather.FsmVariables.FindFsmFloat(field.Name);
|
||||||
|
if (v != null) v.Value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
static object Field(Type t, object o, string name)
|
||||||
|
{
|
||||||
|
var f = t.GetField(name);
|
||||||
|
return f != null ? f.GetValue(o) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>In einem Verteiler-Zustand die Zahl finden, bei der IntCompare das Event auslöst ("gleich").</summary>
|
||||||
|
static bool IntCompareValue(FsmState state, string eventName, out int value)
|
||||||
|
{
|
||||||
|
value = 0;
|
||||||
|
if (!state.ActionsLoaded) state.LoadActions();
|
||||||
|
if (state.Actions == null) return false;
|
||||||
|
foreach (FsmStateAction a in state.Actions)
|
||||||
|
{
|
||||||
|
if (a == null || a.GetType().Name != "IntCompare") continue;
|
||||||
|
Type t = a.GetType();
|
||||||
|
var equal = Field(t, a, "equal") as FsmEvent;
|
||||||
|
var number = Field(t, a, "integer2") as FsmInt;
|
||||||
|
if (equal == null || number == null || equal.Name != eventName) continue;
|
||||||
|
value = number.Value;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------- Netz
|
||||||
|
|
||||||
|
public static byte[] Build(byte kind, int value, Vector3 position)
|
||||||
|
{
|
||||||
|
using (var ms = new MemoryStream())
|
||||||
|
using (var w = new BinaryWriter(ms))
|
||||||
|
{
|
||||||
|
w.Write(kind); w.Write(value);
|
||||||
|
w.Write(position.x); w.Write(position.y); w.Write(position.z);
|
||||||
|
w.Flush();
|
||||||
|
return ms.ToArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Receive(byte[] data)
|
||||||
|
{
|
||||||
|
if (!MenuHelper.InGame || data == null || data.Length < 17) return;
|
||||||
|
using (var r = new PacketReader(data))
|
||||||
|
{
|
||||||
|
byte kind = r.ReadByte();
|
||||||
|
int value = r.ReadInt32();
|
||||||
|
var pos = new Vector3(r.ReadSingle(), r.ReadSingle(), r.ReadSingle());
|
||||||
|
string error = Apply(kind, value, pos);
|
||||||
|
if (error != null) MscoLog.Warning("[MSCO] world admin from other player: " + error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,7 +2,6 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using HutongGames.PlayMaker;
|
using HutongGames.PlayMaker;
|
||||||
using MSCLoader;
|
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
|
|
||||||
namespace MSCO
|
namespace MSCO
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
using System.Reflection;
|
||||||
|
using System.Text;
|
||||||
|
using MSCO;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
[assembly: AssemblyTitle("Hello Mod")]
|
||||||
|
[assembly: AssemblyProduct("Hello Mod")]
|
||||||
|
[assembly: AssemblyVersion("1.0.0.0")]
|
||||||
|
[assembly: AssemblyFileVersion("1.0.0.0")]
|
||||||
|
[assembly: AssemblyInformationalVersion("1.0.0")]
|
||||||
|
|
||||||
|
namespace HelloMod
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Beispiel-Mod: zeigt im Spiel oben links einen Hinweis. Mit H winkst du - alle anderen Spieler, die die Mod auch
|
||||||
|
/// haben, sehen "<Name> waves!".
|
||||||
|
/// </summary>
|
||||||
|
public class HelloMod : MscoMod
|
||||||
|
{
|
||||||
|
public override string ID { get { return "hellomod"; } }
|
||||||
|
public override string Name { get { return "Hello Mod"; } }
|
||||||
|
public override string Version { get { return "1.0.0"; } }
|
||||||
|
public override string Author { get { return "MBM"; } }
|
||||||
|
|
||||||
|
bool inGame;
|
||||||
|
string lastWave = "";
|
||||||
|
float waveUntil;
|
||||||
|
|
||||||
|
public override void OnLoad() { Log("loaded"); }
|
||||||
|
public override void OnMenuLoad() { inGame = false; }
|
||||||
|
public override void OnGameLoad() { inGame = true; }
|
||||||
|
|
||||||
|
public override void Update()
|
||||||
|
{
|
||||||
|
if (!inGame || !IsInSession || !Input.GetKeyDown(KeyCode.H)) return;
|
||||||
|
Send(Encoding.UTF8.GetBytes("wave"));
|
||||||
|
Show("You wave!");
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void OnMessage(int fromPlayerId, byte[] data)
|
||||||
|
{
|
||||||
|
if (Encoding.UTF8.GetString(data) != "wave") return;
|
||||||
|
string name;
|
||||||
|
Show((Players.TryGetValue(fromPlayerId, out name) ? name : "Someone") + " waves!");
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void OnPlayerJoined(int playerId, string name) { Show(name + " joined - press H to wave"); }
|
||||||
|
|
||||||
|
void Show(string text)
|
||||||
|
{
|
||||||
|
lastWave = text;
|
||||||
|
waveUntil = Time.realtimeSinceStartup + 4f;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void OnGUI()
|
||||||
|
{
|
||||||
|
if (!inGame) return;
|
||||||
|
GUI.Label(new Rect(20, 60, 400, 24), "Hello Mod: press H to wave");
|
||||||
|
if (Time.realtimeSinceStartup < waveUntil) GUI.Label(new Rect(20, 84, 400, 24), lastWave);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!--
|
||||||
|
Beispiel-Mod für MSCO. Bauen: msbuild HelloMod.csproj -p:Configuration=Release
|
||||||
|
Ergebnis bin\Release\HelloMod.dll nach <Spielordner>\MSCO\Mods kopieren (oder auf dem Server nach data/mods).
|
||||||
|
GamePath anpassen, wenn My Summer Car woanders liegt.
|
||||||
|
-->
|
||||||
|
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||||
|
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
|
||||||
|
<PropertyGroup>
|
||||||
|
<Configuration Condition=" '$(Configuration)' == '' ">Release</Configuration>
|
||||||
|
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
|
||||||
|
<OutputType>Library</OutputType>
|
||||||
|
<RootNamespace>HelloMod</RootNamespace>
|
||||||
|
<AssemblyName>HelloMod</AssemblyName>
|
||||||
|
<TargetFrameworkVersion>v3.5</TargetFrameworkVersion>
|
||||||
|
<TargetFrameworkProfile>Unity Full v3.5</TargetFrameworkProfile>
|
||||||
|
<LangVersion>13</LangVersion>
|
||||||
|
<OutputPath>bin\$(Configuration)\</OutputPath>
|
||||||
|
<DebugType>none</DebugType>
|
||||||
|
<Optimize>true</Optimize>
|
||||||
|
<GamePath Condition=" '$(GamePath)' == '' ">F:\SteamLibrary\steamapps\common\My Summer Car</GamePath>
|
||||||
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Reference Include="System" />
|
||||||
|
<Reference Include="UnityEngine">
|
||||||
|
<HintPath>$(GamePath)\mysummercar_Data\Managed\UnityEngine.dll</HintPath>
|
||||||
|
<Private>False</Private>
|
||||||
|
</Reference>
|
||||||
|
<!-- MSCO selbst (die Mod-Schnittstelle MscoMod) -->
|
||||||
|
<Reference Include="MSCO">
|
||||||
|
<HintPath>$(GamePath)\MSCO\MSCO.dll</HintPath>
|
||||||
|
<Private>False</Private>
|
||||||
|
</Reference>
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Compile Include="HelloMod.cs" />
|
||||||
|
</ItemGroup>
|
||||||
|
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,269 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Reflection;
|
||||||
|
using HutongGames.PlayMaker;
|
||||||
|
using MSCO;
|
||||||
|
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.4.0")]
|
||||||
|
[assembly: AssemblyFileVersion("1.0.4.0")]
|
||||||
|
[assembly: AssemblyInformationalVersion("1.0.4")]
|
||||||
|
|
||||||
|
namespace ShowBoltSize
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Zeigt unter dem Fadenkreuz die Größe der Schraube, auf die man schaut ("10 mm"). Jede Schraube hat die FSM "Screw"
|
||||||
|
/// mit der Variable "BoltSize". Deren Kollider sind nur mit Werkzeug in der Hand aktiv - deshalb kein Raycast, sondern:
|
||||||
|
/// die Schraube, die am nächsten an der Blickrichtung liegt (bis 1,3 m). Nur Anzeige, ändert nichts am Spiel.
|
||||||
|
/// </summary>
|
||||||
|
public class ShowBoltSize : MscoMod
|
||||||
|
{
|
||||||
|
public override string ID { get { return "showboltsize"; } }
|
||||||
|
public override string Name { get { return "Show Bolt Size"; } }
|
||||||
|
public override string Version { get { return "1.0.4"; } }
|
||||||
|
public override string Author { get { return "MBM"; } }
|
||||||
|
|
||||||
|
const float MaxDistance = 1.5f;
|
||||||
|
const float AimTolerance = 0.05f; // so weit (m) darf die Schraube neben der Blicklinie liegen
|
||||||
|
const float ScanInterval = 5f;
|
||||||
|
|
||||||
|
sealed class Bolt
|
||||||
|
{
|
||||||
|
public Transform Transform;
|
||||||
|
public Renderer Renderer; // sichtbarer Schraubenkopf (Kind-Objekt) - sein Mittelpunkt statt des Drehpunkts
|
||||||
|
public FsmFloat SizeF;
|
||||||
|
public FsmInt SizeI;
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly List<Bolt> bolts = new List<Bolt>();
|
||||||
|
bool inGame;
|
||||||
|
float nextScan;
|
||||||
|
string label;
|
||||||
|
GUIStyle style;
|
||||||
|
Camera camera;
|
||||||
|
bool reported;
|
||||||
|
int diagnostics; // die ersten Male ins Log schreiben, was in Blickrichtung liegt
|
||||||
|
float nextDiagnostic;
|
||||||
|
bool shownOnce;
|
||||||
|
|
||||||
|
// Das Spiel selbst: Schraubenschlüssel/Ratsche in der Hand -> FPSCamera/2Spanner/Raycast, FSM "Check" mit "BoltSize"
|
||||||
|
// (die Größe der Schraube, auf die das Werkzeug gerade zielt). Die Variable "BoltSize" der Schrauben ist oft 0.
|
||||||
|
PlayMakerFSM toolCheck;
|
||||||
|
FsmFloat toolSize;
|
||||||
|
FsmInt toolSizeI;
|
||||||
|
float lastToolSize = -1f;
|
||||||
|
int toolLogs;
|
||||||
|
bool dumped;
|
||||||
|
float dumpAt = -1f;
|
||||||
|
|
||||||
|
public override void OnMenuLoad() { inGame = false; bolts.Clear(); label = null; camera = null; reported = false; toolCheck = null; toolSize = null; toolSizeI = null; }
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Vector3 Center(Bolt b) { return b.Renderer != null ? b.Renderer.bounds.center : b.Transform.position; }
|
||||||
|
|
||||||
|
public override void Update()
|
||||||
|
{
|
||||||
|
label = null;
|
||||||
|
if (!inGame) return;
|
||||||
|
Camera cam = FindCamera();
|
||||||
|
if (cam == null) return;
|
||||||
|
if (Time.realtimeSinceStartup >= nextScan) Scan(cam);
|
||||||
|
float fromTool = ToolSize(cam);
|
||||||
|
|
||||||
|
// Die Schraube, die am nächsten an der Blicklinie liegt (Abstand zur Linie, nicht Winkel)
|
||||||
|
Transform eye = cam.transform;
|
||||||
|
Vector3 eyePos = eye.position, forward = eye.forward;
|
||||||
|
Bolt best = null, nearest = null;
|
||||||
|
float bestOff = float.MaxValue, nearestOff = float.MaxValue;
|
||||||
|
for (int i = 0; i < bolts.Count; i++)
|
||||||
|
{
|
||||||
|
Bolt b = bolts[i];
|
||||||
|
if (b.Transform == null || !b.Transform.gameObject.activeInHierarchy) continue;
|
||||||
|
Vector3 to = Center(b) - eyePos;
|
||||||
|
float along = Vector3.Dot(to, forward);
|
||||||
|
if (along < 0.05f || along > MaxDistance + 0.5f) continue;
|
||||||
|
float off = Vector3.Cross(forward, to).magnitude; // Abstand zur Blicklinie
|
||||||
|
if (off < nearestOff) { nearestOff = off; nearest = b; }
|
||||||
|
if (along <= MaxDistance && off < AimTolerance && off < bestOff) { bestOff = off; best = b; }
|
||||||
|
}
|
||||||
|
if (fromTool > 0f)
|
||||||
|
{
|
||||||
|
label = Format(fromTool); // was das Spiel für das Werkzeug ermittelt - immer richtig
|
||||||
|
}
|
||||||
|
else if (best != null)
|
||||||
|
{
|
||||||
|
label = Describe(best);
|
||||||
|
if (!shownOnce) { shownOnce = true; Log("first bolt: " + Info(best, eyePos, forward) + " -> " + label); }
|
||||||
|
}
|
||||||
|
else if (nearest != null && diagnostics < 8 && Time.realtimeSinceStartup >= nextDiagnostic)
|
||||||
|
{
|
||||||
|
// Hilft bei der Fehlersuche: was liegt in Blickrichtung, warum wird nichts angezeigt?
|
||||||
|
nextDiagnostic = Time.realtimeSinceStartup + 4f;
|
||||||
|
if (nearestOff < 0.3f) { diagnostics++; Log("nearest bolt (not shown): " + Info(nearest, eyePos, forward)); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Größe aus dem Werkzeug des Spielers (nur mit Schraubenschlüssel/Ratsche in der Hand), sonst 0. Beim ersten Mal
|
||||||
|
/// wird außerdem einmal die Werkzeug-FSM in MSCOsmdump.txt geschrieben (Fehlersuche).
|
||||||
|
/// </summary>
|
||||||
|
float ToolSize(Camera cam)
|
||||||
|
{
|
||||||
|
if (toolCheck == null)
|
||||||
|
{
|
||||||
|
// Werkzeug-FSM suchen - auch inaktive Objekte (ohne Werkzeug in der Hand ist sie aus), höchstens alle 5 s
|
||||||
|
if (Time.realtimeSinceStartup < nextToolSearch) return 0f;
|
||||||
|
nextToolSearch = Time.realtimeSinceStartup + 5f;
|
||||||
|
toolCheck = FindToolCheck(cam);
|
||||||
|
if (toolCheck == null)
|
||||||
|
{
|
||||||
|
if (!toolMissingLogged) { toolMissingLogged = true; Log("tool FSM (Spanner/Raycast 'Check') not found yet"); }
|
||||||
|
return 0f;
|
||||||
|
}
|
||||||
|
toolSize = toolCheck.FsmVariables.FindFsmFloat("BoltSize");
|
||||||
|
toolSizeI = toolCheck.FsmVariables.FindFsmInt("BoltSize");
|
||||||
|
Log("tool found: " + PathOf(toolCheck.transform) + " / " + toolCheck.FsmName + (toolSize != null ? " with float BoltSize" : toolSizeI != null ? " with int BoltSize" : " without BoltSize"));
|
||||||
|
}
|
||||||
|
if (!toolCheck.gameObject.activeInHierarchy || !toolCheck.enabled) return 0f;
|
||||||
|
if (!dumped)
|
||||||
|
{
|
||||||
|
if (dumpAt < 0f) dumpAt = Time.realtimeSinceStartup + 2f; // erst laufen lassen, dann sind die Aktionen geladen
|
||||||
|
else if (Time.realtimeSinceStartup >= dumpAt)
|
||||||
|
{
|
||||||
|
dumped = true;
|
||||||
|
try { Log("tool dump: " + FsmDump.Write("spanner#0|raycast")); }
|
||||||
|
catch (System.Exception e) { Log("tool dump failed: " + e.Message); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
float value = toolSize != null ? toolSize.Value : toolSizeI != null ? toolSizeI.Value : 0f;
|
||||||
|
if (value != lastToolSize && toolLogs < 12)
|
||||||
|
{
|
||||||
|
lastToolSize = value;
|
||||||
|
toolLogs++;
|
||||||
|
FsmFloat wrench = FsmVariables.GlobalVariables.FindFsmFloat("ToolWrenchSize");
|
||||||
|
Log("tool BoltSize=" + value + " (state " + toolCheck.ActiveStateName + ", wrench size " + (wrench != null ? wrench.Value.ToString() : "?") + ")");
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
float nextToolSearch;
|
||||||
|
bool toolMissingLogged;
|
||||||
|
|
||||||
|
/// <summary>"Check"-FSM unter ...Spanner/Raycast (Schraubenschlüssel/Ratsche des Spielers), auch wenn sie gerade aus ist.</summary>
|
||||||
|
static PlayMakerFSM FindToolCheck(Camera cam)
|
||||||
|
{
|
||||||
|
Transform raycast = cam.transform.Find("2Spanner/Raycast");
|
||||||
|
if (raycast != null)
|
||||||
|
foreach (PlayMakerFSM f in raycast.GetComponents<PlayMakerFSM>())
|
||||||
|
if (f.FsmName == "Check") return f;
|
||||||
|
foreach (PlayMakerFSM f in Resources.FindObjectsOfTypeAll<PlayMakerFSM>())
|
||||||
|
{
|
||||||
|
if (f == null || f.FsmName != "Check" || f.gameObject.hideFlags != HideFlags.None) continue;
|
||||||
|
Transform t = f.transform;
|
||||||
|
if (t.name == "Raycast" && t.parent != null && t.parent.name.IndexOf("Spanner", System.StringComparison.OrdinalIgnoreCase) >= 0) return f;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static string PathOf(Transform t)
|
||||||
|
{
|
||||||
|
string path = t.name;
|
||||||
|
for (Transform p = t.parent; p != null; p = p.parent) path = p.name + "/" + path;
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Alle Zahlen-Variablen der FSMs auf der Schraube und darunter (wo steckt die Größe?).</summary>
|
||||||
|
static string Numbers(Transform t)
|
||||||
|
{
|
||||||
|
var sb = new System.Text.StringBuilder();
|
||||||
|
foreach (PlayMakerFSM f in t.GetComponentsInChildren<PlayMakerFSM>(true))
|
||||||
|
{
|
||||||
|
sb.Append(" [").Append(f.name).Append('/').Append(f.FsmName);
|
||||||
|
foreach (FsmFloat v in f.FsmVariables.FloatVariables) sb.Append(' ').Append(v.Name).Append('=').Append(v.Value.ToString("0.###"));
|
||||||
|
foreach (FsmInt v in f.FsmVariables.IntVariables) sb.Append(' ').Append(v.Name).Append('=').Append(v.Value);
|
||||||
|
sb.Append(']');
|
||||||
|
}
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
static string Info(Bolt b, Vector3 eyePos, Vector3 forward)
|
||||||
|
{
|
||||||
|
Vector3 to = Center(b) - eyePos;
|
||||||
|
float raw = b.SizeF != null ? b.SizeF.Value : (b.SizeI != null ? b.SizeI.Value : -1);
|
||||||
|
return b.Transform.name + " under " + (b.Transform.parent != null ? b.Transform.parent.name : "-")
|
||||||
|
+ ", distance " + Vector3.Dot(to, forward).ToString("0.00") + " m, off the line " + (Vector3.Cross(forward, to).magnitude * 100f).ToString("0") + " cm"
|
||||||
|
+ ", angle " + Vector3.Angle(forward, to).ToString("0.0") + "°, BoltSize=" + raw + (b.Renderer != null ? "" : " (no renderer)")
|
||||||
|
+ ", scale " + b.Transform.localScale.ToString("0.###") + " / world " + b.Transform.lossyScale.ToString("0.###")
|
||||||
|
+ (b.Renderer != null ? ", mesh size " + (b.Renderer.bounds.size * 1000f).ToString("0") + " mm" : "")
|
||||||
|
+ ", FSMs:" + Numbers(b.Transform);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <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 SceneCache.Fsms(30f))
|
||||||
|
{
|
||||||
|
if (fsm == null || fsm.FsmName != "Screw") continue;
|
||||||
|
FsmFloat f = fsm.FsmVariables.FindFsmFloat("BoltSize");
|
||||||
|
FsmInt i = fsm.FsmVariables.FindFsmInt("BoltSize");
|
||||||
|
if (f == null && i == null) continue;
|
||||||
|
bolts.Add(new Bolt { Transform = fsm.transform, SizeF = f, SizeI = i, Renderer = fsm.GetComponentInChildren<Renderer>() });
|
||||||
|
}
|
||||||
|
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>
|
||||||
|
static string Describe(Bolt b)
|
||||||
|
{
|
||||||
|
float value = b.SizeF != null ? b.SizeF.Value : b.SizeI.Value;
|
||||||
|
if (value <= 0f) return "? mm"; // Größe unbekannt - aber die Schraube wurde erkannt
|
||||||
|
return Format(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
static string Format(float value)
|
||||||
|
{
|
||||||
|
float mm = value < 2.5f ? value * 10f : value;
|
||||||
|
return Mathf.RoundToInt(mm) + " mm";
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void OnGUI()
|
||||||
|
{
|
||||||
|
if (label == null) return;
|
||||||
|
if (style == null)
|
||||||
|
{
|
||||||
|
style = new GUIStyle(GUI.skin.label) { alignment = TextAnchor.MiddleCenter, fontSize = 16, fontStyle = FontStyle.Bold };
|
||||||
|
style.normal.textColor = Color.white;
|
||||||
|
}
|
||||||
|
var r = new Rect(Screen.width / 2f - 50f, Screen.height / 2f + 22f, 100f, 26f);
|
||||||
|
Color old = GUI.color;
|
||||||
|
GUI.color = new Color(0f, 0f, 0f, 0.6f);
|
||||||
|
GUI.DrawTexture(r, Texture2D.whiteTexture);
|
||||||
|
GUI.color = old;
|
||||||
|
GUI.Label(r, label, style);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!--
|
||||||
|
MSCO-Mod "Show Bolt Size". Bauen: msbuild ShowBoltSize.csproj -p:Configuration=Release
|
||||||
|
Ergebnis bin\Release\ShowBoltSize.dll - im Launcher unter Mods hochladen oder nach <Spielordner>\MSCO\Mods kopieren.
|
||||||
|
GamePath anpassen, wenn My Summer Car woanders liegt.
|
||||||
|
-->
|
||||||
|
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||||
|
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
|
||||||
|
<PropertyGroup>
|
||||||
|
<Configuration Condition=" '$(Configuration)' == '' ">Release</Configuration>
|
||||||
|
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
|
||||||
|
<OutputType>Library</OutputType>
|
||||||
|
<RootNamespace>ShowBoltSize</RootNamespace>
|
||||||
|
<AssemblyName>ShowBoltSize</AssemblyName>
|
||||||
|
<TargetFrameworkVersion>v3.5</TargetFrameworkVersion>
|
||||||
|
<TargetFrameworkProfile>Unity Full v3.5</TargetFrameworkProfile>
|
||||||
|
<LangVersion>13</LangVersion>
|
||||||
|
<OutputPath>bin\$(Configuration)\</OutputPath>
|
||||||
|
<DebugType>none</DebugType>
|
||||||
|
<Optimize>true</Optimize>
|
||||||
|
<GamePath Condition=" '$(GamePath)' == '' ">F:\SteamLibrary\steamapps\common\My Summer Car</GamePath>
|
||||||
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Reference Include="System" />
|
||||||
|
<Reference Include="UnityEngine">
|
||||||
|
<HintPath>$(GamePath)\mysummercar_Data\Managed\UnityEngine.dll</HintPath>
|
||||||
|
<Private>False</Private>
|
||||||
|
</Reference>
|
||||||
|
<Reference Include="PlayMaker">
|
||||||
|
<HintPath>$(GamePath)\mysummercar_Data\Managed\PlayMaker.dll</HintPath>
|
||||||
|
<Private>False</Private>
|
||||||
|
</Reference>
|
||||||
|
<Reference Include="MSCO">
|
||||||
|
<HintPath>$(GamePath)\MSCO\MSCO.dll</HintPath>
|
||||||
|
<Private>False</Private>
|
||||||
|
</Reference>
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Compile Include="ShowBoltSize.cs" />
|
||||||
|
</ItemGroup>
|
||||||
|
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||||
|
</Project>
|
||||||
Binary file not shown.
@@ -0,0 +1,16 @@
|
|||||||
|
Third-party libraries shipped with MSCO
|
||||||
|
=======================================
|
||||||
|
|
||||||
|
0Harmony.dll - Harmony 1.2.0.1 by Andreas Pardeike
|
||||||
|
https://github.com/pardeike/Harmony - MIT License
|
||||||
|
Copyright (c) 2017 Andreas Pardeike
|
||||||
|
|
||||||
|
MIT License: Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
|
||||||
|
associated documentation files (the "Software"), to deal in the Software without restriction, including without
|
||||||
|
limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
|
||||||
|
and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above
|
||||||
|
copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
|
||||||
|
WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||||
|
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 105 KiB |
+301
@@ -0,0 +1,301 @@
|
|||||||
|
# Neue Version automatisch auf den Keymaster hochladen (msco.mbm.wtf -> /download, Launcher-Updates).
|
||||||
|
# Läuft nach dem Release-Build von selbst (MSCO.csproj bzw. MSCO.Launcher.csproj) - oder von Hand:
|
||||||
|
# Windows: powershell -ExecutionPolicy Bypass -File release.ps1 -Product mod -File bin\Release\MSCO.dll
|
||||||
|
# Mac: pwsh release.ps1 -Product mod -File bin/Release/MSCO.dll (PowerShell 7: brew install powershell)
|
||||||
|
#
|
||||||
|
# Braucht den Release-Token des Keymasters (RELEASE_TOKEN in dessen .env) und den privaten Signier-Schlüssel.
|
||||||
|
# Beide liegen verschlüsselt beim Entwickler:
|
||||||
|
# Windows: %USERPROFILE%\.msco\*.dpapi (Windows DPAPI - nur dein Windows-Konto kann sie lesen)
|
||||||
|
# macOS: im Schlüsselbund (Keychain, Einträge "MSCO release-token" / "MSCO release-signing-key")
|
||||||
|
# Einrichten: "-SaveToken" (Token), Schlüssel einmal mit "-NewSigningKey" erzeugen bzw. auf einem weiteren Rechner mit
|
||||||
|
# "-ImportSigningKey <backup>" aus der Sicherung ("-ExportSigningKey <datei>", mit eigenem Passwort) übernehmen.
|
||||||
|
# MSCO_RELEASE_TOKEN (Umgebungsvariable) ersetzt den gespeicherten Token, MSCO_NO_RELEASE=1 schaltet Hochladen ab.
|
||||||
|
#
|
||||||
|
# Signatur: jede Datei wird signiert (RSA-3072, SHA-256, "MSCO-RELEASE|produkt|version|sha256") - der Launcher und
|
||||||
|
# das Setup installieren nur, was zu ihrem eingebauten öffentlichen Schlüssel passt. Selbst wer den Keymaster oder
|
||||||
|
# den Token übernimmt, kann so keine fremde Datei als MSCO-Update verteilen.
|
||||||
|
# Gibt es die Version auf dem Keymaster schon, passiert nichts - also einfach die Version erhöhen und bauen.
|
||||||
|
param(
|
||||||
|
[ValidateSet("mod", "launcher", "installer")][string]$Product,
|
||||||
|
[string]$File,
|
||||||
|
[switch]$SaveToken,
|
||||||
|
[switch]$NewSigningKey,
|
||||||
|
[string]$ExportSigningKey = "",
|
||||||
|
[string]$ImportSigningKey = "",
|
||||||
|
[string]$Version = "",
|
||||||
|
[string]$Changelog = ""
|
||||||
|
)
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
$here = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- Ablage der Geheimnisse (Windows / macOS / Linux)
|
||||||
|
|
||||||
|
$onWindows = ($PSVersionTable.PSEdition -eq "Desktop") -or ($IsWindows -eq $true)
|
||||||
|
$onMac = ($IsMacOS -eq $true)
|
||||||
|
$homeDir = if ($env:USERPROFILE) { $env:USERPROFILE } else { $HOME }
|
||||||
|
# Nicht in AppData: das virtualisieren App-Pakete (z.B. die Claude-App) - ein normales Terminal sähe die Dateien nicht
|
||||||
|
$secretDir = Join-Path $homeDir ".msco"
|
||||||
|
if ($onWindows) { Add-Type -AssemblyName System.Security }
|
||||||
|
|
||||||
|
function Get-SecretFile([string]$name) {
|
||||||
|
$new = Join-Path $secretDir "$name.dpapi"
|
||||||
|
if (Test-Path -LiteralPath $new) { return $new }
|
||||||
|
if ($env:APPDATA) {
|
||||||
|
$old = Join-Path $env:APPDATA "MSCO\$name.dpapi" # frühere Ablage
|
||||||
|
if (Test-Path -LiteralPath $old) { return $old }
|
||||||
|
}
|
||||||
|
return $new
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-SecretLocation([string]$name) {
|
||||||
|
if ($onWindows) { return Get-SecretFile $name }
|
||||||
|
if ($onMac) { return "macOS Keychain (MSCO $name)" }
|
||||||
|
return (Join-Path $secretDir $name)
|
||||||
|
}
|
||||||
|
|
||||||
|
# Gespeichertes Geheimnis lesen (Bytes) - $null, wenn es keins gibt
|
||||||
|
function Read-Secret([string]$name, [string]$entropyText) {
|
||||||
|
if ($onWindows) {
|
||||||
|
$file = Get-SecretFile $name
|
||||||
|
if (-not (Test-Path -LiteralPath $file)) { return $null }
|
||||||
|
$entropy = [Text.Encoding]::UTF8.GetBytes($entropyText)
|
||||||
|
return [Security.Cryptography.ProtectedData]::Unprotect([IO.File]::ReadAllBytes($file), $entropy, [Security.Cryptography.DataProtectionScope]::CurrentUser)
|
||||||
|
}
|
||||||
|
if ($onMac) {
|
||||||
|
$value = & security find-generic-password -a $env:USER -s "MSCO $name" -w 2>$null
|
||||||
|
if ($LASTEXITCODE -ne 0 -or -not $value) { return $null }
|
||||||
|
return [Convert]::FromBase64String(($value | Out-String).Trim())
|
||||||
|
}
|
||||||
|
$file = Join-Path $secretDir $name
|
||||||
|
if (-not (Test-Path -LiteralPath $file)) { return $null }
|
||||||
|
return [Convert]::FromBase64String([IO.File]::ReadAllText($file).Trim())
|
||||||
|
}
|
||||||
|
|
||||||
|
function Write-Secret([string]$name, [string]$entropyText, [byte[]]$data) {
|
||||||
|
if ($onWindows) {
|
||||||
|
New-Item -ItemType Directory -Force -Path $secretDir | Out-Null
|
||||||
|
$entropy = [Text.Encoding]::UTF8.GetBytes($entropyText)
|
||||||
|
$file = Join-Path $secretDir "$name.dpapi"
|
||||||
|
[IO.File]::WriteAllBytes($file, [Security.Cryptography.ProtectedData]::Protect($data, $entropy, [Security.Cryptography.DataProtectionScope]::CurrentUser))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if ($onMac) {
|
||||||
|
# -U: vorhandenen Eintrag ersetzen; der Wert liegt danach verschlüsselt im Anmelde-Schlüsselbund
|
||||||
|
& security add-generic-password -U -a $env:USER -s "MSCO $name" -l "MSCO $name" -w ([Convert]::ToBase64String($data))
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw "Could not store MSCO $name in the keychain" }
|
||||||
|
return
|
||||||
|
}
|
||||||
|
# Linux: Datei nur für den eigenen Benutzer lesbar (kein Schlüsselbund vorausgesetzt)
|
||||||
|
New-Item -ItemType Directory -Force -Path $secretDir | Out-Null
|
||||||
|
$file = Join-Path $secretDir $name
|
||||||
|
[IO.File]::WriteAllText($file, [Convert]::ToBase64String($data))
|
||||||
|
& chmod 600 $file
|
||||||
|
}
|
||||||
|
|
||||||
|
# RSA: Windows PowerShell 5.1 braucht RSACng (SHA-256), PowerShell 7 (auch macOS) nimmt RSA.Create()
|
||||||
|
function New-Rsa([int]$bits = 0) {
|
||||||
|
if ($PSVersionTable.PSEdition -eq "Desktop") {
|
||||||
|
if ($bits -gt 0) { return New-Object System.Security.Cryptography.RSACng $bits }
|
||||||
|
return New-Object System.Security.Cryptography.RSACng
|
||||||
|
}
|
||||||
|
if ($bits -gt 0) { return [System.Security.Cryptography.RSA]::Create($bits) }
|
||||||
|
return [System.Security.Cryptography.RSA]::Create()
|
||||||
|
}
|
||||||
|
|
||||||
|
$tokenName = "release-token"; $tokenEntropy = "MSCO release token v1"
|
||||||
|
$keyName = "release-signing-key"; $keyEntropy = "MSCO release signing key v1"
|
||||||
|
|
||||||
|
# Passwort für Backups: verdeckt fragen - oder aus MSCO_BACKUP_PASSWORD (für automatische Backups)
|
||||||
|
function Get-BackupPassword([string]$prompt) {
|
||||||
|
if ($env:MSCO_BACKUP_PASSWORD) { return $env:MSCO_BACKUP_PASSWORD }
|
||||||
|
$secure = Read-Host $prompt -AsSecureString
|
||||||
|
return (New-Object System.Net.NetworkCredential('', $secure)).Password
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-Pbkdf2Key([string]$password, [byte[]]$salt) {
|
||||||
|
$kdf = New-Object Security.Cryptography.Rfc2898DeriveBytes($password, $salt, 300000, [Security.Cryptography.HashAlgorithmName]::SHA256)
|
||||||
|
return $kdf.GetBytes(32)
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- Einrichten / Sichern
|
||||||
|
|
||||||
|
# Token einmal verschlüsselt ablegen: fragt verdeckt danach (oder nimmt MSCO_RELEASE_TOKEN)
|
||||||
|
if ($SaveToken) {
|
||||||
|
$plain = $env:MSCO_RELEASE_TOKEN
|
||||||
|
if (-not $plain) {
|
||||||
|
$secure = Read-Host "Release token" -AsSecureString
|
||||||
|
$plain = (New-Object System.Net.NetworkCredential('', $secure)).Password
|
||||||
|
}
|
||||||
|
if (-not $plain -or $plain.Length -lt 24) { Write-Host "Token too short - nothing saved"; exit 1 }
|
||||||
|
Write-Secret $tokenName $tokenEntropy ([Text.Encoding]::UTF8.GetBytes($plain))
|
||||||
|
Write-Host "Release token saved encrypted: $(Get-SecretLocation $tokenName)"
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Neues Schlüsselpaar (nur einmal - der öffentliche Schlüssel ist in Launcher und Setup eingebaut)
|
||||||
|
if ($NewSigningKey) {
|
||||||
|
if ($null -ne (Read-Secret $keyName $keyEntropy)) { Write-Host "There already is a signing key ($(Get-SecretLocation $keyName)) - not replaced"; exit 1 }
|
||||||
|
$rsa = New-Rsa 3072
|
||||||
|
Write-Secret $keyName $keyEntropy ([Text.Encoding]::UTF8.GetBytes($rsa.ToXmlString($true)))
|
||||||
|
Write-Host "Signing key saved encrypted: $(Get-SecretLocation $keyName)"
|
||||||
|
Write-Host "PUBLIC KEY (put into Launcher/Setup):"
|
||||||
|
Write-Host $rsa.ToXmlString($false)
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Sicherung des privaten Schlüssels mit eigenem Passwort (AES-256, PBKDF2) - z.B. auf dem NAS / USB-Stick
|
||||||
|
if ($ExportSigningKey) {
|
||||||
|
$private = Read-Secret $keyName $keyEntropy
|
||||||
|
if ($null -eq $private) { Write-Host "No signing key ($(Get-SecretLocation $keyName))"; exit 1 }
|
||||||
|
$plainPw = Get-BackupPassword "Password for the backup"
|
||||||
|
if ($plainPw.Length -lt 12) { Write-Host "Use at least 12 characters"; exit 1 }
|
||||||
|
# Zweimal fragen: ein Tippfehler machte das Backup sonst unbrauchbar
|
||||||
|
$plainPw2 = Get-BackupPassword "Repeat the password"
|
||||||
|
if ($plainPw2 -ne $plainPw) { Write-Host "The passwords do not match - nothing written"; exit 1 }
|
||||||
|
$salt = New-Object byte[] 16; [Security.Cryptography.RandomNumberGenerator]::Create().GetBytes($salt)
|
||||||
|
$aes = [Security.Cryptography.Aes]::Create(); $aes.Key = Get-Pbkdf2Key $plainPw $salt; $aes.GenerateIV()
|
||||||
|
$enc = $aes.CreateEncryptor().TransformFinalBlock($private, 0, $private.Length)
|
||||||
|
$out = "MSCO-SIGNING-KEY-BACKUP v1`n" + [Convert]::ToBase64String($salt) + "`n" + [Convert]::ToBase64String($aes.IV) + "`n" + [Convert]::ToBase64String($enc)
|
||||||
|
try {
|
||||||
|
$dir = Split-Path -Parent $ExportSigningKey
|
||||||
|
if ($dir -and -not (Test-Path -LiteralPath $dir)) { New-Item -ItemType Directory -Force -Path $dir | Out-Null }
|
||||||
|
[IO.File]::WriteAllText($ExportSigningKey, $out)
|
||||||
|
} catch {
|
||||||
|
Write-Host "Could not write $ExportSigningKey : $($_.Exception.Message)"
|
||||||
|
Write-Host "Network drive? Drive letters like Z: are not visible in a PowerShell window that runs as administrator."
|
||||||
|
Write-Host "Use a normal (not elevated) PowerShell window, or the network path, e.g. \\server\share\folder\msco-signing-key-backup.txt"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
# Gegenprobe: Datei lesen und mit dem Passwort wieder entschlüsseln
|
||||||
|
$lines = [IO.File]::ReadAllText($ExportSigningKey).Split("`n")
|
||||||
|
$aes2 = [Security.Cryptography.Aes]::Create(); $aes2.Key = Get-Pbkdf2Key $plainPw ([Convert]::FromBase64String($lines[1])); $aes2.IV = [Convert]::FromBase64String($lines[2])
|
||||||
|
$check = [Convert]::FromBase64String($lines[3]); $check = $aes2.CreateDecryptor().TransformFinalBlock($check, 0, $check.Length)
|
||||||
|
if ([Convert]::ToBase64String($check) -ne [Convert]::ToBase64String($private)) { Write-Host "Verification FAILED - the backup is not usable"; exit 1 }
|
||||||
|
Write-Host "Backup written and verified: $ExportSigningKey (AES-256, PBKDF2-SHA256 300000) - keep it and the password safe"
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Sicherung übernehmen (zweiter Rechner, z.B. Mac, oder neuer PC): Datei + Passwort -> verschlüsselte Ablage dieses Rechners
|
||||||
|
if ($ImportSigningKey) {
|
||||||
|
if (-not (Test-Path -LiteralPath $ImportSigningKey)) { Write-Host "Not found: $ImportSigningKey (network drive? see -ExportSigningKey)"; exit 1 }
|
||||||
|
$lines = [IO.File]::ReadAllText($ImportSigningKey).Split("`n")
|
||||||
|
if ($lines.Count -lt 4 -or $lines[0].Trim() -ne "MSCO-SIGNING-KEY-BACKUP v1") { Write-Host "This is not an MSCO signing key backup"; exit 1 }
|
||||||
|
$plainPw = Get-BackupPassword "Password of the backup"
|
||||||
|
try {
|
||||||
|
$aes = [Security.Cryptography.Aes]::Create(); $aes.Key = Get-Pbkdf2Key $plainPw ([Convert]::FromBase64String($lines[1])); $aes.IV = [Convert]::FromBase64String($lines[2])
|
||||||
|
$enc = [Convert]::FromBase64String($lines[3].Trim())
|
||||||
|
$private = $aes.CreateDecryptor().TransformFinalBlock($enc, 0, $enc.Length)
|
||||||
|
$rsa = New-Rsa
|
||||||
|
$rsa.FromXmlString([Text.Encoding]::UTF8.GetString($private))
|
||||||
|
} catch { Write-Host "Wrong password or damaged backup"; exit 1 }
|
||||||
|
$current = Read-Secret $keyName $keyEntropy
|
||||||
|
if ($null -ne $current) {
|
||||||
|
if ([Convert]::ToBase64String($current) -eq [Convert]::ToBase64String($private)) { Write-Host "This key is already installed"; exit 0 }
|
||||||
|
Write-Host "There already is a different signing key ($(Get-SecretLocation $keyName)) - not replaced"; exit 1
|
||||||
|
}
|
||||||
|
Write-Secret $keyName $keyEntropy $private
|
||||||
|
Write-Host "Signing key restored: $(Get-SecretLocation $keyName)"
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- Hochladen
|
||||||
|
|
||||||
|
if (-not $Product -or -not $File) { Write-Host "Usage: release.ps1 -Product mod|launcher|installer -File <file> | -SaveToken | -NewSigningKey | -ExportSigningKey <file> | -ImportSigningKey <file>"; exit 1 }
|
||||||
|
|
||||||
|
if ($env:MSCO_NO_RELEASE -eq "1") { Write-Host "MSCO release: MSCO_NO_RELEASE=1 - not uploading."; exit 0 }
|
||||||
|
$token = $env:MSCO_RELEASE_TOKEN
|
||||||
|
if (-not $token) {
|
||||||
|
try {
|
||||||
|
$stored = Read-Secret $tokenName $tokenEntropy
|
||||||
|
if ($null -ne $stored) { $token = [Text.Encoding]::UTF8.GetString($stored) }
|
||||||
|
} catch { Write-Host "MSCO release: the stored release token cannot be decrypted (other user/PC?) - not uploading."; exit 0 }
|
||||||
|
}
|
||||||
|
if (-not $token) {
|
||||||
|
Write-Host "MSCO release: no release token ($(Get-SecretLocation $tokenName) or MSCO_RELEASE_TOKEN) - set it with 'release.ps1 -SaveToken'. Not uploading."
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
$keymaster = if ($env:MSCO_KEYMASTER) { $env:MSCO_KEYMASTER.TrimEnd("/") } else { "https://msco.mbm.wtf" }
|
||||||
|
|
||||||
|
if (-not (Test-Path $File)) { Write-Host "MSCO release: $File not found"; exit 1 }
|
||||||
|
|
||||||
|
# Version: aus MSCO.cs (Mod) bzw. aus der Datei (Produktversion)
|
||||||
|
if (-not $Version) {
|
||||||
|
$source = Join-Path $here "MSCO.cs"
|
||||||
|
if ($Product -eq "mod" -and (Test-Path $source)) {
|
||||||
|
$m = Select-String -Path $source -Pattern 'ModVersion\s*=\s*"([^"]+)"' | Select-Object -First 1
|
||||||
|
if ($m) { $Version = $m.Matches[0].Groups[1].Value }
|
||||||
|
}
|
||||||
|
if (-not $Version) { $Version = ((Get-Item $File).VersionInfo.ProductVersion -split '\+')[0] }
|
||||||
|
}
|
||||||
|
if (-not $Version) { Write-Host "MSCO release: version unknown - pass -Version"; exit 1 }
|
||||||
|
|
||||||
|
# Changelog: Abschnitt "## <Version>" aus CHANGELOG.md (Text ohne Markdown, gekürzt)
|
||||||
|
if (-not $Changelog) {
|
||||||
|
$log = Join-Path $here "CHANGELOG.md"
|
||||||
|
if (Test-Path $log) {
|
||||||
|
$lines = Get-Content $log -Encoding UTF8
|
||||||
|
$start = [Array]::FindIndex($lines, [Predicate[string]] { param($l) $l -match "^##\s+$([regex]::Escape($Version))\b" })
|
||||||
|
if ($start -ge 0) {
|
||||||
|
$text = @()
|
||||||
|
for ($i = $start + 1; $i -lt $lines.Count -and $lines[$i] -notmatch '^##\s'; $i++) {
|
||||||
|
$l = $lines[$i].Trim() -replace '\*\*', '' -replace '`', ''
|
||||||
|
if ($l) { $text += $l }
|
||||||
|
}
|
||||||
|
$Changelog = ($text -join " ")
|
||||||
|
if ($Changelog.Length -gt 3500) { $Changelog = $Changelog.Substring(0, 3500) + " …" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Signieren: "MSCO-RELEASE|<produkt>|<version>|<sha256>" - so passt die Signatur nur zu genau dieser Datei, diesem
|
||||||
|
# Produkt und dieser Version (keine alte Version als neue, keine Mod-DLL als Launcher)
|
||||||
|
$bytes = [System.IO.File]::ReadAllBytes((Resolve-Path $File))
|
||||||
|
$sha = [BitConverter]::ToString([Security.Cryptography.SHA256]::Create().ComputeHash($bytes)).Replace("-", "").ToLowerInvariant()
|
||||||
|
$signature = ""
|
||||||
|
try {
|
||||||
|
$privateBytes = Read-Secret $keyName $keyEntropy
|
||||||
|
} catch { Write-Host "MSCO release: the signing key cannot be decrypted (other user/PC?) - not uploading"; exit 0 }
|
||||||
|
if ($null -eq $privateBytes) {
|
||||||
|
Write-Host "MSCO release: no signing key ($(Get-SecretLocation $keyName)) - import it with 'release.ps1 -ImportSigningKey <backup>'. Not uploading."
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
$rsa = New-Rsa
|
||||||
|
$rsa.FromXmlString([Text.Encoding]::UTF8.GetString($privateBytes))
|
||||||
|
$message = [Text.Encoding]::UTF8.GetBytes("MSCO-RELEASE|$Product|$Version|$sha")
|
||||||
|
$signature = [Convert]::ToBase64String($rsa.SignData($message, [Security.Cryptography.HashAlgorithmName]::SHA256, [Security.Cryptography.RSASignaturePadding]::Pkcs1))
|
||||||
|
$privateBytes = $null
|
||||||
|
} catch { Write-Host "MSCO release: signing failed ($($_.Exception.Message)) - not uploading"; exit 0 }
|
||||||
|
|
||||||
|
Add-Type -AssemblyName System.Net.Http
|
||||||
|
$client = New-Object System.Net.Http.HttpClient
|
||||||
|
$client.Timeout = [TimeSpan]::FromMinutes(10)
|
||||||
|
$client.DefaultRequestHeaders.Authorization = New-Object System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", $token)
|
||||||
|
# Multipart von Hand: Windows PowerShell (.NET Framework) schreibt name=... ohne Anführungszeichen und die Boundary in
|
||||||
|
# Anführungszeichen - das lehnt der Parser des Keymasters (Node/undici) ab.
|
||||||
|
$boundary = "----MSCO" + [Guid]::NewGuid().ToString("N")
|
||||||
|
$utf8 = New-Object System.Text.UTF8Encoding($false)
|
||||||
|
$body = New-Object System.IO.MemoryStream
|
||||||
|
function Write-Text([string]$text) { $b = $utf8.GetBytes($text); $body.Write($b, 0, $b.Length) }
|
||||||
|
foreach ($field in @(@("product", $Product), @("version", $Version), @("changelog", $Changelog), @("signature", $signature))) {
|
||||||
|
Write-Text "--$boundary`r`nContent-Disposition: form-data; name=`"$($field[0])`"`r`nContent-Type: text/plain; charset=utf-8`r`n`r`n$($field[1])`r`n"
|
||||||
|
}
|
||||||
|
Write-Text "--$boundary`r`nContent-Disposition: form-data; name=`"file`"; filename=`"$([System.IO.Path]::GetFileName($File))`"`r`nContent-Type: application/octet-stream`r`n`r`n"
|
||||||
|
$body.Write($bytes, 0, $bytes.Length)
|
||||||
|
Write-Text "`r`n--$boundary--`r`n"
|
||||||
|
$form = New-Object System.Net.Http.ByteArrayContent(,$body.ToArray())
|
||||||
|
$form.Headers.TryAddWithoutValidation("Content-Type", "multipart/form-data; boundary=$boundary") | Out-Null
|
||||||
|
|
||||||
|
try {
|
||||||
|
$res = $client.PostAsync("$keymaster/api/releases/upload", $form).GetAwaiter().GetResult()
|
||||||
|
$body = $res.Content.ReadAsStringAsync().GetAwaiter().GetResult()
|
||||||
|
} catch {
|
||||||
|
Write-Host "MSCO release: keymaster not reachable ($($_.Exception.Message)) - not uploaded"
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
switch ([int]$res.StatusCode) {
|
||||||
|
200 { Write-Host "MSCO release: $Product $Version uploaded to $keymaster" }
|
||||||
|
409 { Write-Host "MSCO release: $Product $Version is already on the keymaster - raise the version to publish a new one" }
|
||||||
|
default { Write-Host "MSCO release: upload failed (HTTP $([int]$res.StatusCode)): $body" }
|
||||||
|
}
|
||||||
|
exit 0
|
||||||
Reference in New Issue
Block a user