using Cryville.Common; using Cryville.Common.Buffers; using Cryville.Common.Collections.Generic; using Cryville.Common.Collections.Specialized; using Cryville.Common.Pdt; using Cryville.Crtr.Event; using System; using System.Collections.Generic; using System.Globalization; using System.Runtime.Serialization; using System.Text.Formatting; using UnityEngine; using UnsafeIL; namespace Cryville.Crtr.Ruleset { internal struct JudgeResult { public float? Time { get; set; } public Vector4 Vector { get; set; } } internal class JudgeEvent { public double StartTime { get; set; } public double EndTime { get; set; } public double StartClip { get; set; } public double EndClip { get; set; } public Chart.Judge BaseEvent { get; set; } public JudgeDefinition Definition { get; set; } public NoteHandler Handler { get; set; } public JudgeResult JudgeResult { get; set; } public JudgeCallContext CallContext { get; set; } } internal struct JudgeCallContext { public bool CalledOnMiss { get; set; } public float CallTime { get; set; } public JudgeEvent ReturnEvent { get; set; } public int ReturnIndex { get; set; } } internal interface IJudge { void Call(JudgeEvent ev, float time, Identifier id, bool onMiss, int index); bool Pass(JudgeEvent ev, float time, Identifier[] ids, bool onMiss, int depth); void UpdateScore(ScoreOperation op, PdtExpression exp); } public class Judge : IJudge { #region Data readonly ChartPlayer _sys; internal readonly PdtEvaluator _etor; readonly PdtRuleset _rs; internal Dictionary _areaFuncs; readonly Dictionary> evs = new(); readonly Dictionary> activeEvs = new(); static readonly int _var_pause = IdentifierManager.Shared.Request("pause"); readonly JudgeDefinition _judgePause; static readonly IComparer _stcmp = new JudgeEventStartTimeComparer(); class JudgeEventStartTimeComparer : IComparer { public int Compare(JudgeEvent x, JudgeEvent y) { return x.StartClip.CompareTo(y.StartClip); } } public Judge(ChartPlayer sys, PdtRuleset rs) { _sys = sys; _etor = new PdtEvaluator { ContextJudge = this }; _rs = rs; _areaFuncs = rs.areas; _identop = new PropOp.Identifier(v => _identbuf = new Identifier(v)); _clipop = new PropOp.Clip(v => _clipbuf = v); _rs.judges.TryGetValue(new Identifier(_var_pause), out _judgePause); foreach (var i in rs.inputs) { var id = i.Key; var l = new List(); evs.Add(id, l); activeEvs.Add(id, new List()); if (_judgePause != null && id.Key == _var_pause) { l.Add(new JudgeEvent { StartTime = double.NegativeInfinity, EndTime = double.PositiveInfinity, StartClip = double.NegativeInfinity, EndClip = double.PositiveInfinity, Definition = _judgePause, }); } } InitJudges(); InitScores(); } Identifier _identbuf; readonly PropOp _identop; Clip _clipbuf; readonly PropOp _clipop; public void Prepare(StampedEvent sev, NoteHandler handler) { var tev = (Chart.Judge)sev.Unstamped; InsertEvent(tev, new Clip((float)sev.Time, (float)(sev.Time + sev.Duration)), tev.Id, handler); } void InsertEvent(Chart.Judge ev, Clip clip, Identifier id, NoteHandler handler, JudgeCallContext call = default) { if (id.Key == _var_pause) throw new InvalidOperationException("Cannot assign the special judge \"pause\" to notes"); if (!_rs.judges.TryGetValue(id, out JudgeDefinition def)) { throw new ArgumentException(string.Format("The chart uses a judge named \"{0}\" that is undefined in the ruleset.", id)); } _etor.Evaluate(_identop, def.input); _etor.Evaluate(_clipop, def.clip); var list = evs[_identbuf]; var jev = new JudgeEvent { StartTime = clip.Behind, EndTime = clip.Ahead, StartClip = clip.Behind + _clipbuf.Behind, EndClip = clip.Ahead + _clipbuf.Ahead, BaseEvent = ev, Definition = def, Handler = handler, CallContext = call, }; var index = list.BinarySearch(jev, _stcmp); if (index < 0) index = ~index; list.Insert(index, jev); } #endregion #region Judge internal readonly IntKeyedDictionary judgeMap = new(); void InitJudges() { foreach (var i in _rs.judges) { var id = i.Key; judgeMap.Add(id.Key, IdentifierManager.Shared.Request("judge_" + id.Name)); } } static bool _flag; static readonly PropOp.Boolean _flagop = new(v => _flag = v); static readonly HitOp _hitop = new(); class HitOp : PdtOperator { const int MAX_SORTS = 16; readonly float[] _buf = new float[MAX_SORTS]; readonly float[] _sorts = new float[MAX_SORTS]; public bool LastHit { get; private set; } public JudgeResult JudgeResult { get; private set; } public HitOp() : base(MAX_SORTS) { } protected override unsafe void Execute() { LastHit = false; var judgeResult = new JudgeResult(); var judgeVector = new Vector4(); for (int i = 0; i < LoadedOperandCount; i++) { var op = GetOperand(i); var sort = op.AsNumber(); if (sort <= 0) return; if (!LastHit) { if (sort < _sorts[i]) return; if (sort > _sorts[i]) LastHit = true; } _buf[i] = sort; if (op.Type == PdtInternalType.Vector) { var len = (op.Length - sizeof(int)) / sizeof(float); if (len > 1) { judgeResult.Time = op.AsNumber(sizeof(float)); for (int j = 0; j < 4; j++) { if (len > j + 2) judgeVector[j] = op.AsNumber((j + 2) * sizeof(float)); } judgeResult.Vector = judgeVector; } } } if (!LastHit) return; Array.Clear(_buf, LoadedOperandCount, MAX_SORTS - LoadedOperandCount); fixed (float* ptrsrc = _buf, ptrdest = _sorts) { Unsafe.CopyBlock(ptrdest, ptrsrc, MAX_SORTS * sizeof(float)); } JudgeResult = judgeResult; } public void Clear() { Array.Clear(_sorts, 0, MAX_SORTS); } } static readonly int _var_fn = IdentifierManager.Shared.Request("judge_time_from"); static readonly int _var_tn = IdentifierManager.Shared.Request("judge_time_to"); static readonly int _var_ft = IdentifierManager.Shared.Request("input_time_from"); static readonly int _var_tt = IdentifierManager.Shared.Request("input_time_to"); readonly PropStores.Float _numst1 = new(), _numst2 = new(), _numst3 = new(), _numst4 = new(); static readonly int _var_jt = IdentifierManager.Shared.Request("hit_time"); static readonly int _var_jdt = IdentifierManager.Shared.Request("hit_delta_time"); static readonly int _var_jv = IdentifierManager.Shared.Request("hit_vec"); readonly PropStores.Float _jnumst = new(), _jdnumst = new(); readonly PropStores.Vector4 _jvecst = new(); // Adopted from System.Collections.Generic.ArraySortHelper.InternalBinarySearch(T[] array, int index, int length, T value, IComparer comparer) int BinarySearch(List list, float time, int stack) { int num = 0; int num2 = list.Count - 1; while (num <= num2) { int num3 = num + (num2 - num >> 1); int num4 = -list[num3].Definition.stack.CompareTo(stack); if (num4 == 0) num4 = list[num3].StartClip.CompareTo(time); if (num4 == 0) return num3; else if (num4 < 0) num = num3 + 1; else num2 = num3 - 1; } return ~num; } int BinarySearchFirst(List list, int stack) { if (list[0].Definition.stack == stack) return 0; int num = 0; int num2 = list.Count - 1; while (num <= num2) { int num3 = num + (num2 - num >> 1); int num4 = -list[num3].Definition.stack.CompareTo(stack); if (num4 > 0) num2 = num3 - 1; else if (num4 < 0) num = num3 + 1; else if (num != num3) num2 = num3; else return num; } return ~num; } void UpdateContextJudgeEvent(JudgeEvent ev) { _numst1.Value = (float)ev.StartTime; _etor.ContextCascadeUpdate(_var_fn, _numst1.Source); _numst2.Value = (float)ev.EndTime; _etor.ContextCascadeUpdate(_var_tn, _numst2.Source); if (ev.BaseEvent != null) { _etor.ContextEvent = ev.BaseEvent; _etor.ContextState = ev.Handler.cs; } var call = ev.CallContext; if (call.ReturnEvent != null) { JudgeResult judgeResult = call.ReturnEvent.JudgeResult; _jnumst.Value = judgeResult.Time.Value; _etor.ContextCascadeUpdate(_var_jt, _jnumst.Source); _jdnumst.Value = (float)(judgeResult.Time.Value - call.ReturnEvent.StartTime); _etor.ContextCascadeUpdate(_var_jdt, _jdnumst.Source); _jvecst.Value = judgeResult.Vector; _etor.ContextCascadeUpdate(_var_jv, _jvecst.Source); } else { _etor.ContextCascadeUpdate(_var_jt, PropSrc.Null); _etor.ContextCascadeUpdate(_var_jv, PropSrc.Null); } } public void Feed(Identifier target, float ft, float tt) { Forward(target, tt); var actlist = activeEvs[target]; if (actlist.Count > 0) { _numst3.Value = ft; _etor.ContextCascadeUpdate(_var_ft, _numst3.Source); _numst4.Value = tt; _etor.ContextCascadeUpdate(_var_tt, _numst4.Source); int index = 0, iter = 0; while (index < actlist.Count) { if (iter++ >= 16) throw new JudgePropagationException(); _hitop.Clear(); int cstack = actlist[index].Definition.stack; int hitIndex = -1; while (index >= 0 && index < actlist.Count) { var ev = actlist[index]; if (ev.Definition.stack != cstack) break; UpdateContextJudgeEvent(ev); var def = ev.Definition; if (def.hit != null) { _etor.Evaluate(_hitop, def.hit); if (_hitop.LastHit) { hitIndex = index; ev.JudgeResult = _hitop.JudgeResult; } } else if (hitIndex == -1) hitIndex = index; index++; } if (hitIndex != -1) { var hitEvent = actlist[hitIndex]; UpdateContextJudgeEvent(hitEvent); var def = hitEvent.Definition; if (def == _judgePause) _sys.TogglePause(); if (def.persist != null) _etor.Evaluate(_flagop, def.persist); else _flag = false; Execute(hitEvent, (ft + tt) / 2, def.on_hit, false); if (!_flag) { actlist.RemoveAt(hitIndex); --index; } if (def.prop != 0 && actlist.Count > 0) { index = BinarySearchFirst(actlist, def.stack - def.prop); if (index < 0) index = ~index; } } } _etor.ContextState = null; _etor.ContextEvent = null; } } public void Cleanup(Identifier target, float tt) { lock (_etor) { Forward(target, tt); var actlist = activeEvs[target]; for (int i = actlist.Count - 1; i >= 0; i--) { JudgeEvent ev = actlist[i]; if (tt > ev.EndClip) { actlist.RemoveAt(i); Execute(ev, tt, ev.Definition.on_miss, true); } } } } void Forward(Identifier target, float tt) { var list = evs[target]; var actlist = activeEvs[target]; JudgeEvent ev; while (list.Count > 0 && (ev = list[0]).StartClip <= tt) { list.RemoveAt(0); var index = BinarySearch(actlist, (float)ev.StartClip, ev.Definition.stack); if (index < 0) index = ~index; actlist.Insert(index, ev); } } void Execute(JudgeEvent ev, float time, PairList actions, bool onMiss, int depth = 0, int index = 0) { JudgeResult judgeResult = ev.JudgeResult; if (!onMiss && judgeResult.Time != null) { _jnumst.Value = judgeResult.Time.Value; _etor.ContextCascadeUpdate(_var_jt, _jnumst.Source); _jdnumst.Value = (float)(judgeResult.Time.Value - ev.StartTime); _etor.ContextCascadeUpdate(_var_jdt, _jdnumst.Source); _jvecst.Value = judgeResult.Vector; _etor.ContextCascadeUpdate(_var_jv, _jvecst.Source); } if (actions != null) { // Ensure that all actions that modifies judge result sources break the execution for (int i = index; i < actions.Count; i++) { var a = actions[i]; if (a.Key.Execute(this, ev, time, a.Value, onMiss, depth, i).BreakExecution) break; } } else { var call = ev.CallContext; if (call.ReturnEvent != null) { // TODO if (onMiss) Execute(call.ReturnEvent, time, call.ReturnEvent.Definition.on_miss, true, depth + 1, 0); else Execute(call.ReturnEvent, time, call.ReturnEvent.Definition.on_hit, false, depth + 1, call.ReturnIndex); } } } void IJudge.Call(JudgeEvent ev, float time, Identifier id, bool onMiss, int index) { InsertEvent(ev.BaseEvent, new Clip((float)ev.StartTime, (float)ev.EndTime), id, ev.Handler, new JudgeCallContext { CalledOnMiss = onMiss, CallTime = time, ReturnEvent = ev, ReturnIndex = index + 1, }); // TODO optimize GC } bool IJudge.Pass(JudgeEvent ev, float time, Identifier[] ids, bool onMiss, int depth) { if (depth >= 16) throw new JudgePropagationException(); foreach (var i in ids) { var def = _rs.judges[i]; bool hitFlag; if (def.hit != null) { _hitop.Clear(); _etor.Evaluate(_hitop, def.hit); hitFlag = _hitop.LastHit; } else hitFlag = true; if (hitFlag) { Execute(ev, time, def.on_hit, onMiss, depth + 1); ev.Handler.ReportJudge(ev, time, i); return true; } } return false; } void IJudge.UpdateScore(ScoreOperation op, PdtExpression exp) { _etor.ContextSelfValue = scoreSrcs[op.name.Key]; _etor.Evaluate(scoreOps[op.name.Key], exp); InvalidateScore(op.name.Key); foreach (var s in _rs.scores) { if (s.Value.value != null) { _etor.ContextSelfValue = scoreSrcs[s.Key.Key]; _etor.Evaluate(scoreOps[s.Key.Key], s.Value.value); InvalidateScore(s.Key.Key); } } } #endregion #region Score readonly IntKeyedDictionary scoreStringKeys = new(); readonly IntKeyedDictionary scoreStringKeysRev = new(); readonly IntKeyedDictionary scoreSrcs = new(); readonly IntKeyedDictionary scoreOps = new(); readonly IntKeyedDictionary scoreDefs = new(); readonly IntKeyedDictionary scores = new(); readonly IntKeyedDictionary scoreStringCache = new(); readonly ArrayPool scoreStringPool = new(); readonly IntKeyedDictionary scoreFormatCache = new(); readonly StringBuffer scoreFullBuf = new(); void InitScores() { foreach (var s in _rs.scores) { var key = s.Key.Key; var strkey = IdentifierManager.Shared.Request("_score_" + (string)s.Key.Name); scoreStringKeys.Add(key, strkey); scoreStringKeysRev.Add(strkey, key); scoreSrcs.Add(key, new PropSrc.Float(() => scores[key])); scoreOps.Add(key, new PropOp.Float(v => scores[key] = v)); scoreDefs.Add(key, s.Value); scores.Add(key, s.Value.init); scoreStringCache.Add(scoreStringKeys[key], null); scoreSrcs.Add(scoreStringKeys[key], new ScoreStringSrc(scoreStringPool, () => scores[key], scoreDefs[key].format)); scoreFormatCache[key] = string.Format("{{0:{0}}}", s.Value.format); } } void InvalidateScore(int key) { scoreSrcs[key].Invalidate(); scoreStringCache[scoreStringKeys[key]] = null; scoreSrcs[scoreStringKeys[key]].Invalidate(); } public bool TryGetScoreSrc(int key, out PropSrc value) { return scoreSrcs.TryGetValue(key, out value); } public int GetFullFormattedScoreString(ArrayPool bufferPool, out char[] result) { lock (_etor) { bool flag = false; scoreFullBuf.Clear(); foreach (var s in scores) { var id = s.Key; scoreFullBuf.AppendFormat(flag ? "\n{0}: " : "{0}: ", (string)IdentifierManager.Shared.Retrieve(id)); scoreFullBuf.AppendFormat(scoreFormatCache[id], scores[id]); flag = true; } result = bufferPool.Rent(scoreFullBuf.Count); scoreFullBuf.CopyTo(0, result, 0, scoreFullBuf.Count); return scoreFullBuf.Count; } } class ScoreStringSrc : PropSrc { readonly Func _cb; readonly string _format; readonly ArrayPool _pool; readonly StringBuffer _buf = new() { Culture = CultureInfo.InvariantCulture }; public ScoreStringSrc(ArrayPool pool, Func cb, string format) : base(PdtInternalType.String) { _pool = pool; _cb = cb; _format = string.Format("{{0:{0}}}", format); } public override void Invalidate() { if (buf != null) { _pool.Return(buf); base.Invalidate(); } } protected override unsafe void InternalGet() { var src = _cb(); _buf.Clear(); _buf.AppendFormat(_format, src); int strlen = _buf.Count; buf = _pool.Rent(sizeof(int) + strlen * sizeof(char)); fixed (byte* _ptr = buf) { *(int*)_ptr = strlen; char* ptr = (char*)(_ptr + sizeof(int)); _buf.CopyTo(ptr, 0, strlen); } } } #endregion } [Serializable] public class JudgePropagationException : Exception { public JudgePropagationException() : base("Judge propagation limit reached\nThe ruleset has invalid judge definitions") { } public JudgePropagationException(string message) : base(message) { } public JudgePropagationException(string message, Exception inner) : base(message, inner) { } protected JudgePropagationException(SerializationInfo info, StreamingContext context) : base(info, context) { } } }