- 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>
198 lines
10 KiB
C#
198 lines
10 KiB
C#
using System;
|
|
using System.Runtime.InteropServices;
|
|
using System.Text;
|
|
|
|
namespace BSO
|
|
{
|
|
public struct Vec3
|
|
{
|
|
public float X, Y, Z;
|
|
public Vec3(float x, float y, float z) { X = x; Y = y; Z = z; }
|
|
public static Vec3 operator -(Vec3 a, Vec3 b) => new(a.X - b.X, a.Y - b.Y, a.Z - b.Z);
|
|
public float Length => MathF.Sqrt(X * X + Y * Y + Z * Z);
|
|
public override string ToString() => $"{X:0.0} / {Y:0.0} / {Z:0.0}";
|
|
}
|
|
|
|
/// <summary>
|
|
/// Zugriff auf die Havok Vision Engine (Vision90.dll). Die Engine exportiert ihre C++-Funktionen und globalen Objekte
|
|
/// mit Namen - die Adressen holen wir ueber die dekorierten Namen, aufgerufen wird ueber Funktionszeiger
|
|
/// (x64: "this" ist einfach das erste Argument).
|
|
/// </summary>
|
|
public static unsafe class Vision
|
|
{
|
|
[DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
|
|
static extern nint GetModuleHandleW(string name);
|
|
|
|
static bool tried;
|
|
public static bool Loaded { get; private set; }
|
|
|
|
static nint message; // Vision::Message (VisMessage_cl)
|
|
static nint camera; // Vision::Camera (VisCamera_cl)
|
|
static nint entityTable; // VisElementManager_cl<VisBaseEntity_cl*>::elementTable (DynArray_cl)
|
|
|
|
static delegate* unmanaged<nint, int, int, int, byte*, byte*, void> print;
|
|
static delegate* unmanaged<nint, nint> getMainCamera;
|
|
static delegate* unmanaged<nint, float*, float*, float*, void> getPosition;
|
|
static delegate* unmanaged<nint, float*, float*, float*, void> getOrientation;
|
|
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;
|
|
tried = true;
|
|
|
|
nint module = GetModuleHandleW("Vision90.dll");
|
|
if (module == 0) { Bootstrap.Log("Vision90.dll ist nicht geladen"); return false; }
|
|
|
|
message = Export(module, "?Message@Vision@@2VVisMessage_cl@@A");
|
|
camera = Export(module, "?Camera@Vision@@2VVisCamera_cl@@A");
|
|
entityTable = Export(module, "?elementTable@?$VisElementManager_cl@PEAVVisBaseEntity_cl@@@@0V?$DynArray_cl@PEAVVisBaseEntity_cl@@@@A");
|
|
// void VisMessage_cl::Print(int, int x, int y, const char* format, ...) - variadisch, auf x64 normal aufrufbar
|
|
print = (delegate* unmanaged<nint, int, int, int, byte*, byte*, void>)Export(module, "?Print@VisMessage_cl@@QEAAXHHHPEBDZZ");
|
|
getMainCamera = (delegate* unmanaged<nint, nint>)Export(module, "?GetMainCamera@VisCamera_cl@@QEBAPEAVVisContextCamera_cl@@XZ");
|
|
getPosition = (delegate* unmanaged<nint, float*, float*, float*, void>)Export(module, "?GetPosition@VisObject3D_cl@@QEBAXAEAM00@Z");
|
|
getOrientation = (delegate* unmanaged<nint, float*, float*, float*, void>)Export(module, "?GetOrientation@VisObject3D_cl@@QEBAXAEAM00@Z");
|
|
getTimer = (delegate* unmanaged<nint>)Export(module, "?GetTimer@Vision@@SAPEAVIVTimer@@XZ");
|
|
getTime = (delegate* unmanaged<nint, float>)Export(module, "?GetTime@IVTimer@@QEBAMXZ");
|
|
|
|
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;
|
|
Bootstrap.Log("Export fehlt: " + name);
|
|
return 0;
|
|
}
|
|
|
|
static float lastTime = -1;
|
|
static long lastTick;
|
|
|
|
/// <summary>
|
|
/// Ist die Engine-Uhr (Vision::GetTimer()->GetTime()) weitergelaufen? Sie hat nur ~1 ms Aufloesung, die Spielschleife
|
|
/// laeuft im Menue aber ~9000-mal pro Sekunde - fuer Berechnungen reicht das, fuer die Textausgabe nicht (siehe
|
|
/// PositionHud.Draw). Steht die Uhr (Pause), zaehlt hoechstens alle 16 ms ein Aufruf.
|
|
/// </summary>
|
|
public static bool TimerAdvanced()
|
|
{
|
|
nint timer = getTimer();
|
|
float now = timer != 0 ? getTime(timer) : 0;
|
|
long tick = Environment.TickCount64;
|
|
if (now == lastTime && tick - lastTick < 16) return false;
|
|
lastTime = now;
|
|
lastTick = tick;
|
|
return true;
|
|
}
|
|
|
|
/// <summary>Text fuer dieses Bild auf den Bildschirm (Vision::Message.Print).</summary>
|
|
public static void Print(int x, int y, string text) => Print(x, y, ToAscii(text));
|
|
|
|
/// <summary>Wie Print(string), mit vorbereitetem Text (ToAscii) - fuer Ausgaben bei jedem Durchlauf.</summary>
|
|
public static void Print(int x, int y, byte[] text)
|
|
{
|
|
byte* format = stackalloc byte[] { (byte)'%', (byte)'s', 0 };
|
|
fixed (byte* p = text) print(message, 1, x, y, format, p);
|
|
}
|
|
|
|
public static byte[] ToAscii(string text) => Encoding.ASCII.GetBytes(text + "\0");
|
|
|
|
/// <summary>Hauptkamera (VisContextCamera_cl) oder 0, solange keine Szene geladen ist.</summary>
|
|
public static nint MainCamera => getMainCamera(camera);
|
|
|
|
/// <summary>Position eines VisObject3D_cl (Entity, Kamera). obj muss gueltig sein.</summary>
|
|
public static Vec3 GetPosition(nint obj)
|
|
{
|
|
float x, y, z;
|
|
getPosition(obj, &x, &y, &z);
|
|
return new Vec3(x, y, z);
|
|
}
|
|
|
|
/// <summary>Drehung eines VisObject3D_cl in Grad (Gier, Nick, Roll), wie SetOrientation sie nimmt.</summary>
|
|
public static Vec3 GetOrientation(nint obj)
|
|
{
|
|
float yaw, pitch, roll;
|
|
getOrientation(obj, &yaw, &pitch, &roll);
|
|
return new Vec3(yaw, pitch, roll);
|
|
}
|
|
|
|
static bool tableLogged;
|
|
|
|
/// <summary>
|
|
/// Liste aller Entities (Zeiger, freie Plaetze sind 0). Der Aufbau von DynArray_cl ist nicht dokumentiert -
|
|
/// wir probieren die ueblichen Anordnungen und nehmen die erste, deren Datenzeiger lesbar ist.
|
|
/// </summary>
|
|
public static bool TryGetEntities(out nint* data, out int count)
|
|
{
|
|
data = null;
|
|
count = 0;
|
|
nint* h = (nint*)entityTable;
|
|
if (!tableLogged)
|
|
{
|
|
tableLogged = true;
|
|
Bootstrap.Log($"Entity-Tabelle: {h[0]:X} {h[1]:X} {h[2]:X} {h[3]:X}");
|
|
}
|
|
|
|
// (Groesse, Daten): [size][data] / [vtable][size][data] / [data][size]
|
|
if (TryLayout(h[1], h[0], out data, out count)) return true;
|
|
if (Mem.IsImage(h[0]) && TryLayout(h[2], h[1], out data, out count)) return true;
|
|
return TryLayout(h[0], h[1], out data, out count);
|
|
}
|
|
|
|
static bool TryLayout(nint dataPtr, nint sizeField, out nint* data, out int count)
|
|
{
|
|
data = null;
|
|
count = 0;
|
|
uint size = (uint)sizeField;
|
|
if (size == 0 || size > 1 << 20 || dataPtr == 0 || Mem.IsImage(dataPtr)) return false;
|
|
if (!Mem.IsReadable(dataPtr, (int)size * 8)) return false;
|
|
data = (nint*)dataPtr;
|
|
count = (int)size;
|
|
return true;
|
|
}
|
|
}
|
|
}
|