private ConditionVariant GetVariantByName(string docName, string cmd) { ConditionSelector selector = Selectors[cmd]; ConditionVariant variant = selector.Variants[cmd].Find(f => f.Doc.Name == docName); if (variant == null) { throw new Exception($"Internal error: no variant for {docName} and {cmd}"); } return(variant); }
public Intermediate DecompileCond(Instr instr) { if (Selectors.TryGetValue(instr.Cmd, out ConditionSelector selector)) { if (instr.Layers != null) { throw new FancyNotSupportedException($"Control flow instruction with layers is not supported", instr); } ConditionVariant variant = selector.GetVariant(instr); return(variant.ExtractInstrArgs(instr)); } else if (LabelDocs.TryGetValue(instr.Cmd, out int label)) { if (instr.Layers != null) { throw new FancyNotSupportedException($"Control flow instruction with layers is not supported", instr); } return(new Label { Num = label }); } return(instr); }
public static InstructionTranslator GetTranslator(InstructionDocs docs) { ConditionData conds; if (File.Exists("conditions.json")) { conds = ReadFile("conditions.json"); } else if (File.Exists(@"Resources\conditions.json")) { conds = ReadFile(@"Resources\conditions.json"); } else { conds = ReadStream("conditions.json"); } List <string> games = conds.Games.Select(g => g.Name).ToList(); string game = games.Find(g => docs.ResourceString.StartsWith(g + "-common")); if (game == null) { return(null); } EMEDF emedf = docs.DOC; // Mapping from instruction id, like 3[03] or 1003[11], to EMEDF doc // This is the shorthand used by the config. Dictionary <string, EMEDF.InstrDoc> instrs = emedf.Classes .Where(c => c.Index < 2000 && c.Index != 1014) .SelectMany(c => c.Instructions.Select(i => (InstructionID(c.Index, i.Index), i))) .ToDictionary(e => e.Item1, e => e.Item2); // Account for a command. Each condition/control flow statement should have a unique treatment. HashSet <string> visited = new HashSet <string>(); bool processInfo(string type, string instr) { if (instr == null) { return(false); } if (!instrs.TryGetValue(instr, out EMEDF.InstrDoc doc)) { return(false); } // Console.WriteLine($"{type} {instr}: {doc.Name}"); if (visited.Contains(instr)) { throw new Exception($"{instr} appears twice in condition config for {game}"); } visited.Add(instr); return(true); } foreach (string instr in conds.NoControlFlow) { processInfo("Excluded", instr); } int expectArg(string cmd, string name, object requiredType = null, int pos = -1) { EMEDF.InstrDoc doc = instrs[cmd]; int arg; List <string> names = name.Split('|').ToList(); if (pos >= 0) { if (pos >= doc.Arguments.Length) { throw new ArgumentException($"{doc.Name} doesn't have arg {pos}"); } if (!names.Contains(doc.Arguments[pos].Name)) { throw new ArgumentException($"{doc.Name} arg {pos} dooesn't have name {name}, has {string.Join(", ", doc.Arguments.Select(s => s.Name))}"); } arg = pos; } else { arg = doc.Arguments.ToList().FindIndex(a => names.Contains(a.Name)); if (arg == -1) { throw new ArgumentException($"{doc.Name} doesn't have arg named {name}, has {string.Join(", ", doc.Arguments.Select(s => s.Name))}"); } } if (requiredType is string enumName) { if (doc.Arguments[arg].EnumName != enumName) { throw new ArgumentException($"{doc.Name} arg {name} has enum type {doc.Arguments[arg].EnumName}, not {enumName}"); } } else if (requiredType is ArgType argType) { if (doc.Arguments[arg].Type != (long)argType) { throw new ArgumentException($"{doc.Name} arg {name} has type {doc.Arguments[arg].Type}, not {argType}"); } } else if (requiredType != null) { throw new Exception(requiredType.ToString()); } return(arg); } // Indexed by condition function name Dictionary <string, FunctionDoc> condDocs = new Dictionary <string, FunctionDoc>(); // Indexed by command id Dictionary <string, ConditionSelector> selectors = new Dictionary <string, ConditionSelector>(); int asInt(object obj) => int.Parse(obj.ToString()); void addVariants(ConditionSelector selector, ConditionDoc cond, ControlType use, string cmd, int control = -1) { if (selector.Variants.ContainsKey(cmd)) { throw new Exception($"Already added variants of {cond.Name} for {cmd}"); } selector.Variants[cmd] = new List <ConditionVariant>(); selectors[cmd] = selector; EMEDF.InstrDoc doc = instrs[cmd]; int negateArg = -1; string negateName = cond.IsCompare ? "Comparison Type" : cond.NegateField; EMEDF.EnumDoc negateDoc = null; if (negateName != null) { negateArg = expectArg(cmd, negateName, cond.IsCompare ? "Comparison Type" : null); string negateEnum = doc.Arguments[negateArg].EnumName; // Pretend these are compatible if (negateEnum == "ON/OFF") { negateEnum = "ON/OFF/CHANGE"; } negateDoc = emedf.Enums.Where(d => d.Name == negateEnum).FirstOrDefault(); if (negateDoc == null) { throw new ArgumentException($"Command {cmd} for cond {cond.Name} at arg {negateArg} uses enum which does not exist"); } if (selector.NegateEnum == null) { selector.NegateEnum = negateDoc; } else if (selector.NegateEnum.Name != negateEnum) { throw new ArgumentException($"Command {cmd} for {cond.Name} has negate enum {negateEnum} but {selector.NegateEnum.Name} was already used in a different command"); } } void addVariant(BoolVersion bv = null, CompareVersion cv = null) { ConditionVariant variant = new ConditionVariant { Variant = use }; string name = cond.Name; List <int> ignore = new List <int>(); if (control >= 0) { variant.ControlArg = control; ignore.Add(control); } if (negateArg >= 0 && (cv != null || bv != null)) { variant.NegateArg = negateArg; ignore.Add(negateArg); } if (bv != null) { name = bv.Name; if (negateDoc == null) { throw new ArgumentException($"Cond {cond.Name} has boolean variant {name} but no negate_field"); } string trueVal = negateDoc.Name == "BOOL" ? "TRUE" : bv.True; string trueKey = negateDoc.Values.Where(e => e.Value == trueVal).First().Key; variant.TrueOp = asInt(trueKey); if (bv.Required != null) { foreach (FieldValue req in bv.Required) { int reqArg = expectArg(cmd, req.Field); variant.ExtraArgs[reqArg] = req.Value; ignore.Add(reqArg); } } } else if (cv != null) { name = cv.Name; if (cv.Rhs == null) { throw new ArgumentException($"Cond {cond.Name} has compare variant {name} with no RHS specified"); } variant.CompareArg = expectArg(cmd, cv.Rhs); ignore.Add(variant.CompareArg); if (cv.Lhs != null) { variant.CompareArg2 = expectArg(cmd, cv.Lhs); ignore.Add(variant.CompareArg2); } } if (name == null) { throw new ArgumentException($"Name for {cond.Name} and {cmd} is missing"); } EMEDF.ArgDoc optArgDoc = null; // Optional args serve two purposes: to hide noisy default values, and to address inconsistencies between variants. // In all cases, they can be established from the COND variant. if (use == ControlType.COND && cond.OptFields != null) { for (int i = 0; i < cond.OptFields.Count; i++) { if (doc.Arguments[doc.Arguments.Length - cond.OptFields.Count + i].Name != cond.OptFields[i]) { break; } } bool matching = cond.OptFields.Select((a, i) => a == doc.Arguments[doc.Arguments.Length - cond.OptFields.Count + i].Name).All(b => b); // Missing opt args can happen when "# of target character"-type arguments get added over time. // This is mostly a pretty-printing feature. so it might be tedious to specify each individual change between games. if (matching) { int optArg = doc.Arguments.Length - cond.OptFields.Count; if (ignore.Contains(optArg)) { throw new Exception($"Optional arg {cond.OptFields[0]} is not part of the normal argument list for {name} with {cmd}"); } optArgDoc = doc.Arguments[optArg]; } } // Non-control-flow arguments should match for all uses of a condition, across goto/skip/etc. commands // (other than for optional args) List <EMEDF.ArgDoc> condArgs = doc.Arguments.Where((a, i) => !ignore.Contains(i)).ToList(); if (condDocs.TryGetValue(name, out FunctionDoc condDoc)) { string older = string.Join(", ", condDoc.Args.Select(a => a.Name)); string newer = string.Join(", ", condArgs.Select(a => a.Name)); bool matching = older == newer; if (!matching && condDoc.OptionalArgs > 0) { // This is only permissible if the existing definition has optional args, in which case the shared segment should still match. older = string.Join(", ", condDoc.Args.Take(condDoc.Args.Count - condDoc.OptionalArgs).Select(a => a.Name)); matching = older == newer; } if (!matching) { throw new Exception($"Multiple possible definitions found for {name}: [{older}] existing vs [{newer}] for {cmd}. opt args {condDoc.OptionalArgs}"); } } else { condDocs[name] = condDoc = new FunctionDoc { Name = name, Args = condArgs, ConditionDoc = cond, NegateEnum = negateDoc, }; if (optArgDoc != null) { condDoc.OptionalArgs = condArgs.Count - condArgs.IndexOf(optArgDoc); } } condDoc.Variants[use] = cmd; variant.Doc = condDoc; selector.Variants[cmd].Add(variant); } foreach (BoolVersion version in cond.AllBools) { addVariant(bv: version); } foreach (CompareVersion version in cond.AllCompares) { addVariant(cv: version); } addVariant(); } foreach (ConditionDoc cond in conds.Conditions) { ConditionSelector selector = new ConditionSelector { Cond = cond }; if (cond.Games != null && !cond.Games.Contains(game)) { continue; } if (processInfo($"Cond {cond.Name}", cond.Cond)) { // The cond variant should go first, as it can have a superset of other variants' args if marked in the config. expectArg(cond.Cond, "Result Condition Group", "Condition Group", 0); addVariants(selector, cond, ControlType.COND, cond.Cond, 0); } if (processInfo($"Skip {cond.Name}", cond.Skip)) { expectArg(cond.Skip, "Number Of Skipped Lines", EMEVD.Instruction.ArgType.Byte, 0); addVariants(selector, cond, ControlType.SKIP, cond.Skip, 0); } if (processInfo($"End {cond.Name}", cond.End)) { expectArg(cond.End, "Execution End Type", "Event End Type", 0); addVariants(selector, cond, ControlType.END, cond.End, 0); } if (processInfo($"Goto {cond.Name}", cond.Goto)) { expectArg(cond.Goto, "Label", "Label", 0); addVariants(selector, cond, ControlType.GOTO, cond.Goto, 0); } if (processInfo($"Wait {cond.Name}", cond.Wait)) { // Implicit main arg addVariants(selector, cond, ControlType.WAIT, cond.Wait); } } string undocError = string.Join(", ", instrs.Where(e => !visited.Contains(e.Key)).Select(e => $"{e.Key}:{e.Value.Name}")); if (undocError.Length > 0) { // This doesn't have to be an error, but it does mean that condition group decompilation is impossible when these commands are present. throw new ArgumentException($"Present in emedf but not condition config for {game}: {undocError}"); } Dictionary <string, int> labels = new Dictionary <string, int>(); EMEDF.ClassDoc labelClass = emedf[1014]; if (labelClass != null) { labels = labelClass.Instructions.ToDictionary(i => InstructionID(1014, i.Index), i => (int)i.Index); } return(new InstructionTranslator { CondDocs = condDocs, Selectors = selectors, InstrDocs = instrs, LabelDocs = labels, }); }
// As part of instruction compilation, rewrite all instruction so that they correspond to valid emevd commands. // But don't convert them into instructions yet, since we need to edit control things like condition group register allocation and line skipping. public Instr CompileCond(Intermediate im) { // Mainly NoOp and JSStatement shouldn't be passed in here if (im is Instr imInstr) { // Maybe validate Instrs here? Or just do that in AST conversion return(imInstr); } else if (im is Label label) { string cmd = InstructionID(1014, label.Num); return(new Instr { Name = $"Label{label.Num}", Cmd = cmd, Args = new List <object>() }); } else if (im is CondIntermediate condIm) { Cond cond = condIm.Cond; if (cond is ErrorCond) { // Just quit out here. Error is already recorded elsewhere return(null); } else if (cond is OpCond) { throw new Exception($"Internal error: should have expanded out all subconditions in {im}"); } // The only non-synthetic use of WAIT is in condition groups, and these can just be converted to main group eval with no behavior change. ControlType type = condIm.ControlType; if (type == ControlType.WAIT) { type = ControlType.COND; } string docName = cond.DocName; FunctionDoc functionDoc = CondDocs[docName]; if (!functionDoc.Variants.ContainsKey(type)) { string errName = cond.Always ? $"unconditional {type}" : $"{type} {docName}"; StringBuilder sb = new StringBuilder(); sb.Append($"Compiling {errName} is not supported."); sb.Append($" Only [{string.Join(", ", functionDoc.Variants.Keys)}] are supported."); if (type == ControlType.GOTO && functionDoc.Variants.ContainsKey(ControlType.SKIP)) { sb.Append($" Try using a synthetic label instead."); } throw new FancyNotSupportedException(sb.ToString(), im); } string cmd = functionDoc.Variants[type]; ConditionVariant variant = GetVariantByName(docName, cmd); EMEDF.InstrDoc instrDoc = InstrDocs[cmd]; Instr instr = new Instr { Cmd = cmd, Name = instrDoc.DisplayName, Args = Enumerable.Repeat((object)null, instrDoc.Arguments.Length).ToList() }; int controlVal = condIm.ControlArg; int negateVal = cond is CompareCond cmp ? (int)cmp.Type : variant.TrueOp; if (cond != null && cond.Negate) { if (functionDoc.NegateEnum == null) { throw new FancyNotSupportedException($"No way to negate {functionDoc.Name} compiling {docName} to {instr.Name}", im); } negateVal = OppositeOp(functionDoc.ConditionDoc, functionDoc.NegateEnum, negateVal); } variant.SetInstrArgs(instr, cond, controlVal, negateVal); return(instr); } else { throw new Exception($"Internal error: unable to compile unknown instruction {im}"); } }
// As part of instruction compilation, rewrite all instruction so that they correspond to valid emevd commands. // But don't convert them into instructions yet, since we need to edit control things like condition group register allocation and line skipping. public List <Intermediate> ExpandCond(Intermediate im, Func <string> newVar) { if (im is Instr || im is Label || im is NoOp || im is JSStatement) { return(new List <Intermediate> { im }); } else if (im is CondIntermediate condIm) { // Either use a direct command, or require a condition group because of negation or missing variant (or, in future, optional args). // Some commands do not support condition groups: // - goto/skip/end only: CompareCompiledConditionGroup, CompareNumberOfCoopClients, CompareNumberOfCoopClients // - goto only: HollowArenaMatchType Cond cond = condIm.Cond; if (cond == null) { return(new List <Intermediate> { im }); } else if (cond is ErrorCond) { // Just quit out here. Error is already recorded elsewhere return(new List <Intermediate>()); } else if (cond is OpCond) { throw new Exception($"Internal error: should have expanded out all subconditions in {im}"); } string docName = cond.DocName; if (!CondDocs.TryGetValue(docName, out FunctionDoc functionDoc)) { throw new Exception($"Internal error: Unknown condition function {docName}"); } ControlType type = condIm.ControlType; // The only non-synthetic use of WAIT is in condition groups, and these can just be converted to main group eval with no behavior change. if (type == ControlType.WAIT) { type = ControlType.COND; } bool indirection = false; if (functionDoc.Variants.ContainsKey(type)) { // If variant exists, see if it can be used ConditionVariant mainVariant = GetVariantByName(docName, functionDoc.Variants[type]); if (cond.Negate && mainVariant.NegateArg == -1) { if (!functionDoc.Variants.ContainsKey(ControlType.COND)) { throw new FancyNotSupportedException($"Can't use {docName} in this form since it does't have a condition version. Add or remove negation so that it can be translated to emevd.", im); } // Console.WriteLine($"Expanding {im} because it can't be negated as {type}"); indirection = true; } } else { // Needed for DS1, which only has skip. The instruction is rewritten later with # of lines if the label is synthetic. if (type == ControlType.GOTO && functionDoc.Variants.ContainsKey(ControlType.SKIP)) { // The type is ControlType.SKIP in this case, which will be accounted for once SkipLines is filled in. } else if (functionDoc.Variants.ContainsKey(ControlType.COND)) { indirection = true; } else { // Also part of error: can't use compiled groups in condition group. throw new FancyNotSupportedException($"Can't use statement type {type} with {functionDoc.Name}; can only use it with {string.Join(", ", functionDoc.Variants.Keys)}", im); } } if (indirection) { string var = newVar() + "z"; ConditionVariant cmdVariant = GetVariantByName(docName, functionDoc.Variants[ControlType.COND]); CondRef tmpCond = new CondRef { Compiled = false, Name = var }; if (cond.Negate && cmdVariant.NegateArg == -1) { cond.Negate = false; tmpCond.Negate = true; } Intermediate instr = new CondAssign { Cond = cond, Op = CondAssignOp.Assign, ToVar = var, Labels = condIm.Labels }; // Note that this is destructive. If this becomes a problem with multiple references that should // remain divergent, conditions may need some deep cloning routines. condIm.Cond = tmpCond; condIm.Labels = new List <string>(); return(new List <Intermediate> { instr, condIm }); } else { return(new List <Intermediate> { condIm }); } } else { throw new Exception($"Internal error: unable to compile unknown instruction {im}"); } }