Per-player needs, jumping figure, round belt/shoulders/boots, reset-point UI

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-27 17:48:16 +02:00
co-authored by Claude Opus 5.5
parent 09c85e3d96
commit 8dae3ac13d
7 changed files with 128 additions and 20 deletions
+11 -1
View File
@@ -17,11 +17,21 @@ Server und Mod müssen beide 2.8 sein (Server: `git pull && docker compose up -d
- `vehicles` in der F7-Konsole zeigt, wie viele Karosserie-Meshes, Job-Werte und Verkehrsautos synchronisiert werden
**Admin: Welt verwalten (neuer Reiter WORLD im Admin-Menü)**
- **RESET WORLD**: Weltzustand zurücksetzen, alle verbinden automatisch neu
- **RESET WORLD**: die ganze Welt zurück auf den Spielstand, wie er auf den Server hochgeladen wurde (Reset-Punkt) –
das automatische Hochladen alle 5 Minuten ändert den Reset-Punkt nicht; der Stand davor wird gesichert
- **SET RESET POINT**: den aktuellen Spielstand als neuen Reset-Punkt nehmen (Server-Befehl: `set-reset-point`)
- **BACKUP NOW**: aktuellen Server-Spielstand sichern (Server-Befehl: `backup`)
- **Backups** mit Datum/Uhrzeit, **RESTORE** holt einen älteren Stand zurück (der aktuelle wird vorher gesichert)
- Jede Aktion mit Sicherheitsabfrage; nur für Admins (vom Server geprüft)
**Pro Spieler**
- Hunger, Durst, Stress, Harndrang, Müdigkeit und Schmutz werden für jeden Spieler einzeln auf dem Server gespeichert
und beim Beitreten zurückgegeben (nicht mehr die Werte aus dem gemeinsamen Spielstand)
**Spielerfiguren**
- Springen: die ganze Figur hebt ab (vorher ging nur der Oberkörper nach oben), Knie werden angezogen
- Runder Gürtel, runde Schultern, abgerundete Stiefel mit Sohle
**Hinweise**
- Kleine Benachrichtigung oben rechts, wenn jemand beitritt oder das Spiel verlässt
+1
View File
@@ -80,6 +80,7 @@
<Compile Include="HingeSync.cs" />
<Compile Include="ControlSync.cs" />
<Compile Include="Toasts.cs" />
<Compile Include="PlayerNeeds.cs" />
<Compile Include="MpCommand.cs" />
<Compile Include="Commands.cs" />
<Compile Include="MscoLog.cs" />
+19
View File
@@ -66,6 +66,8 @@ namespace MSCO
readonly HingeSync hinges = new HingeSync();
readonly ControlSync controls = new ControlSync();
readonly Toasts toasts = new Toasts();
float nextNeedsSend;
const float NeedsInterval = 10f;
void ToastJoined(string name) { toasts.Add(name, "joined the game", UiTheme.Online); }
void ToastLeft(string name) { toasts.Add(name, "left the game", UiTheme.Muted); }
@@ -1685,6 +1687,17 @@ namespace MSCO
try { SendGame(MsgType.Player, BuildLocalState().Serialize()); }
catch (System.Exception e) { ReportError("player state", e); }
}
// Eigene Bedürfnisse (Hunger, Durst ...) pro Spieler auf dem Server sichern
if (dedicated && worldSynced && Time.realtimeSinceStartup >= nextNeedsSend)
{
nextNeedsSend = Time.realtimeSinceStartup + NeedsInterval;
try
{
byte[] needs = PlayerNeeds.Build();
if (needs != null) session.SendTo(NetSession.HostId, MsgType.PlayerNeeds, needs);
}
catch (System.Exception e) { ReportError("player needs", e); }
}
try { objects.Update(localPlayer, Time.deltaTime); }
catch (System.Exception e) { ReportError("object sync", e); }
try { vehicleFx.Update(objects, SendGame, Time.deltaTime, IsWorldAuthority); }
@@ -1888,6 +1901,12 @@ namespace MSCO
int destroyed = r.ReadInt32();
for (int i = 0; i < destroyed; i++) destroyedIds.Add(r.ReadInt32());
}
// Eigene Bedürfnisse (Hunger, Durst ...) - statt der Werte aus dem gemeinsamen Spielstand
if (r.BaseStream.Length - r.BaseStream.Position >= 1 && r.ReadBoolean())
{
int needs = PlayerNeeds.Apply(r);
if (needs > 0) MscoLog.Info("[MSCO] Your own hunger/thirst/... restored (" + needs + " values)");
}
// Abgebaute Teile und gespawnte Items sind jetzt eigene Objekte: erfassen, dann alle Positionen setzen
yield return null;
+3 -2
View File
@@ -34,7 +34,7 @@ namespace MSCO
AdminStatus = 23, // Server -> Client: bool, ist Admin
AdminListRequest = 24, // Client -> Server: Spielerliste fürs Admin-Menü
AdminList = 25, // Server -> Client: int Anzahl, je [int id, string name, string steamId, bool admin]
AdminAction = 26, // Client -> Server: byte (1 kick, 2 ban, 3 bring, 4 unban, 5 reset world, 6 backup, 7 restore backup), int Spieler-ID, string Grund/Name
AdminAction = 26, // Client -> Server: byte (1 kick, 2 ban, 3 bring, 4 unban, 5 reset world, 6 backup, 7 restore backup, 8 set reset point), int Spieler-ID, string Grund/Name
AdminResult = 27, // Server -> Client: string Meldung
Chat = 28, // Chatnachricht (string)
VarSync = 29, // Fahrzeugzustand: int n, je [int key, byte typ, wert]
@@ -51,7 +51,8 @@ namespace MSCO
TrafficStates = 40, // Verkehr (nur Welt-Autorität): int n, je [int Schlüssel, 3 float Pos, 4 float Rot, 3 float Geschw.]
WorldVars = 41, // Jobs/Laden: int n, je [int Schlüssel, byte Typ (2 int, 3 bool), Wert]
HingeStates = 42, // Autotüren/Haube/Kofferraum: int n, je [int Schlüssel, float Scharnier-Winkel]
VehicleControls = 43 // Zündung/Motor/Licht ...: int n, je [int FSM-Schlüssel, string Zustand]
VehicleControls = 43, // Zündung/Motor/Licht ...: int n, je [int FSM-Schlüssel, string Zustand]
PlayerNeeds = 44 // Client -> Server: eigene Bedürfnisse (Hunger, Durst ...): int n, je [string Name, float Wert]
}
public struct Message
+11 -2
View File
@@ -405,7 +405,7 @@ namespace MSCO
GUILayout.Label("World reset and save backups are only available on a dedicated server.", small);
return;
}
GUILayout.Label("Everyone reconnects after a reset or restore. The current save is backed up first.", small);
GUILayout.Label("Everyone reconnects after a reset or restore. The current state is backed up first.", small);
GUILayout.Space(8);
GUILayout.BeginHorizontal();
@@ -418,9 +418,18 @@ namespace MSCO
else if (GUILayout.Button("RESET WORLD", danger, GUILayout.Height(32), GUILayout.Width(170))) confirmWorld = "reset";
GUILayout.Space(10);
if (GUILayout.Button("BACKUP NOW", primary, GUILayout.Height(32), GUILayout.Width(150))) mp.WorldAction(6, null);
GUILayout.Space(6);
if (confirmWorld == "point")
{
if (GUILayout.Button("REALLY?", danger, GUILayout.Height(32), GUILayout.Width(100))) { mp.WorldAction(8, null); confirmWorld = null; }
GUILayout.Space(4);
if (GUILayout.Button("CANCEL", secondary, GUILayout.Height(32), GUILayout.Width(90))) confirmWorld = null;
}
else if (GUILayout.Button("SET RESET POINT", secondary, GUILayout.Height(32), GUILayout.Width(170))) confirmWorld = "point";
GUILayout.FlexibleSpace();
GUILayout.EndHorizontal();
GUILayout.Label("Reset: all moved objects, bought items, doors and vehicle values go back to the server's save game.", small);
GUILayout.Label("Reset: the whole world goes back to the save game as it was uploaded to the server (the reset point). " +
"SET RESET POINT makes the current save the new reset point.", small);
GUILayout.Space(10);
GUILayout.Label("SAVE BACKUPS (NEWEST FIRST)", small);
+55
View File
@@ -0,0 +1,55 @@
using System.Collections.Generic;
using System.IO;
using HutongGames.PlayMaker;
namespace MSCO
{
/// <summary>
/// Bedürfnisse pro Spieler (Hunger, Durst, Stress, Harndrang, Müdigkeit, Schmutz): Im gemeinsamen Spielstand gibt es
/// sie nur einmal - ohne das hätte jeder beim Beitreten die Werte desjenigen, der den Spielstand zuletzt hochgeladen
/// hat. Deshalb schickt jeder seine eigenen Werte regelmäßig an den Server, der sie pro Spieler speichert und beim
/// nächsten Beitreten zurückgibt.
/// </summary>
public static class PlayerNeeds
{
public static readonly string[] Names =
{
"PlayerHunger", "PlayerThirst", "PlayerStress", "PlayerUrine", "PlayerFatigue", "PlayerDirtiness"
};
/// <summary>Eigene Werte: int n, je [string Name, float Wert]. null, wenn das Spiel sie (noch) nicht hat.</summary>
public static byte[] Build()
{
var values = new List<KeyValuePair<string, float>>();
foreach (string name in Names)
{
FsmFloat f = FsmVariables.GlobalVariables.FindFsmFloat(name);
if (f != null && !float.IsNaN(f.Value) && !float.IsInfinity(f.Value)) values.Add(new KeyValuePair<string, float>(name, f.Value));
}
if (values.Count == 0) return null;
return NetSession.Build(w =>
{
w.Write(values.Count);
foreach (KeyValuePair<string, float> v in values) { w.Write(v.Key); w.Write(v.Value); }
});
}
/// <summary>Gespeicherte Werte vom Server übernehmen (Format wie Build).</summary>
public static int Apply(BinaryReader r)
{
int count = r.ReadInt32();
int applied = 0;
for (int i = 0; i < count; i++)
{
string name = r.ReadString();
float value = r.ReadSingle();
if (System.Array.IndexOf(Names, name) < 0 || float.IsNaN(value) || float.IsInfinity(value)) continue;
FsmFloat f = FsmVariables.GlobalVariables.FindFsmFloat(name);
if (f == null) continue;
f.Value = value;
applied++;
}
return applied;
}
}
}
+28 -15
View File
@@ -21,6 +21,7 @@ namespace MSCO
const float TorsoLength = 0.55f; // Hüfte bis Hals
const float HipWidth = 0.1f, ShoulderWidth = 0.21f;
const float AnkleHeight = 0.07f;
const float LegReach = ThighLength + ShinLength + AnkleHeight - 0.02f; // Hüfthöhe bei gestrecktem Bein
static readonly Color Skin = new Color(0.86f, 0.66f, 0.52f);
static readonly Color Jeans = new Color(0.2f, 0.26f, 0.38f);
@@ -39,7 +40,7 @@ namespace MSCO
Transform pelvis, torso, belt, neck, belly, collar, placket, buckle;
readonly Transform[] thigh = new Transform[2], shin = new Transform[2], boot = new Transform[2];
readonly Transform[] upperArm = new Transform[2], forearm = new Transform[2], hand = new Transform[2];
readonly Transform[] sole = new Transform[2], cuff = new Transform[2];
readonly Transform[] sole = new Transform[2], cuff = new Transform[2], shoulderBall = new Transform[2];
Vector3 targetFeet;
Vector3 targetHead;
@@ -50,6 +51,7 @@ namespace MSCO
Vector3 lastPosition;
float walkPhase, walkAmount, speed;
float airAmount; // 0 = steht am Boden, 1 = in der Luft (Sprung, Fallen)
public int HeldId { get { return last.HeldId; } }
public PlayerState LastState { get { return last; } }
@@ -90,7 +92,7 @@ namespace MSCO
pelvis = Capsule("Pelvis", r, Jeans);
torso = Textured(Capsule("Chest", r, Color.white), plaid, new Vector2(4f, 4f));
belly = Textured(Capsule("Belly", r, Color.white), plaid, new Vector2(4f, 2f));
belt = Box("Belt", r, new Color(0.2f, 0.13f, 0.08f));
belt = Primitive(PrimitiveType.Cylinder, "Belt", r, new Color(0.2f, 0.13f, 0.08f)); // rund um den Bauch
buckle = Box("Buckle", r, Metal);
placket = Box("Buttons", r, jacketDark);
collar = Capsule("Collar", r, jacketDark);
@@ -100,11 +102,12 @@ namespace MSCO
{
thigh[i] = Capsule("Thigh", r, Jeans);
shin[i] = Capsule("Shin", r, Jeans * 0.92f);
boot[i] = Box("Boot", r, Boots);
sole[i] = Box("Sole", r, Sole);
boot[i] = Capsule("Boot", r, Boots); // abgerundete Arbeitsstiefel statt Klötze
sole[i] = Capsule("Sole", r, Sole);
upperArm[i] = Textured(Capsule("UpperArm", r, Color.white), plaid, new Vector2(2f, 3f));
forearm[i] = Textured(Capsule("Forearm", r, Color.white), plaid, new Vector2(2f, 3f));
cuff[i] = Capsule("Cuff", r, jacketDark);
shoulderBall[i] = Textured(Sphere("Shoulder", r, Color.white, 0.13f), plaid, new Vector2(2f, 2f)); // runde Schulter
hand[i] = Sphere("Hand", r, Skin, 0.1f);
}
@@ -292,6 +295,7 @@ namespace MSCO
// Hüfte: beim Lehnen nach hinten ausweichen, beim Ducken absinken (Füße bleiben am Boden)
Vector3 hip;
float air = 0f;
if (last.Seated)
{
hip = neckBase + new Vector3(0f, -TorsoLength, -0.08f);
@@ -302,8 +306,12 @@ namespace MSCO
hip = -lean * 0.25f;
float horizontal = (new Vector3(neckBase.x - hip.x, 0f, neckBase.z - hip.z)).magnitude;
float vertical = Mathf.Sqrt(Mathf.Max(0.04f, TorsoLength * TorsoLength - horizontal * horizontal));
hip.y = Mathf.Clamp(neckBase.y - vertical, 0.3f, ThighLength + ShinLength + AnkleHeight - 0.02f);
// Ist der Kopf höher, als die Beine reichen (Sprung, Fallen), hebt die ganze Figur ab - vorher blieben die
// Füße am Boden und nur der Oberkörper ging nach oben
hip.y = Mathf.Max(0.3f, neckBase.y - vertical);
air = Mathf.Max(0f, hip.y - LegReach);
}
airAmount = Mathf.Lerp(airAmount, last.Seated ? 0f : Mathf.Clamp01(air / 0.12f), dt > 0f ? Mathf.Clamp01(dt * 10f) : 1f);
Vector3 up = (neckBase - hip).normalized;
Vector3 right = Vector3.right;
@@ -316,8 +324,8 @@ namespace MSCO
Segment(belly, hip + up * 0.07f + forward * 0.015f, hip + up * 0.3f + forward * 0.015f, 0.33f, 0.25f);
belt.localPosition = hip + up * 0.06f;
belt.localRotation = torsoRot;
belt.localScale = new Vector3(0.345f, 0.045f, 0.275f);
buckle.localPosition = hip + up * 0.06f + forward * 0.14f;
belt.localScale = new Vector3(0.34f, 0.024f, 0.27f); // Zylinder: 0.048 m hoch, elliptisch
buckle.localPosition = hip + up * 0.06f + forward * 0.136f;
buckle.localRotation = torsoRot;
buckle.localScale = new Vector3(0.055f, 0.04f, 0.012f);
placket.localPosition = Vector3.Lerp(hip + up * 0.12f, neckBase, 0.6f) + forward * 0.124f;
@@ -331,7 +339,7 @@ namespace MSCO
headPivot.localPosition = head;
headPivot.localRotation = Quaternion.Euler(pitch, 0f, 0f);
float swing = Mathf.Sin(walkPhase) * walkAmount;
float swing = Mathf.Sin(walkPhase) * walkAmount * (1f - airAmount);
for (int i = 0; i < 2; i++)
{
float side = i == 0 ? -1f : 1f;
@@ -343,19 +351,24 @@ namespace MSCO
if (last.Seated)
ankle = hipJoint + new Vector3(0f, -0.4f, 0.48f);
else
{
ankle = new Vector3(HipWidth * side, AnkleHeight + Mathf.Max(0f, Mathf.Cos(walkPhase + i * Mathf.PI)) * 0.07f * walkAmount, phase * 0.3f);
// In der Luft: Füße unter der Hüfte mitnehmen und die Knie etwas anziehen
ankle.y += air + 0.1f * airAmount;
ankle.z -= 0.06f * airAmount;
}
Vector3 knee = Joint(hipJoint, ankle, ThighLength, ShinLength, Vector3.forward);
Segment(thigh[i], hipJoint, knee, 0.15f, 0.16f);
Segment(shin[i], knee, ankle, 0.12f, 0.13f);
boot[i].localPosition = ankle + new Vector3(0f, -0.015f, 0.05f);
boot[i].localRotation = Quaternion.identity;
boot[i].localScale = new Vector3(0.12f, 0.1f, 0.27f);
sole[i].localPosition = ankle + new Vector3(0f, -0.065f, 0.05f);
sole[i].localRotation = Quaternion.identity;
sole[i].localScale = new Vector3(0.13f, 0.025f, 0.29f);
// Stiefel: Kapsel von der Ferse zur Spitze, darunter eine flache Sohle
Segment(boot[i], ankle + new Vector3(0f, -0.025f, -0.06f), ankle + new Vector3(0f, -0.03f, 0.14f), 0.12f, 0.1f);
Segment(sole[i], ankle + new Vector3(0f, -0.07f, -0.075f), ankle + new Vector3(0f, -0.07f, 0.155f), 0.125f, 0.03f);
// Arme
Vector3 shoulder = neckBase - up * 0.06f + right * (ShoulderWidth * side);
shoulderBall[i].localPosition = shoulder + up * 0.005f;
shoulderBall[i].localRotation = torsoRot;
shoulderBall[i].localScale = new Vector3(0.125f, 0.12f, 0.13f);
Vector3 handTarget;
Vector3 heldPos;
Quaternion heldRot;
@@ -364,7 +377,7 @@ namespace MSCO
else if (last.Seated)
handTarget = shoulder + new Vector3(-side * 0.05f, -0.3f, 0.38f); // Hände nach vorne (Lenkrad)
else
handTarget = shoulder + new Vector3(side * 0.06f, -(UpperArmLength + ForearmLength) + 0.05f, 0.02f - phase * 0.22f);
handTarget = shoulder + new Vector3(side * (0.06f + 0.12f * airAmount), -(UpperArmLength + ForearmLength) + 0.05f + 0.14f * airAmount, 0.02f - phase * 0.22f);
// Nicht weiter als die Armlänge
Vector3 reach = handTarget - shoulder;