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;