From 51c1ebc938a71286ed4a7cbea42921a2eb140739 Mon Sep 17 00:00:00 2001 From: MrMaho Date: Wed, 28 Oct 2026 18:10:48 +0100 Subject: [PATCH] 0.4.0: Mitspieler in der Welt, BSO-Konto (Spiel-Ticket) - Mitspieler als Bus/Spielfigur mit Namensschild, weich bewegt (RemotePlayers, Models) - Ohne gueltiges Spiel-Ticket vom Launcher kein Multiplayer; Name kommt aus dem Konto - Protokoll 2 (Hello mit Ticket), IPv6-Adressen - Nach Szenenwechsel erst nach 3 s neu anlegen (Absturz im Test 0.3.0) Co-Authored-By: Claude Opus 5.5 --- BSO.csproj | 2 +- Bootstrap.cs | 4 +- Config.cs | 6 ++- Entities.cs | 8 +++ LocalPlayer.cs | 4 ++ Models.cs | 63 +++++++++++++++++++++++ Multiplayer.cs | 84 ++++++++++++++++++++++++++++-- PositionHud.cs | 5 +- RemotePlayers.cs | 125 +++++++++++++++++++++++++++++++++++++++++++++ Shared/Protocol.cs | 4 +- Shared/Ticket.cs | 62 ++++++++++++++++++++++ Vision.cs | 40 +++++++++++++++ 12 files changed, 397 insertions(+), 10 deletions(-) create mode 100644 Models.cs create mode 100644 RemotePlayers.cs create mode 100644 Shared/Ticket.cs diff --git a/BSO.csproj b/BSO.csproj index 3d3e4fa..14aacd7 100644 --- a/BSO.csproj +++ b/BSO.csproj @@ -5,7 +5,7 @@ latest BSO BSO - 0.2.0 + 0.4.0 x64 true disable diff --git a/Bootstrap.cs b/Bootstrap.cs index adf8996..2243ceb 100644 --- a/Bootstrap.cs +++ b/Bootstrap.cs @@ -19,7 +19,7 @@ namespace BSO /// public static unsafe class Bootstrap { - public const string ModVersion = "0.2.0"; + public const string ModVersion = "0.4.0"; static HostApi* host; static int frameErrors; @@ -60,8 +60,10 @@ namespace BSO BorderlessWindow.Update(); LocalPlayer.Update(); Multiplayer.Update(); + RemotePlayers.Update(); PositionHud.Update(); } + RemotePlayers.Draw(); PositionHud.Draw(); } catch (Exception e) diff --git a/Config.cs b/Config.cs index bfbf2dc..56e79f5 100644 --- a/Config.cs +++ b/Config.cs @@ -13,6 +13,8 @@ namespace BSO public string Server { get; set; } = "127.0.0.1:7777"; /// Beim Spielstart automatisch verbinden (sonst F8). public bool AutoConnect { get; set; } + /// Spiel-Ticket der BSO-Website - schreibt der Launcher bei SPIELEN hinein (24 h gueltig). + public string Ticket { get; set; } public static Config Current { get; private set; } = new(); @@ -26,7 +28,9 @@ namespace BSO if (File.Exists(ConfigFile)) Current = JsonSerializer.Deserialize(File.ReadAllText(ConfigFile), Options) ?? new Config(); // Immer zurueckschreiben: neue Einstellungen erscheinen so auch in alten Dateien File.WriteAllText(ConfigFile, JsonSerializer.Serialize(Current, Options)); - Bootstrap.Log($"Einstellungen: Name \"{Current.Name}\", Server {Current.Server}, AutoConnect {Current.AutoConnect}"); + var ticket = BSO.Net.Ticket.Read(Current.Ticket); + Bootstrap.Log($"Einstellungen: Name \"{Current.Name}\", Server {Current.Server}, AutoConnect {Current.AutoConnect}, " + + (ticket == null ? "kein Spiel-Ticket" : $"Ticket fuer {ticket.Username} bis {ticket.Expires.ToLocalTime():dd.MM. HH:mm}")); } catch (Exception e) { diff --git a/Entities.cs b/Entities.cs index e3bdc4a..91e9698 100644 --- a/Entities.cs +++ b/Entities.cs @@ -31,6 +31,14 @@ namespace BSO return list; } + /// Platz einer Entity in der Tabelle, -1 = nicht (mehr) da. + public static int SlotOf(nint ptr) + { + if (ptr == 0 || !Vision.TryGetEntities(out nint* data, out int count)) return -1; + for (int i = 0; i < count; i++) if (data[i] == ptr) return i; + return -1; + } + /// Steht in Platz slot noch dasselbe Objekt? Dann lebt es noch und darf gelesen werden. public static bool StillThere(int slot, nint ptr) { diff --git a/LocalPlayer.cs b/LocalPlayer.cs index f1a05d9..b8c10a0 100644 --- a/LocalPlayer.cs +++ b/LocalPlayer.cs @@ -107,8 +107,11 @@ namespace BSO Bootstrap.Log(character != null ? $"Spieler = KeyControlledCharacter in Platz {character.Value.Slot}" : "Spielfigur weg - Spieler sitzt im Bus"); Character.Set(character); Character.Refresh(); + if (character != null) Models.RememberCharacter(character.Value.Ptr); } + if (Models.Bus == null && nearestCar != null) Models.RememberFallbackVehicle(nearestCar.Value.Ptr); + // Bus nur festlegen, wenn man drinsitzt (keine Spielfigur), und nur wechseln, wenn der alte fehlt oder weit weg ist bool driving = character == null; bool keep = Bus.Valid && (!driving || (Bus.Pos - Camera).Length < RelockDistance); @@ -116,6 +119,7 @@ namespace BSO Bootstrap.Log($"Eigener Bus = CarVehicle in Platz {nearestCar.Value.Slot}, {bestDist / 100:0.0} m von der Kamera"); Bus.Set(nearestCar); Bus.Refresh(); + Models.RememberBus(nearestCar.Value.Ptr); } static void UpdateSpeed() diff --git a/Models.cs b/Models.cs new file mode 100644 index 0000000..a742e80 --- /dev/null +++ b/Models.cs @@ -0,0 +1,63 @@ +using System; +using System.Collections.Generic; + +namespace BSO +{ + /// + /// Modelldatei einer Entity finden (z.B. Models\Vehicles\Citaro\Citaro12m\...\.model). Die .pak-Dateien des Spiels sind + /// verschluesselt und VisBaseEntity_cl::GetMesh ist nicht exportiert - also: Entity -> Mesh (Zeiger in der Entity) -> + /// Dateiname (char* im Mesh) durchsuchen. Jeder Zeiger wird vorher geprueft (Mem). + /// + public static class Models + { + /// Modell des eigenen Busses / der eigenen Figur - werden fuer die Mitspieler benutzt. + public static string Bus { get; private set; } + public static string Character { get; private set; } + + static readonly Dictionary byEntity = new(); + + /// Solange man noch nicht im eigenen Bus sass: irgendein Fahrzeug aus der Szene. + static string fallbackVehicle; + public static string Vehicle => Bus ?? fallbackVehicle; + + public static void RememberBus(nint entity) => Bus ??= Remember(entity, "Bus"); + public static void RememberCharacter(nint entity) => Character ??= Remember(entity, "Spielfigur"); + public static void RememberFallbackVehicle(nint entity) => fallbackVehicle ??= Remember(entity, "Fahrzeug (Ersatz, bis du im Bus sitzt)"); + + static string Remember(nint entity, string what) + { + string file = Find(entity); + Bootstrap.Log(file != null ? $"Modell {what}: {file}" : $"Modell {what}: nicht gefunden"); + return file; + } + + /// Modelldatei der Entity oder null. + public static string Find(nint entity) + { + if (byEntity.TryGetValue(entity, out string known)) return known; + string found = null; + for (int off = 8; off < 0x400 && found == null; off += 8) + { + if (!Mem.TryReadPtr(entity + off, out nint p) || p == 0 || Mem.IsImage(p)) continue; + found = ModelString(p); // Dateiname direkt in der Entity + for (int off2 = 0; off2 < 0x100 && found == null; off2 += 8) + { + // ... oder im Mesh, auf das die Entity zeigt + if (!Mem.TryReadPtr(p + off2, out nint q) || q == 0 || Mem.IsImage(q)) continue; + found = ModelString(q); + } + } + byEntity[entity] = found; + return found; + } + + static string ModelString(nint p) + { + if (!Mem.IsReadable(p, 8)) return null; + string s = Mem.ReadAscii(p, 260); + if (s.Length < 8 || !s.EndsWith(".model", StringComparison.OrdinalIgnoreCase)) return null; + foreach (char c in s) if (c < 0x20 || c > 0x7e) return null; + return s; + } + } +} diff --git a/Multiplayer.cs b/Multiplayer.cs index ac071bf..f8ec5d7 100644 --- a/Multiplayer.cs +++ b/Multiplayer.cs @@ -21,8 +21,49 @@ namespace BSO { public int Id; public string Name; - public PlayerState State; + public PlayerState State; // zuletzt empfangen public bool HasState; + + // Weiche Bewegung: vom angezeigten Stand zum neuen Paket gleiten, so lange wie seit dem letzten Paket vergangen ist + PlayerState from, to; + long startTicks, durationTicks = 1; + + public void Receive(PlayerState s) + { + long now = Stopwatch.GetTimestamp(); + from = HasState ? Display : s; + to = s; + long since = now - startTicks; + durationTicks = Math.Clamp(since, Stopwatch.Frequency / 20, Stopwatch.Frequency / 4); // 50..250 ms + startTicks = now; + State = s; + HasState = true; + } + + /// Stand fuer dieses Bild (zwischen den 10 Paketen pro Sekunde weich weiterbewegt). + public PlayerState Display + { + get + { + if (from.Mode != to.Mode) return to; // Ein-/Aussteigen: nicht gleiten + float t = Math.Clamp((Stopwatch.GetTimestamp() - startTicks) / (float)durationTicks, 0, 1); + PlayerState s = to; + s.X = from.X + (to.X - from.X) * t; + s.Y = from.Y + (to.Y - from.Y) * t; + s.Z = from.Z + (to.Z - from.Z) * t; + s.Yaw = Angle(from.Yaw, to.Yaw, t); + s.Pitch = Angle(from.Pitch, to.Pitch, t); + s.Roll = Angle(from.Roll, to.Roll, t); + return s; + } + } + + /// Winkel (Grad) auf dem kuerzeren Weg mischen - sonst dreht sich der Bus bei 359 -> 1 einmal ganz herum. + static float Angle(float a, float b, float t) + { + float d = ((b - a) % 360 + 540) % 360 - 180; + return a + d * t; + } } const int ConnectTimeoutMs = 5000; @@ -85,8 +126,32 @@ namespace BSO public static string ServerAddress => Config.Current.Server; + /// + /// Gueltiges Spiel-Ticket vom Launcher? Ohne gibt es keinen Multiplayer - der Server prueft es zusaetzlich (die Mod + /// allein liesse sich austricksen), hier gibt es nur die verstaendliche Meldung vorab. + /// + public static string AccountProblem + { + get + { + var t = Ticket.Read(Config.Current.Ticket); + if (t == null) return "Nicht ueber den BSO Launcher gestartet - Multiplayer gesperrt"; + if (t.Expired) return "Spiel-Ticket abgelaufen - im BSO Launcher neu auf SPIELEN druecken"; + return null; + } + } + + /// Name im Spiel: aus dem Konto (Ticket), sonst aus BSO.json. + public static string PlayerName => Ticket.Read(Config.Current.Ticket)?.Username ?? Config.Current.Name; + static void Connect() { + if (AccountProblem is string problem) + { + LastError = problem; + Bootstrap.Log("Nicht verbunden: " + problem); + return; + } (string host, int port) = ParseAddress(ServerAddress); Bootstrap.Log($"Verbinde mit {host}:{port} ..."); LastError = ""; @@ -114,7 +179,8 @@ namespace BSO { w.Write(Protocol.Version); w.Write(Bootstrap.ModVersion); - w.Write(Config.Current.Name ?? ""); + w.Write(PlayerName ?? ""); + w.Write(Config.Current.Ticket ?? ""); })); // Erst nach Welcome gilt man als verbunden - bis dahin bleibt der Status "Verbinde" Status = NetStatus.Connected; @@ -167,8 +233,7 @@ namespace BSO case MsgType.PlayerState: int id = r.ReadInt32(); if (!Players.TryGetValue(id, out Remote p2)) return; // PlayerJoined kommt immer vorher - p2.State = PlayerState.Read(r); - p2.HasState = true; + p2.Receive(PlayerState.Read(r)); break; case MsgType.Pong: @@ -196,6 +261,17 @@ namespace BSO static (string, int) ParseAddress(string address) { address = (address ?? "").Trim(); + // IPv6: [::1]:7777 oder [::1] + if (address.StartsWith("[")) + { + int end = address.IndexOf(']'); + if (end > 1) + { + string host6 = address.Substring(1, end - 1); + return address.Length > end + 2 && address[end + 1] == ':' && int.TryParse(address.Substring(end + 2), out int p6) + ? (host6, p6) : (host6, Protocol.DefaultPort); + } + } int colon = address.LastIndexOf(':'); if (colon > 0 && int.TryParse(address.Substring(colon + 1), out int port)) return (address.Substring(0, colon), port); return (address.Length > 0 ? address : "127.0.0.1", Protocol.DefaultPort); diff --git a/PositionHud.cs b/PositionHud.cs index 39d8928..dcfd0b1 100644 --- a/PositionHud.cs +++ b/PositionHud.cs @@ -59,8 +59,11 @@ namespace BSO { switch (Multiplayer.Status) { + case Multiplayer.NetStatus.Offline when Multiplayer.AccountProblem != null: + text.Add(Multiplayer.AccountProblem); + return; case Multiplayer.NetStatus.Offline: - text.Add($"Offline - [F8] verbinden mit {Multiplayer.ServerAddress}"); + text.Add($"Offline - [F8] verbinden mit {Multiplayer.ServerAddress} als {Multiplayer.PlayerName}"); if (Multiplayer.LastError.Length > 0) text.Add(" " + Multiplayer.LastError); return; case Multiplayer.NetStatus.Connecting: diff --git a/RemotePlayers.cs b/RemotePlayers.cs new file mode 100644 index 0000000..c65f103 --- /dev/null +++ b/RemotePlayers.cs @@ -0,0 +1,125 @@ +using System.Collections.Generic; +using System.Linq; +using BSO.Net; + +namespace BSO +{ + /// + /// Mitspieler in der Welt: je Spieler eine eigene Entity mit dem Modell des eigenen Busses bzw. der eigenen Spielfigur + /// (Models), jedes Bild auf den weich bewegten Stand gesetzt (Remote.Display), darueber ein Namensschild. Die Entities + /// sind nur Grafik (VisBaseEntity_cl, keine Physik): man faehrt durch sie hindurch. + /// + public static class RemotePlayers + { + const float TagHeightBus = 420, TagHeightFoot = 215; // cm ueber dem Ursprung + const float TagMaxDistance = 30000; // weiter weg: kein Schild (300 m) + + class Shown + { + public nint Entity; + public int Slot = -1; + public PlayerMode Mode; + public string Model; + public byte[] Tag; + public Vec3 TagPos; + public bool TagVisible; + } + + static readonly Dictionary shown = new(); + static bool warned; + + // Nach einem Szenenwechsel (Entity von der Engine entfernt) erst nach einer Pause neu anlegen - mitten im Abbau + // der Welt eine Entity anzulegen liess das Spiel abstuerzen (Test 0.3.0: zweimal neu angelegt, dann Ende) + static readonly System.Diagnostics.Stopwatch sceneCalm = System.Diagnostics.Stopwatch.StartNew(); + const int RespawnDelayMs = 3000; + + public static void Update() + { + if (!Vision.CanSpawn) + { + if (!warned) { warned = true; Bootstrap.Log("Mitspieler in der Welt: Engine-Funktionen fehlen - nur Liste"); } + return; + } + // Szene weg oder man ist nicht in der Welt (Menue, Laden): die Engine raeumt unsere Entities mit weg - nur vergessen, + // nichts anfassen und nichts anlegen + PlayerState me = LocalPlayer.State; + if (!LocalPlayer.InScene || me.Mode == PlayerMode.Menu) + { + if (shown.Count > 0) Bootstrap.Log("Nicht in der Welt - Mitspieler-Modelle vergessen"); + shown.Clear(); + sceneCalm.Restart(); + return; + } + + // Gegangen oder getrennt + foreach (int id in shown.Keys.Where(id => !Multiplayer.Players.ContainsKey(id)).ToList()) + Remove(id); + + foreach (Multiplayer.Remote p in Multiplayer.Players.Values) + { + if (!p.HasState || p.State.Mode == PlayerMode.Menu) { Remove(p.Id); continue; } + PlayerState s = p.Display; + string model = s.Mode == PlayerMode.InBus ? Models.Vehicle : Models.Character; + shown.TryGetValue(p.Id, out Shown v); + + // Ein-/Aussteigen oder besseres Modell bekannt (eigener Bus statt Ersatzfahrzeug): neu anlegen + if (v != null && (v.Mode != s.Mode || v.Model != model)) { Remove(p.Id); v = null; } + // Von der Engine entfernt? (z.B. beim Neuladen der Stadt) - nach einer Pause neu anlegen + if (v != null && v.Entity != 0 && !Entities.StillThere(v.Slot, v.Entity)) + { + Bootstrap.Log($"{p.Name}: Modell von der Engine entfernt - lege es nach {RespawnDelayMs / 1000} s neu an"); + shown.Remove(p.Id); + v = null; + sceneCalm.Restart(); + } + + var pos = new Vec3(s.X, s.Y, s.Z); + if (v == null) + { + if (sceneCalm.ElapsedMilliseconds < RespawnDelayMs) continue; + v = new Shown { Mode = s.Mode, Model = model, Tag = Vision.ToAscii(p.Name) }; + shown[p.Id] = v; + if (model != null) Spawn(p, v, pos); + else Bootstrap.Log($"{p.Name}: noch kein Modell fuer \"{s.Mode}\" bekannt - nur Namensschild"); + } + if (v.Entity != 0) + { + Vision.SetPosition(v.Entity, pos); + Vision.SetOrientation(v.Entity, new Vec3(s.Yaw, s.Pitch, s.Roll)); + } + + float dist = me.Mode == PlayerMode.Menu ? 0 : s.DistanceTo(me); + v.TagVisible = dist < TagMaxDistance; + v.TagPos = new Vec3(s.X, s.Y, s.Z + (s.Mode == PlayerMode.InBus ? TagHeightBus : TagHeightFoot)); + v.Tag = Vision.ToAscii(me.Mode == PlayerMode.Menu ? p.Name : $"{p.Name} ({dist / 100:0} m)"); + } + } + + static void Spawn(Multiplayer.Remote p, Shown v, Vec3 pos) + { + v.Entity = Vision.CreateEntity("VisBaseEntity_cl", pos, v.Model); + v.Slot = Entities.SlotOf(v.Entity); + if (v.Entity == 0 || v.Slot < 0) + { + Bootstrap.Log($"{p.Name}: Entity mit {v.Model} liess sich nicht anlegen"); + v.Entity = 0; + return; + } + Bootstrap.Log($"{p.Name} erscheint in der Welt ({v.Mode}, {v.Model}, Platz {v.Slot})"); + } + + static void Remove(int id) + { + if (!shown.Remove(id, out Shown v)) return; + if (v.Entity != 0 && Entities.StillThere(v.Slot, v.Entity)) Vision.RemoveEntity(v.Entity); + } + + /// Namensschilder - bei jedem Durchlauf wie der Text oben links (sonst flackern sie). + public static void Draw() + { + if (!Vision.CanSpawn || !LocalPlayer.InScene) return; + foreach (Shown v in shown.Values) + if (v.TagVisible) Vision.DrawText3D(v.Tag, v.TagPos); + } + } +} diff --git a/Shared/Protocol.cs b/Shared/Protocol.cs index f47b0c0..cb13710 100644 --- a/Shared/Protocol.cs +++ b/Shared/Protocol.cs @@ -10,13 +10,13 @@ namespace BSO.Net public static class Protocol { /// Bei jeder inkompatiblen Aenderung erhoehen - der Server lehnt andere Versionen ab. - public const int Version = 1; + public const int Version = 2; // 2: Hello mit Spiel-Ticket public const int DefaultPort = 7777; } public enum MsgType : byte { - Hello = 1, // Client -> Server: int Protokoll, string Mod-Version, string Name + Hello = 1, // Client -> Server: int Protokoll, string Mod-Version, string Name, string Spiel-Ticket (vom Launcher) Welcome = 2, // Server -> Client: int eigene ID, string Servername, int n, je [int ID, string Name] Reject = 3, // Server -> Client: string Grund PlayerState = 4, // Client -> Server: PlayerState; Server -> Client: int ID + PlayerState diff --git a/Shared/Ticket.cs b/Shared/Ticket.cs new file mode 100644 index 0000000..90093dc --- /dev/null +++ b/Shared/Ticket.cs @@ -0,0 +1,62 @@ +using System; +using System.Security.Cryptography; +using System.Text; +using System.Text.Json; + +namespace BSO.Net +{ + /// + /// Spiel-Ticket der BSO-Website: base64url(JSON {u, id, iat, exp}) + "." + base64url(RSA-SHA256-Signatur). Der Launcher + /// holt es beim Spielen und schreibt es in BSO.json, die Mod schickt es im Hello mit. Die Mod liest es nur (Name, Ablauf), + /// pruefen kann es nur der Server mit dem oeffentlichen Schluessel der Website (Verify) - wer die Mod veraendert, kommt so + /// trotzdem nicht ohne gueltiges Konto auf einen Server. + /// + public sealed class Ticket + { + public string Username { get; private set; } + public string AccountId { get; private set; } + public DateTime Expires { get; private set; } + public bool Expired => DateTime.UtcNow >= Expires; + + /// Inhalt lesen, ohne die Signatur zu pruefen. null = kein Ticket / kaputt. + public static Ticket Read(string ticket) + { + try + { + if (string.IsNullOrEmpty(ticket) || ticket.Length > 4096) return null; + string[] parts = ticket.Split('.'); + if (parts.Length != 2) return null; + using var doc = JsonDocument.Parse(FromB64(parts[0])); + var r = doc.RootElement; + return new Ticket + { + Username = r.GetProperty("u").GetString(), + AccountId = r.GetProperty("id").GetString(), + Expires = DateTimeOffset.FromUnixTimeSeconds(r.GetProperty("exp").GetInt64()).UtcDateTime + }; + } + catch { return null; } + } + + /// Signatur pruefen (Server). Gueltig und nicht abgelaufen -> Ticket, sonst null mit Grund. + public static Ticket Verify(string ticket, RSA publicKey, out string error) + { + error = null; + Ticket t = Read(ticket); + if (t == null) { error = "Kein gueltiges BSO-Konto - bitte ueber den BSO Launcher starten"; return null; } + string[] parts = ticket.Split('.'); + bool ok; + try { ok = publicKey.VerifyData(Encoding.ASCII.GetBytes(parts[0]), FromB64(parts[1]), HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); } + catch { ok = false; } + if (!ok) { error = "Ungueltiges Spiel-Ticket - bitte ueber den BSO Launcher starten"; return null; } + if (t.Expired) { error = "Spiel-Ticket abgelaufen - bitte im BSO Launcher neu auf SPIELEN druecken"; return null; } + return t; + } + + static byte[] FromB64(string s) + { + s = s.Replace('-', '+').Replace('_', '/'); + return Convert.FromBase64String(s.PadRight(s.Length + (4 - s.Length % 4) % 4, '=')); + } + } +} diff --git a/Vision.cs b/Vision.cs index c44eab0..9cab29c 100644 --- a/Vision.cs +++ b/Vision.cs @@ -37,6 +37,15 @@ namespace BSO static delegate* unmanaged getTimer; static delegate* unmanaged getTime; + // Mitspieler in der Welt (RemotePlayers) - optional: fehlt etwas, laeuft der Rest trotzdem + static nint game; // Vision::Game (VisGame_cl) + static delegate* unmanaged createEntity; + static delegate* unmanaged setPosition; + static delegate* unmanaged setOrientation; + static delegate* unmanaged removeEntity; + static delegate* unmanaged drawMessage3D; + public static bool CanSpawn { get; private set; } + public static bool Init() { if (tried) return Loaded; @@ -58,10 +67,41 @@ namespace BSO Loaded = message != 0 && camera != 0 && entityTable != 0 && print != null && getMainCamera != null && getPosition != null && getOrientation != null && getTimer != null && getTime != null; + game = Export(module, "?Game@Vision@@2VVisGame_cl@@A"); + // VisBaseEntity_cl* VisGame_cl::CreateEntity(const char* klasse, const VisVector_cl& pos, const char* modell, const char* parameter) + createEntity = (delegate* unmanaged)Export(module, "?CreateEntity@VisGame_cl@@QEAAPEAVVisBaseEntity_cl@@PEBDAEBVVisVector_cl@@00@Z"); + setPosition = (delegate* unmanaged)Export(module, "?SetPosition@VisObject3D_cl@@QEAAXMMM@Z"); + setOrientation = (delegate* unmanaged)Export(module, "?SetOrientation@VisObject3D_cl@@QEAAXMMM@Z"); + removeEntity = (delegate* unmanaged)Export(module, "?Remove@VisBaseEntity_cl@@QEAAXXZ"); + // int VisGame_cl::DrawMessage3D(const char* text, const VisVector_cl& pos, short) - wie Print nur fuer dieses Bild + drawMessage3D = (delegate* unmanaged)Export(module, "?DrawMessage3D@VisGame_cl@@QEAAHPEBDAEBVVisVector_cl@@F@Z"); + CanSpawn = game != 0 && createEntity != null && setPosition != null && setOrientation != null && removeEntity != null && drawMessage3D != null; + Bootstrap.Log(Loaded ? "Vision Engine gefunden, alle Funktionen da" : "Vision Engine: Funktionen fehlen"); return Loaded; } + /// Neue Entity mit Modell (nur Grafik, keine Physik). 0 = fehlgeschlagen. + public static nint CreateEntity(string className, Vec3 pos, string modelFile) + { + float* v = stackalloc float[4] { pos.X, pos.Y, pos.Z, 0 }; // VisVector_cl (3 float, Platz fuer Ausrichtung) + byte[] cls = ToAscii(className), model = ToAscii(modelFile); + byte* none = stackalloc byte[1] { 0 }; + fixed (byte* c = cls, m = model) return createEntity(game, c, v, m, none); + } + + public static void SetPosition(nint obj, Vec3 p) => setPosition(obj, p.X, p.Y, p.Z); + public static void SetOrientation(nint obj, Vec3 yawPitchRoll) => setOrientation(obj, yawPitchRoll.X, yawPitchRoll.Y, yawPitchRoll.Z); + /// Entity zum Entfernen vormerken (die Engine raeumt sie am Ende des Bildes weg). + public static void RemoveEntity(nint entity) => removeEntity(entity); + + /// Text an einer Stelle in der Welt, fuer dieses Bild (Namensschild). text mit ToAscii vorbereitet. + public static void DrawText3D(byte[] text, Vec3 pos) + { + float* v = stackalloc float[4] { pos.X, pos.Y, pos.Z, 0 }; + fixed (byte* t = text) drawMessage3D(game, t, v, 0); + } + static nint Export(nint module, string name) { if (NativeLibrary.TryGetExport(module, name, out nint address)) return address;