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 <noreply@anthropic.com>
This commit is contained in:
+1
-1
@@ -5,7 +5,7 @@
|
||||
<LangVersion>latest</LangVersion>
|
||||
<RootNamespace>BSO</RootNamespace>
|
||||
<AssemblyName>BSO</AssemblyName>
|
||||
<Version>0.2.0</Version>
|
||||
<Version>0.4.0</Version>
|
||||
<PlatformTarget>x64</PlatformTarget>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
<Nullable>disable</Nullable>
|
||||
|
||||
+3
-1
@@ -19,7 +19,7 @@ namespace BSO
|
||||
/// </summary>
|
||||
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)
|
||||
|
||||
@@ -13,6 +13,8 @@ namespace BSO
|
||||
public string Server { get; set; } = "127.0.0.1:7777";
|
||||
/// <summary>Beim Spielstart automatisch verbinden (sonst F8).</summary>
|
||||
public bool AutoConnect { get; set; }
|
||||
/// <summary>Spiel-Ticket der BSO-Website - schreibt der Launcher bei SPIELEN hinein (24 h gueltig).</summary>
|
||||
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<Config>(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)
|
||||
{
|
||||
|
||||
@@ -31,6 +31,14 @@ namespace BSO
|
||||
return list;
|
||||
}
|
||||
|
||||
/// <summary>Platz einer Entity in der Tabelle, -1 = nicht (mehr) da.</summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>Steht in Platz slot noch dasselbe Objekt? Dann lebt es noch und darf gelesen werden.</summary>
|
||||
public static bool StillThere(int slot, nint ptr)
|
||||
{
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace BSO
|
||||
{
|
||||
/// <summary>
|
||||
/// 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).
|
||||
/// </summary>
|
||||
public static class Models
|
||||
{
|
||||
/// <summary>Modell des eigenen Busses / der eigenen Figur - werden fuer die Mitspieler benutzt.</summary>
|
||||
public static string Bus { get; private set; }
|
||||
public static string Character { get; private set; }
|
||||
|
||||
static readonly Dictionary<nint, string> byEntity = new();
|
||||
|
||||
/// <summary>Solange man noch nicht im eigenen Bus sass: irgendein Fahrzeug aus der Szene.</summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>Modelldatei der Entity oder null.</summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+80
-4
@@ -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;
|
||||
}
|
||||
|
||||
/// <summary>Stand fuer dieses Bild (zwischen den 10 Paketen pro Sekunde weich weiterbewegt).</summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Winkel (Grad) auf dem kuerzeren Weg mischen - sonst dreht sich der Bus bei 359 -> 1 einmal ganz herum.</summary>
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Name im Spiel: aus dem Konto (Ticket), sonst aus BSO.json.</summary>
|
||||
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);
|
||||
|
||||
+4
-1
@@ -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:
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using BSO.Net;
|
||||
|
||||
namespace BSO
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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<int, Shown> 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);
|
||||
}
|
||||
|
||||
/// <summary>Namensschilder - bei jedem Durchlauf wie der Text oben links (sonst flackern sie).</summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -10,13 +10,13 @@ namespace BSO.Net
|
||||
public static class Protocol
|
||||
{
|
||||
/// <summary>Bei jeder inkompatiblen Aenderung erhoehen - der Server lehnt andere Versionen ab.</summary>
|
||||
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
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
using System;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace BSO.Net
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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;
|
||||
|
||||
/// <summary>Inhalt lesen, ohne die Signatur zu pruefen. null = kein Ticket / kaputt.</summary>
|
||||
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; }
|
||||
}
|
||||
|
||||
/// <summary>Signatur pruefen (Server). Gueltig und nicht abgelaufen -> Ticket, sonst null mit Grund.</summary>
|
||||
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, '='));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -37,6 +37,15 @@ namespace BSO
|
||||
static delegate* unmanaged<nint> getTimer;
|
||||
static delegate* unmanaged<nint, float> getTime;
|
||||
|
||||
// Mitspieler in der Welt (RemotePlayers) - optional: fehlt etwas, laeuft der Rest trotzdem
|
||||
static nint game; // Vision::Game (VisGame_cl)
|
||||
static delegate* unmanaged<nint, byte*, float*, byte*, byte*, nint> createEntity;
|
||||
static delegate* unmanaged<nint, float, float, float, void> setPosition;
|
||||
static delegate* unmanaged<nint, float, float, float, void> setOrientation;
|
||||
static delegate* unmanaged<nint, void> removeEntity;
|
||||
static delegate* unmanaged<nint, byte*, float*, short, int> 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<nint, byte*, float*, byte*, byte*, nint>)Export(module, "?CreateEntity@VisGame_cl@@QEAAPEAVVisBaseEntity_cl@@PEBDAEBVVisVector_cl@@00@Z");
|
||||
setPosition = (delegate* unmanaged<nint, float, float, float, void>)Export(module, "?SetPosition@VisObject3D_cl@@QEAAXMMM@Z");
|
||||
setOrientation = (delegate* unmanaged<nint, float, float, float, void>)Export(module, "?SetOrientation@VisObject3D_cl@@QEAAXMMM@Z");
|
||||
removeEntity = (delegate* unmanaged<nint, void>)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<nint, byte*, float*, short, int>)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;
|
||||
}
|
||||
|
||||
/// <summary>Neue Entity mit Modell (nur Grafik, keine Physik). 0 = fehlgeschlagen.</summary>
|
||||
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);
|
||||
/// <summary>Entity zum Entfernen vormerken (die Engine raeumt sie am Ende des Bildes weg).</summary>
|
||||
public static void RemoveEntity(nint entity) => removeEntity(entity);
|
||||
|
||||
/// <summary>Text an einer Stelle in der Welt, fuer dieses Bild (Namensschild). text mit ToAscii vorbereitet.</summary>
|
||||
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;
|
||||
|
||||
Reference in New Issue
Block a user