- Passenger: auto ride-along only when the vehicle moves (speed from its motion, also for kinematic remote cars) - no more getting stuck while working in the engine bay or re-captured - Bolts with a function (drain plug: stage 0-5 = oil leak on) are synced as stage and applied through the bolt's own stage logic; world authority sends its state once after joining. Fixes endless oil leaking when players had the drain plug in different states - Admin menu VEHICLE tab: fluids with FILL/EMPTY, all Database parts with wear/damage, REPAIR/BREAK per part, REPAIR ALL, filter - Server profile: info and players as two equal cards, no wrapping - fsmdump also lists FSM variables Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
481 lines
21 KiB
C#
481 lines
21 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Text;
|
|
using HutongGames.PlayMaker;
|
|
using UnityEngine;
|
|
|
|
namespace MSCO
|
|
{
|
|
/// <summary>
|
|
/// Gesamtzustand der Fahrzeuge (vor allem Satsuma): Werte in den PlayMaker-FSMs unter dem Fahrzeug,
|
|
/// z.B. Tankinhalt, Öl, Kühlwasser, Bremsflüssigkeit, Batterieladung, Schrauben-Festigkeit,
|
|
/// Schäden/Verschleiß, Kilometerstand, Lack, Einstellungen (Vergaser, Zündung ...).
|
|
/// Wer das Fahrzeug gerade besitzt (fährt/schiebt), gibt die Werte vor; ist es frei,
|
|
/// schickt jeder seine Änderungen (z.B. Öl nachfüllen, Schraube festziehen).
|
|
/// </summary>
|
|
public class VehicleStateSync
|
|
{
|
|
// Nur Werte, die den Zustand beschreiben - nicht Drehzahl, Lenkung, Raycasts usw.
|
|
static readonly string[] Keywords =
|
|
{
|
|
"fuel", "oil", "coolant", "water", "fluid", "brake", "clutchfluid", "battery", "charge",
|
|
"damage", "wear", "condition", "odo", "km", "dirt", "paint", "color", "colour",
|
|
"tune", "tuning", "ratio", "broken", "fixed", "flat", "pressure", "level", "mixture", "timing",
|
|
"idle", "air", "jet", "spark", "gap", "rust", "health", "burn", "cool", "temp"
|
|
};
|
|
|
|
// Montiert-Flags nicht direkt setzen: das echte Anbauen läuft über FsmSync (Events)
|
|
// Schrauben/Festigkeit laufen über FsmSync (Events) - ein falscher Wert hier würde Teile lockern und abfallen lassen
|
|
// Vom Spiel selbst berechnete Werte ebenfalls nicht: z.B. LeakOilpan/LeakOilfilter (leckt Öl?) ergibt sich aus
|
|
// angebauten/festgezogenen Teilen - ein übertragener alter Wert ließ beim Beitreten Öl auslaufen. Raten (…Rate)
|
|
// rechnet das Spiel laufend neu aus.
|
|
static readonly string[] Excluded =
|
|
{
|
|
"install", "bolt", "tight", "detach", "attach", "rpm", "speed", "steer", "throttle", "gear", "raycast", "distance",
|
|
"leak", "rate",
|
|
// Spieler-Werte (Database/PlayerDatabase: Schmutz, Verbrennungen ...) sind pro Spieler; Fluch-Geräusche,
|
|
// Ton-Verzerrung und Anzeige-Hilfswerte (GUI) rechnet jeder selbst
|
|
"player", "swear", "distortion", "gui"
|
|
};
|
|
|
|
// Wurzeln außer den Fahrzeugen: Satsuma-Teile (lose und angebaut) und ihre Datenbank (Verschleiß, beschädigt,
|
|
// Lack, Füllstände ...). Maßgeblich ist dort wie beim Satsuma selbst, wer den Satsuma gerade fährt.
|
|
static readonly string[] PartRoots = { "CARPARTS", "Database" };
|
|
const string PartOwnerPrefix = "SATSUMA";
|
|
|
|
const float ScanInterval = 10f;
|
|
const float CheckInterval = 0.5f;
|
|
const int MaxPerMessage = 200;
|
|
|
|
class Var
|
|
{
|
|
public int Key;
|
|
public string Name; // Pfad|FSM|Variable (für die Diagnose "vars")
|
|
public string Root;
|
|
public NamedVariable Variable;
|
|
public byte Type; // 1 float, 2 int, 3 bool (übertragen); Farben als 4 einzelne Kanäle vom Typ 1
|
|
public int Channel = -1; // FsmColor: 0-3 = r/g/b/a
|
|
public PlayMakerFSM Fsm;
|
|
public float LastF;
|
|
public int LastI;
|
|
public bool LastB;
|
|
// Schraube mit Funktion (z.B. Ablassschraube der Ölwanne: offen = Öl läuft aus): die Stufe wird als Zustand
|
|
// übertragen und über die Stufen-Logik der Schraube angewendet (Position, Dichtung, Zähler des Teils)
|
|
public FsmState BoltDispatch;
|
|
public bool InitialSent;
|
|
}
|
|
|
|
readonly Dictionary<int, Var> vars = new Dictionary<int, Var>();
|
|
readonly Dictionary<int, byte[]> pending = new Dictionary<int, byte[]>(); // noch nicht gefundene Werte
|
|
readonly List<Var> outgoing = new List<Var>();
|
|
// Erfasste FSMs mit dem Elternobjekt beim Erfassen: wird ein Teil an-/abgebaut, ändert sich sein Pfad (und damit
|
|
// die Schlüssel) - dann neu erfassen. FSMs außerhalb der Fahrzeuge/Teile werden bei jedem Suchlauf neu geprüft
|
|
// (vorher einmal als "nicht Fahrzeug" gemerkt und nie wieder angesehen: später angebaute Teile fehlten)
|
|
readonly Dictionary<PlayMakerFSM, Transform> scanned = new Dictionary<PlayMakerFSM, Transform>();
|
|
readonly Dictionary<PlayMakerFSM, List<int>> keysByFsm = new Dictionary<PlayMakerFSM, List<int>>();
|
|
string satsumaRoot;
|
|
float nextScan, nextCheck;
|
|
|
|
public int Count { get { return vars.Count; } }
|
|
|
|
/// <summary>Diagnose (Konsole "vars <filter>"): synchronisierte Fahrzeugwerte mit aktuellem Wert.</summary>
|
|
public List<string> Describe(string filter, int max)
|
|
{
|
|
var lines = new List<string>();
|
|
string f = (filter ?? "").ToLower();
|
|
foreach (Var v in vars.Values)
|
|
{
|
|
if (f.Length > 0 && (v.Name == null || !v.Name.ToLower().Contains(f))) continue;
|
|
string value = v.Type == 1 ? GetFloat(v).ToString("0.###")
|
|
: v.Type == 2 ? ((FsmInt)v.Variable).Value.ToString()
|
|
: ((FsmBool)v.Variable).Value.ToString();
|
|
lines.Add(" " + v.Name + " = " + value);
|
|
if (lines.Count >= max) { lines.Add(" ... (more - use a narrower filter)"); break; }
|
|
}
|
|
if (lines.Count == 0) lines.Add(" no synced vehicle value matches '" + filter + "'");
|
|
return lines;
|
|
}
|
|
|
|
public void Reset()
|
|
{
|
|
vars.Clear();
|
|
pending.Clear();
|
|
scanned.Clear();
|
|
keysByFsm.Clear();
|
|
satsumaRoot = null;
|
|
nextScan = -1f; // erster Suchlauf verteilt, siehe Update
|
|
}
|
|
|
|
public void Update(ObjectSync objects, int localId, Action<MsgType, byte[]> send, bool worldAuthority = false)
|
|
{
|
|
if (nextScan < 0f) nextScan = Time.time + 3.1f; // nicht alle Sync-Teile im selben Frame suchen lassen
|
|
if (Time.time >= nextScan)
|
|
{
|
|
nextScan = Time.time + ScanInterval;
|
|
Scan();
|
|
}
|
|
if (Time.time < nextCheck) return;
|
|
nextCheck = Time.time + CheckInterval;
|
|
|
|
outgoing.Clear();
|
|
ResolveSatsuma(objects);
|
|
foreach (Var v in vars.Values)
|
|
{
|
|
// Fährt/schiebt ein anderer das Fahrzeug, ist seine Version maßgeblich - nichts senden
|
|
int owner = objects.VehicleOwner(OwnerRoot(v.Root));
|
|
if (owner != SyncedBody.NoOwner && owner != localId) { if (Changed(v, false)) Remember(v); continue; }
|
|
|
|
// Eigenes Fahrzeug: jede Änderung. Freies Fahrzeug: nur deutliche Sprünge (Öl nachfüllen, tanken ...) -
|
|
// langsames Abdriften (z.B. Abkühlen) rechnet jeder selbst, sonst überschreiben sich alle gegenseitig
|
|
bool big = owner == SyncedBody.NoOwner && v.BoltDispatch == null;
|
|
if (v.BoltDispatch != null && worldAuthority && !v.InitialSent)
|
|
{
|
|
// Einmal den eigenen Stand vorgeben, damit alle gleich sind (vorher auseinandergelaufene Schrauben)
|
|
v.InitialSent = true;
|
|
Remember(v);
|
|
outgoing.Add(v);
|
|
continue;
|
|
}
|
|
if (!Changed(v, big)) continue;
|
|
Remember(v);
|
|
outgoing.Add(v);
|
|
}
|
|
|
|
for (int start = 0; start < outgoing.Count; start += MaxPerMessage)
|
|
{
|
|
int from = start, count = Math.Min(MaxPerMessage, outgoing.Count - start);
|
|
send(MsgType.VarSync, NetSession.Build(w =>
|
|
{
|
|
w.Write(count);
|
|
for (int i = from; i < from + count; i++) Write(w, outgoing[i]);
|
|
}));
|
|
}
|
|
}
|
|
|
|
public void Handle(int from, BinaryReader r, ObjectSync objects, int localId)
|
|
{
|
|
int count = r.ReadInt32();
|
|
for (int i = 0; i < count; i++)
|
|
{
|
|
int key = r.ReadInt32();
|
|
byte type = r.ReadByte();
|
|
byte[] value = r.ReadBytes(type == 3 ? 1 : 4);
|
|
Var v;
|
|
if (!vars.TryGetValue(key, out v)) { pending[key] = Combine(type, value); continue; }
|
|
if (from >= 0 && objects.VehicleOwner(OwnerRoot(v.Root)) == localId) continue; // ich fahre gerade: meine Werte gelten
|
|
Apply(v, type, value);
|
|
}
|
|
}
|
|
|
|
/// <summary>Gespeicherte Werte vom Server (Snapshot) - auch für Teile, die erst später auftauchen.</summary>
|
|
public void ApplySnapshot(BinaryReader r)
|
|
{
|
|
int count = r.ReadInt32();
|
|
for (int i = 0; i < count; i++)
|
|
{
|
|
int key = r.ReadInt32();
|
|
byte type = r.ReadByte();
|
|
byte[] value = r.ReadBytes(type == 3 ? 1 : 4);
|
|
Var v;
|
|
if (vars.TryGetValue(key, out v)) Apply(v, type, value);
|
|
else pending[key] = Combine(type, value);
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------- intern
|
|
|
|
void Scan()
|
|
{
|
|
int before = vars.Count;
|
|
foreach (PlayMakerFSM fsm in SceneCache.Fsms(20f))
|
|
{
|
|
if (fsm == null) continue;
|
|
// Schon erfasst und nicht umgehängt (an-/abgebaut): nichts zu tun (Pfad + Hash kosten)
|
|
Transform parent;
|
|
if (scanned.TryGetValue(fsm, out parent) && parent == fsm.transform.parent) continue;
|
|
string root = fsm.transform.root.name;
|
|
if (!IsTracked(root)) { Forget(fsm); continue; }
|
|
scanned[fsm] = fsm.transform.parent;
|
|
Forget(fsm);
|
|
if (RegisterFunctionalBolt(fsm, root)) continue;
|
|
// FSMs, die Größen/Positionen setzen (z.B. Füllstands-Anzeigen), NICHT anfassen: ein übertragener Wert
|
|
// kann dort eine Größe 0 ergeben - Unity stürzt dann in der Physik ab ("cleaning the mesh failed")
|
|
if (TouchesTransform(fsm)) continue;
|
|
if (fsm.transform.root.name == "Database" && FsmSync.PathOf(fsm.transform).Contains("PlayerDatabase")) continue;
|
|
string prefix = FsmSync.PathOf(fsm.transform) + "|" + fsm.FsmName + "|";
|
|
FsmVariables fv = fsm.FsmVariables;
|
|
foreach (FsmFloat f in fv.FloatVariables) Register(fsm, prefix, root, f, 1, -1);
|
|
foreach (FsmInt n in fv.IntVariables) Register(fsm, prefix, root, n, 2, -1);
|
|
foreach (FsmBool b in fv.BoolVariables) Register(fsm, prefix, root, b, 3, -1);
|
|
// Lack-/Farbwerte (FsmColor) als vier Zahlen: alte Server/Mods kennen nur float/int/bool
|
|
foreach (FsmColor c in fv.ColorVariables)
|
|
{
|
|
if (c == null || !IsColorName(c.Name)) continue;
|
|
for (int ch = 0; ch < 4; ch++) Register(fsm, prefix, root, c, 1, ch);
|
|
}
|
|
}
|
|
if (vars.Count != before) MscoLog.Info("[MSCO] Vehicle state: " + vars.Count + " values synced");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Schrauben, deren Stufen etwas ein-/ausschalten (ActivateGameObject - z.B. die Ablassschraube: Stufe 0-5 = das
|
|
/// Leck "Oil" ist an). Sonst laufen Schrauben über FsmSync (Ereignisse): ging eins verloren, war die Ablassschraube
|
|
/// bei einem offen und beim anderen zu - bei ihm lief dann endlos Öl aus (der Ölstand kam ja ständig nach).
|
|
/// </summary>
|
|
bool RegisterFunctionalBolt(PlayMakerFSM fsm, string root)
|
|
{
|
|
if (fsm.FsmName != "Screw") return false;
|
|
FsmInt stage = fsm.FsmVariables.FindFsmInt("Stage");
|
|
if (stage == null) return false;
|
|
FsmState dispatch = fsm.Fsm.GetState("Stages 2") ?? fsm.Fsm.GetState("Stages");
|
|
if (dispatch == null) return false;
|
|
bool switches = false;
|
|
try
|
|
{
|
|
foreach (FsmState state in fsm.FsmStates)
|
|
{
|
|
if (!state.ActionsLoaded) state.LoadActions();
|
|
if (state.Actions == null) continue;
|
|
foreach (FsmStateAction a in state.Actions)
|
|
if (a != null && a.GetType().Name == "ActivateGameObject") { switches = true; break; }
|
|
if (switches) break;
|
|
}
|
|
}
|
|
catch { return false; }
|
|
if (!switches) return false;
|
|
|
|
string name = FsmSync.PathOf(fsm.transform) + "|" + fsm.FsmName + "|Stage";
|
|
int key = Hash(name);
|
|
Var existing;
|
|
if (vars.TryGetValue(key, out existing) && existing.Variable == stage) return true;
|
|
var v = new Var { Key = key, Name = name, Root = root, Variable = stage, Type = 2, Fsm = fsm, BoltDispatch = dispatch };
|
|
Remember(v);
|
|
vars[key] = v;
|
|
List<int> keys;
|
|
if (!keysByFsm.TryGetValue(fsm, out keys)) keysByFsm[fsm] = keys = new List<int>();
|
|
keys.Add(key);
|
|
byte[] stored;
|
|
if (pending.TryGetValue(key, out stored))
|
|
{
|
|
pending.Remove(key);
|
|
byte[] value = new byte[stored.Length - 1];
|
|
Array.Copy(stored, 1, value, 0, value.Length);
|
|
Apply(v, stored[0], value);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static readonly string[] TransformActionWords = { "Scale", "Position", "Rotate", "Rotation", "Translate", "Parent" };
|
|
|
|
static bool TouchesTransform(PlayMakerFSM fsm)
|
|
{
|
|
try
|
|
{
|
|
foreach (FsmState state in fsm.FsmStates)
|
|
{
|
|
if (!state.ActionsLoaded) state.LoadActions();
|
|
if (state.Actions == null) continue;
|
|
foreach (FsmStateAction action in state.Actions)
|
|
{
|
|
if (action == null) continue;
|
|
string type = action.GetType().Name;
|
|
foreach (string word in TransformActionWords)
|
|
if (type.Contains(word)) return true;
|
|
}
|
|
}
|
|
}
|
|
catch { return true; } // im Zweifel nicht anfassen
|
|
return false;
|
|
}
|
|
|
|
void Register(PlayMakerFSM fsm, string prefix, string root, NamedVariable variable, byte type, int channel)
|
|
{
|
|
if (variable == null || string.IsNullOrEmpty(variable.Name)) return;
|
|
if (channel < 0 && !Wanted(variable.Name)) return;
|
|
string name = prefix + variable.Name + (channel >= 0 ? "#" + "rgba"[channel] : "");
|
|
int key = Hash(name);
|
|
Var existing;
|
|
if (vars.TryGetValue(key, out existing) && existing.Variable == variable) return;
|
|
var v = new Var { Key = key, Name = name, Root = root, Variable = variable, Type = type, Channel = channel, Fsm = fsm };
|
|
Remember(v);
|
|
vars[key] = v;
|
|
List<int> keys;
|
|
if (!keysByFsm.TryGetValue(fsm, out keys)) keysByFsm[fsm] = keys = new List<int>();
|
|
keys.Add(key);
|
|
|
|
byte[] stored;
|
|
if (pending.TryGetValue(key, out stored))
|
|
{
|
|
pending.Remove(key);
|
|
byte[] value = new byte[stored.Length - 1];
|
|
Array.Copy(stored, 1, value, 0, value.Length);
|
|
Apply(v, stored[0], value);
|
|
}
|
|
}
|
|
|
|
static bool Wanted(string name)
|
|
{
|
|
string n = name.ToLower();
|
|
foreach (string x in Excluded) if (n.Contains(x)) return false;
|
|
foreach (string k in Keywords) if (n.Contains(k)) return true;
|
|
return false;
|
|
}
|
|
|
|
static bool IsVehicle(string root)
|
|
{
|
|
foreach (string v in ObjectSync.VehicleRoots) if (root.StartsWith(v)) return true;
|
|
return false;
|
|
}
|
|
|
|
static bool IsTracked(string root)
|
|
{
|
|
return IsVehicle(root) || Array.IndexOf(PartRoots, root) >= 0;
|
|
}
|
|
|
|
static bool IsColorName(string name)
|
|
{
|
|
string n = name.ToLower();
|
|
return n.Contains("paint") || n.Contains("color") || n.Contains("colour");
|
|
}
|
|
|
|
/// <summary>Werte einer umgehängten/verschwundenen FSM vergessen (Pfad und damit Schlüssel ändern sich).</summary>
|
|
void Forget(PlayMakerFSM fsm)
|
|
{
|
|
List<int> keys;
|
|
if (!keysByFsm.TryGetValue(fsm, out keys)) return;
|
|
foreach (int k in keys)
|
|
{
|
|
Var v;
|
|
if (vars.TryGetValue(k, out v) && v.Fsm == fsm) vars.Remove(k);
|
|
}
|
|
keysByFsm.Remove(fsm);
|
|
}
|
|
|
|
/// <summary>Satsuma-Teile gehören dem, der den Satsuma fährt.</summary>
|
|
string OwnerRoot(string root)
|
|
{
|
|
return Array.IndexOf(PartRoots, root) >= 0 ? satsumaRoot : root;
|
|
}
|
|
|
|
void ResolveSatsuma(ObjectSync objects)
|
|
{
|
|
if (satsumaRoot != null) return;
|
|
foreach (SyncedBody b in objects.AllVehicles())
|
|
if (b.RootName != null && b.RootName.StartsWith(PartOwnerPrefix)) { satsumaRoot = b.RootName; return; }
|
|
}
|
|
|
|
static float GetFloat(Var v)
|
|
{
|
|
if (v.Channel < 0) return ((FsmFloat)v.Variable).Value;
|
|
Color c = ((FsmColor)v.Variable).Value;
|
|
return c[v.Channel];
|
|
}
|
|
|
|
static void SetFloat(Var v, float f)
|
|
{
|
|
if (v.Channel < 0) { ((FsmFloat)v.Variable).Value = f; return; }
|
|
var cv = (FsmColor)v.Variable;
|
|
Color c = cv.Value;
|
|
c[v.Channel] = Mathf.Clamp(f, 0f, 4f);
|
|
cv.Value = c;
|
|
ShowPaint(v, c);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Lack sichtbar machen: Die Lackier-FSM eines Teils setzt beim Lackieren die Materialfarbe ihres eigenen Meshes.
|
|
/// Beim Mitspieler kommt nur der Farbwert an - also dieselbe Farbe aufs Mesh des Teils legen.
|
|
/// </summary>
|
|
static void ShowPaint(Var v, Color c)
|
|
{
|
|
if (v.Fsm == null || v.Fsm.FsmName.IndexOf("paint", StringComparison.OrdinalIgnoreCase) < 0) return;
|
|
Renderer r = v.Fsm.GetComponent<Renderer>();
|
|
if (r == null || r.material == null) return;
|
|
r.material.color = c;
|
|
}
|
|
|
|
static bool Changed(Var v, bool bigOnly)
|
|
{
|
|
switch (v.Type)
|
|
{
|
|
case 1:
|
|
float f = GetFloat(v);
|
|
float delta = Mathf.Abs(f - v.LastF);
|
|
if (v.Channel >= 0) return delta > 0.002f; // Farben: jede Änderung (Lackieren)
|
|
return bigOnly
|
|
? delta > Mathf.Max(0.5f, Mathf.Abs(v.LastF) * 0.05f)
|
|
: delta > Mathf.Max(0.01f, Mathf.Abs(v.LastF) * 0.002f);
|
|
case 2: return ((FsmInt)v.Variable).Value != v.LastI;
|
|
default: return ((FsmBool)v.Variable).Value != v.LastB;
|
|
}
|
|
}
|
|
|
|
static void Remember(Var v)
|
|
{
|
|
switch (v.Type)
|
|
{
|
|
case 1: v.LastF = GetFloat(v); break;
|
|
case 2: v.LastI = ((FsmInt)v.Variable).Value; break;
|
|
default: v.LastB = ((FsmBool)v.Variable).Value; break;
|
|
}
|
|
}
|
|
|
|
static void Apply(Var v, byte type, byte[] value)
|
|
{
|
|
if (type != v.Type) return;
|
|
switch (type)
|
|
{
|
|
case 1:
|
|
float f = BitConverter.ToSingle(value, 0);
|
|
if (float.IsNaN(f) || float.IsInfinity(f)) return; // nie ungültige Werte übernehmen
|
|
SetFloat(v, f);
|
|
break;
|
|
case 2:
|
|
int n = BitConverter.ToInt32(value, 0);
|
|
if (v.BoltDispatch != null)
|
|
{
|
|
if (n < 0 || n > 8 || ((FsmInt)v.Variable).Value == n) { Remember(v); return; }
|
|
((FsmInt)v.Variable).Value = n;
|
|
if (v.Fsm != null && v.Fsm.gameObject.activeInHierarchy) FsmSync.SwitchStateSilently(v.Fsm, v.BoltDispatch);
|
|
if (FsmSync.Debug) MscoLog.Info("[MSCO] << BOLT " + v.Name + " -> " + n);
|
|
break;
|
|
}
|
|
((FsmInt)v.Variable).Value = n;
|
|
break;
|
|
default: ((FsmBool)v.Variable).Value = value[0] != 0; break;
|
|
}
|
|
Remember(v); // nicht als eigene Änderung zurückschicken
|
|
}
|
|
|
|
static void Write(BinaryWriter w, Var v)
|
|
{
|
|
w.Write(v.Key);
|
|
w.Write(v.Type);
|
|
switch (v.Type)
|
|
{
|
|
case 1: w.Write(v.LastF); break;
|
|
case 2: w.Write(v.LastI); break;
|
|
default: w.Write(v.LastB); break;
|
|
}
|
|
}
|
|
|
|
static byte[] Combine(byte type, byte[] value)
|
|
{
|
|
var all = new byte[value.Length + 1];
|
|
all[0] = type;
|
|
Array.Copy(value, 0, all, 1, value.Length);
|
|
return all;
|
|
}
|
|
|
|
static int Hash(string s)
|
|
{
|
|
unchecked
|
|
{
|
|
uint h = 2166136261;
|
|
foreach (byte c in Encoding.UTF8.GetBytes(s)) { h ^= c; h *= 16777619; }
|
|
return (int)h;
|
|
}
|
|
}
|
|
}
|
|
}
|