Code structure cleanup.
This commit is contained in:
460
Assets/Cryville/Crtr/Ruleset/Judge.cs
Normal file
460
Assets/Cryville/Crtr/Ruleset/Judge.cs
Normal file
@@ -0,0 +1,460 @@
|
||||
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<Identifier, PdtExpression> _areaFuncs;
|
||||
readonly Dictionary<Identifier, List<JudgeEvent>> evs
|
||||
= new Dictionary<Identifier, List<JudgeEvent>>();
|
||||
readonly Dictionary<Identifier, List<JudgeEvent>> activeEvs
|
||||
= new Dictionary<Identifier, List<JudgeEvent>>();
|
||||
static readonly int _var_pause = IdentifierManager.Shared.Request("pause");
|
||||
readonly JudgeDefinition _judgePause;
|
||||
static readonly IComparer<JudgeEvent> _stcmp = new JudgeEventStartTimeComparer();
|
||||
class JudgeEventStartTimeComparer : IComparer<JudgeEvent> {
|
||||
public int Compare(JudgeEvent x, JudgeEvent y) {
|
||||
return x.StartClip.CompareTo(y.StartClip);
|
||||
}
|
||||
}
|
||||
public Judge(ChartPlayer sys, PdtRuleset rs) {
|
||||
_sys = sys;
|
||||
_etor = new PdtEvaluator();
|
||||
_etor.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<JudgeEvent>();
|
||||
evs.Add(id, l);
|
||||
activeEvs.Add(id, new List<JudgeEvent>());
|
||||
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(JudgeCallContext)) {
|
||||
if (id.Key == _var_pause) throw new InvalidOperationException("Cannot assign the special judge \"pause\" to notes");
|
||||
var def = _rs.judges[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<int> judgeMap = new IntKeyedDictionary<int>();
|
||||
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 PropOp.Boolean(v => _flag = v);
|
||||
static readonly HitOp _hitop = new HitOp();
|
||||
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 PropStores.Float(),
|
||||
_numst2 = new PropStores.Float(),
|
||||
_numst3 = new PropStores.Float(),
|
||||
_numst4 = new PropStores.Float();
|
||||
|
||||
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 PropStores.Float(),
|
||||
_jdnumst = new PropStores.Float();
|
||||
readonly PropStores.Vector4 _jvecst = new PropStores.Vector4();
|
||||
|
||||
// Adopted from System.Collections.Generic.ArraySortHelper<T>.InternalBinarySearch(T[] array, int index, int length, T value, IComparer<T> comparer)
|
||||
int BinarySearch(List<JudgeEvent> 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<JudgeEvent> 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<JudgeAction, PdtExpression> 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<int> scoreStringKeys = new IntKeyedDictionary<int>();
|
||||
readonly IntKeyedDictionary<int> scoreStringKeysRev = new IntKeyedDictionary<int>();
|
||||
readonly IntKeyedDictionary<PropSrc> scoreSrcs = new IntKeyedDictionary<PropSrc>();
|
||||
readonly IntKeyedDictionary<PropOp> scoreOps = new IntKeyedDictionary<PropOp>();
|
||||
readonly IntKeyedDictionary<ScoreDefinition> scoreDefs = new IntKeyedDictionary<ScoreDefinition>();
|
||||
readonly IntKeyedDictionary<float> scores = new IntKeyedDictionary<float>();
|
||||
readonly IntKeyedDictionary<string> scoreStringCache = new IntKeyedDictionary<string>();
|
||||
readonly ArrayPool<byte> scoreStringPool = new ArrayPool<byte>();
|
||||
readonly IntKeyedDictionary<string> scoreFormatCache = new IntKeyedDictionary<string>();
|
||||
readonly TargetString scoreFullStr = new TargetString();
|
||||
readonly StringBuffer scoreFullBuf = new StringBuffer();
|
||||
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 TargetString GetFullFormattedScoreString() {
|
||||
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;
|
||||
}
|
||||
scoreFullStr.Length = scoreFullBuf.Count;
|
||||
var arr = scoreFullStr.TrustedAsArray();
|
||||
scoreFullBuf.CopyTo(0, arr, 0, scoreFullBuf.Count);
|
||||
return scoreFullStr;
|
||||
}
|
||||
}
|
||||
class ScoreStringSrc : PropSrc {
|
||||
readonly Func<float> _cb;
|
||||
readonly string _format;
|
||||
readonly ArrayPool<byte> _pool;
|
||||
readonly StringBuffer _buf = new StringBuffer() { Culture = CultureInfo.InvariantCulture };
|
||||
public ScoreStringSrc(ArrayPool<byte> pool, Func<float> 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) { }
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user