Files
BSO-Mod/RemotePlayers.cs
MrMahoandClaude Opus 5.5 51c1ebc938 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>
2026-10-28 18:10:48 +01:00

126 lines
5.5 KiB
C#

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);
}
}
}