unity error
This commit is contained in:
@@ -0,0 +1,301 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using Mirror;
|
||||
using UnityEngine;
|
||||
|
||||
/// <summary>
|
||||
/// Server-only match recorder. Samples poses every FixedUpdate; flushes JSON to disk on stop.
|
||||
/// Admin playback must interpolate by time (Hermite using velocity) — never snap one FixedUpdate sample per render frame.
|
||||
/// </summary>
|
||||
public class ReplayRecorder : MonoBehaviour
|
||||
{
|
||||
public static ReplayRecorder Instance { get; private set; }
|
||||
|
||||
public static bool IsRecording => Instance != null && Instance._recording && !Instance._flushed;
|
||||
|
||||
bool _recording;
|
||||
bool _flushed;
|
||||
int _matchId;
|
||||
bool _isPractice;
|
||||
float _startFixedTime;
|
||||
float _duration;
|
||||
|
||||
readonly List<ReplayEntity> _entities = new List<ReplayEntity>();
|
||||
readonly List<Rigidbody2D> _bodies = new List<Rigidbody2D>();
|
||||
readonly Dictionary<int, int> _instanceIdToEntityId = new Dictionary<int, int>();
|
||||
readonly List<ReplayFrame> _frames = new List<ReplayFrame>();
|
||||
readonly List<ReplayEvent> _events = new List<ReplayEvent>();
|
||||
|
||||
/// <returns>True if recording started (or already recording this session).</returns>
|
||||
public static bool StartRecording(int matchId, GameManager gm)
|
||||
{
|
||||
if (!NetworkServer.active || gm == null)
|
||||
return false;
|
||||
|
||||
if (Instance == null)
|
||||
{
|
||||
var go = new GameObject("ReplayRecorder");
|
||||
DontDestroyOnLoad(go);
|
||||
Instance = go.AddComponent<ReplayRecorder>();
|
||||
}
|
||||
|
||||
Instance.Begin(matchId, gm);
|
||||
return true;
|
||||
}
|
||||
|
||||
public static void StopAndFlush()
|
||||
{
|
||||
if (Instance != null)
|
||||
Instance.Flush();
|
||||
}
|
||||
|
||||
public static void RecordHit(string kind, float vol, Component source)
|
||||
{
|
||||
if (!IsRecording)
|
||||
return;
|
||||
Instance.AddHit(kind, vol, source);
|
||||
}
|
||||
|
||||
public static void RecordGoal(Team concedingTeam, int redScore, int blueScore, bool kickoff)
|
||||
{
|
||||
if (!IsRecording)
|
||||
return;
|
||||
Instance.AddEvent(new ReplayEvent
|
||||
{
|
||||
t = Instance.CurrentT(),
|
||||
type = "goal",
|
||||
kind = "",
|
||||
vol = 0f,
|
||||
id = -1,
|
||||
team = concedingTeam.ToString(),
|
||||
redScore = redScore,
|
||||
blueScore = blueScore,
|
||||
kickoff = kickoff
|
||||
});
|
||||
}
|
||||
|
||||
public static void RecordReset(bool kickoff)
|
||||
{
|
||||
if (!IsRecording)
|
||||
return;
|
||||
Instance.AddEvent(new ReplayEvent
|
||||
{
|
||||
t = Instance.CurrentT(),
|
||||
type = "reset",
|
||||
kind = "",
|
||||
vol = 0f,
|
||||
id = -1,
|
||||
team = "",
|
||||
redScore = 0,
|
||||
blueScore = 0,
|
||||
kickoff = kickoff
|
||||
});
|
||||
}
|
||||
|
||||
public static bool TryGetEntityId(Component source, out int entityId)
|
||||
{
|
||||
entityId = -1;
|
||||
if (Instance == null || source == null)
|
||||
return false;
|
||||
return Instance._instanceIdToEntityId.TryGetValue(source.gameObject.GetInstanceID(), out entityId);
|
||||
}
|
||||
|
||||
void Begin(int matchId, GameManager gm)
|
||||
{
|
||||
if (_recording && !_flushed)
|
||||
Flush();
|
||||
|
||||
// Practice / tutorial host uses DedicatedMatchId 0 — mint a unique id so files don't overwrite.
|
||||
_isPractice = matchId <= 0 || GameTutorialManager.tutorialModeEnabled;
|
||||
if (matchId <= 0)
|
||||
matchId = GeneratePracticeMatchId();
|
||||
|
||||
_matchId = matchId;
|
||||
_recording = true;
|
||||
_flushed = false;
|
||||
_startFixedTime = Time.fixedTime;
|
||||
_duration = 0f;
|
||||
_entities.Clear();
|
||||
_bodies.Clear();
|
||||
_instanceIdToEntityId.Clear();
|
||||
_frames.Clear();
|
||||
_events.Clear();
|
||||
|
||||
// Keep log + replay siblings: Logs/{matchId}.txt and Logs/{matchId}.json
|
||||
if (_isPractice)
|
||||
Logger.SetFileName(_matchId.ToString());
|
||||
|
||||
BuildEntities(gm);
|
||||
string mode = _isPractice ? "practice" : "match";
|
||||
Logger.Log($"ReplayRecorder started ({mode}) id {_matchId} with {_entities.Count} entities");
|
||||
}
|
||||
|
||||
/// <summary>Negative unix timestamp so practice ids never collide with positive matchmaker ids.</summary>
|
||||
static int GeneratePracticeMatchId()
|
||||
{
|
||||
long unix = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
||||
if (unix > int.MaxValue)
|
||||
unix %= int.MaxValue;
|
||||
int id = -(int)unix;
|
||||
return id == 0 ? -1 : id;
|
||||
}
|
||||
|
||||
void BuildEntities(GameManager gm)
|
||||
{
|
||||
if (gm.ball != null)
|
||||
{
|
||||
RegisterEntity(gm.ball.gameObject, gm.ball, "ball", "");
|
||||
}
|
||||
|
||||
var sorted = new List<Puck>(gm.pucks);
|
||||
sorted.Sort((a, b) =>
|
||||
{
|
||||
int teamCmp = a.team.CompareTo(b.team);
|
||||
if (teamCmp != 0)
|
||||
return teamCmp;
|
||||
int xCmp = a.transform.position.x.CompareTo(b.transform.position.x);
|
||||
if (xCmp != 0)
|
||||
return xCmp;
|
||||
return a.transform.position.y.CompareTo(b.transform.position.y);
|
||||
});
|
||||
|
||||
foreach (Puck puck in sorted)
|
||||
{
|
||||
if (puck == null || puck.rb == null)
|
||||
continue;
|
||||
RegisterEntity(puck.gameObject, puck.rb, "puck", puck.team.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
void RegisterEntity(GameObject go, Rigidbody2D rb, string type, string team)
|
||||
{
|
||||
int id = _entities.Count;
|
||||
_entities.Add(new ReplayEntity { id = id, type = type, team = team ?? "" });
|
||||
_bodies.Add(rb);
|
||||
_instanceIdToEntityId[go.GetInstanceID()] = id;
|
||||
}
|
||||
|
||||
float CurrentT() => Time.fixedTime - _startFixedTime;
|
||||
|
||||
void FixedUpdate()
|
||||
{
|
||||
if (!_recording || _flushed || !NetworkServer.active)
|
||||
return;
|
||||
|
||||
int n = _bodies.Count;
|
||||
if (n == 0)
|
||||
return;
|
||||
|
||||
var frame = new ReplayFrame
|
||||
{
|
||||
t = CurrentT(),
|
||||
x = new float[n],
|
||||
y = new float[n],
|
||||
vx = new float[n],
|
||||
vy = new float[n]
|
||||
};
|
||||
|
||||
for (int i = 0; i < n; i++)
|
||||
{
|
||||
Rigidbody2D rb = _bodies[i];
|
||||
if (rb == null)
|
||||
continue;
|
||||
Vector2 pos = rb.position;
|
||||
Vector2 vel = rb.linearVelocity;
|
||||
frame.x[i] = pos.x;
|
||||
frame.y[i] = pos.y;
|
||||
frame.vx[i] = vel.x;
|
||||
frame.vy[i] = vel.y;
|
||||
}
|
||||
|
||||
_duration = frame.t;
|
||||
_frames.Add(frame);
|
||||
}
|
||||
|
||||
void AddHit(string kind, float vol, Component source)
|
||||
{
|
||||
int entityId = -1;
|
||||
if (source != null)
|
||||
_instanceIdToEntityId.TryGetValue(source.gameObject.GetInstanceID(), out entityId);
|
||||
|
||||
AddEvent(new ReplayEvent
|
||||
{
|
||||
t = CurrentT(),
|
||||
type = "hit",
|
||||
kind = kind ?? "",
|
||||
vol = vol,
|
||||
id = entityId,
|
||||
team = "",
|
||||
redScore = 0,
|
||||
blueScore = 0,
|
||||
kickoff = false
|
||||
});
|
||||
}
|
||||
|
||||
void AddEvent(ReplayEvent ev)
|
||||
{
|
||||
_events.Add(ev);
|
||||
}
|
||||
|
||||
void Flush()
|
||||
{
|
||||
if (!_recording || _flushed)
|
||||
return;
|
||||
|
||||
_flushed = true;
|
||||
_recording = false;
|
||||
_duration = Mathf.Max(_duration, CurrentT());
|
||||
|
||||
var file = new ReplayFile
|
||||
{
|
||||
version = 1,
|
||||
matchId = _matchId,
|
||||
isPractice = _isPractice,
|
||||
fixedDeltaTime = Time.fixedDeltaTime,
|
||||
duration = _duration,
|
||||
entities = _entities.ToArray(),
|
||||
frames = _frames.ToArray(),
|
||||
events = _events.ToArray()
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
string path = ResolveReplayFilePath();
|
||||
string json = JsonUtility.ToJson(file);
|
||||
File.WriteAllText(path, json);
|
||||
Logger.Log($"ReplayRecorder wrote {path} ({_frames.Count} frames, {_events.Count} events, duration {_duration:F2}s)");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.Log("ReplayRecorder flush failed: " + e.Message);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Same directory and basename as the match log, extension <c>.json</c>
|
||||
/// (e.g. <c>Logs/228.txt</c> → <c>Logs/228.json</c>).
|
||||
/// </summary>
|
||||
string ResolveReplayFilePath()
|
||||
{
|
||||
string expectedName = _matchId.ToString();
|
||||
string logPath = Logger.instance != null ? Logger.instance.LogFilePath : null;
|
||||
if (!string.IsNullOrEmpty(logPath))
|
||||
{
|
||||
string logBase = Path.GetFileNameWithoutExtension(logPath);
|
||||
if (logBase == expectedName)
|
||||
return Path.ChangeExtension(logPath, ".json");
|
||||
}
|
||||
|
||||
return Path.Combine(Logger.GetLogsDirectory(), expectedName + ".json");
|
||||
}
|
||||
|
||||
void OnDestroy()
|
||||
{
|
||||
if (Instance == this)
|
||||
{
|
||||
Flush();
|
||||
Instance = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user