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:
2026-10-28 18:10:48 +01:00
co-authored by Claude Opus 5.5
parent 35f2d1330d
commit 51c1ebc938
12 changed files with 397 additions and 10 deletions
+80 -4
View File
@@ -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);