├── SamplePrograms ├── Sample5 │ └── Sample5.bb ├── Sample2 │ └── Sample2.bb ├── .gitignore ├── Sample4 │ └── Sample4.bb ├── Sample3 │ └── Sample3.bb └── Sample1 │ └── Sample1.bb ├── B3DDecompUtils ├── Compiler.cs ├── DirectoryUtils.cs ├── BinaryReaderExtensions.cs ├── B3DDecompUtils.csproj ├── PathUtils.cs ├── SpanExtensions.cs ├── Logger.cs ├── ParsingExtensions.cs └── Primitives │ ├── Either.cs │ └── Option.cs ├── Blitz3DDisasm ├── DataMember.cs ├── LibFunction.cs ├── Reloc.cs ├── Blitz3DDisasm.csproj ├── BuiltInSymbolExtractor.cs └── Symbol.cs ├── Blitz3DDecomp ├── Basics │ ├── Function │ │ ├── HighLevel │ │ │ ├── Expression │ │ │ │ ├── Access │ │ │ │ │ ├── AccessExpression.cs │ │ │ │ │ ├── VariableExpression.cs │ │ │ │ │ ├── FieldAccessExpression.cs │ │ │ │ │ ├── ArrayAccessExpression.cs │ │ │ │ │ └── DimAccessExpression.cs │ │ │ │ ├── ComparisonResults │ │ │ │ │ ├── BooleanExpression.cs │ │ │ │ │ ├── CompareToZero │ │ │ │ │ │ ├── OneIfZeroExpression.cs │ │ │ │ │ │ ├── OneIfNotZeroExpression.cs │ │ │ │ │ │ ├── OneIfLessThanZeroExpression.cs │ │ │ │ │ │ ├── OneIfGreaterThanZeroExpression.cs │ │ │ │ │ │ ├── OneIfLessThanOrEqualToZeroExpression.cs │ │ │ │ │ │ └── OneIfGreaterThanOrEqualToZeroExpression.cs │ │ │ │ │ └── CompareToOther │ │ │ │ │ │ ├── OneIfExpressionsEqualExpression.cs │ │ │ │ │ │ ├── OneIfExpressionsNotEqualExpression.cs │ │ │ │ │ │ ├── OneIfExpressionIsLessThanOtherExpression.cs │ │ │ │ │ │ ├── OneIfExpressionIsGreaterThanOtherExpression.cs │ │ │ │ │ │ ├── OneIfExpressionIsLessThanOrEqualToOtherExpression.cs │ │ │ │ │ │ └── OneIfExpressionIsGreaterThanOrEqualToOtherExpression.cs │ │ │ │ ├── Expression.cs │ │ │ │ ├── Atoms │ │ │ │ │ └── ConstantExpression.cs │ │ │ │ ├── Object │ │ │ │ │ ├── ConstructorExpression.cs │ │ │ │ │ ├── FirstOfTypeExpression.cs │ │ │ │ │ ├── LastOfTypeExpression.cs │ │ │ │ │ ├── AfterExpression.cs │ │ │ │ │ └── BeforeExpression.cs │ │ │ │ ├── Arithmetic │ │ │ │ │ ├── AbsExpression.cs │ │ │ │ │ ├── AddExpression.cs │ │ │ │ │ ├── AndExpression.cs │ │ │ │ │ ├── OrExpression.cs │ │ │ │ │ ├── SignExpression.cs │ │ │ │ │ ├── XorExpression.cs │ │ │ │ │ ├── DivideExpression.cs │ │ │ │ │ ├── ModuloExpression.cs │ │ │ │ │ ├── SignFlipExpression.cs │ │ │ │ │ ├── MultiplyExpression.cs │ │ │ │ │ ├── SubtractExpression.cs │ │ │ │ │ ├── LogicalOrExpression.cs │ │ │ │ │ ├── ShiftLeftExpression.cs │ │ │ │ │ ├── LogicalAndExpression.cs │ │ │ │ │ ├── ShiftRightSignedExpression.cs │ │ │ │ │ ├── ShiftRightUnsignedExpression.cs │ │ │ │ │ └── ExponentiationExpression.cs │ │ │ │ ├── Conversions │ │ │ │ │ ├── ConvertToIntExpression.cs │ │ │ │ │ ├── ConvertToFloatExpression.cs │ │ │ │ │ ├── ConvertToStringExpression.cs │ │ │ │ │ ├── ConvertObjectToHandleExpression.cs │ │ │ │ │ └── ConvertHandleToObjectExpression.cs │ │ │ │ └── CallExpression.cs │ │ │ └── Statement │ │ │ │ ├── Jumps │ │ │ │ ├── JumpStatement.cs │ │ │ │ ├── UnconditionalJumpStatement.cs │ │ │ │ └── JumpIfExpressionStatement.cs │ │ │ │ ├── CommentStatement.cs │ │ │ │ ├── Loops │ │ │ │ ├── ExitStatement.cs │ │ │ │ ├── Repeat │ │ │ │ │ ├── RepeatStatement.cs │ │ │ │ │ ├── ForeverStatement.cs │ │ │ │ │ └── UntilStatement.cs │ │ │ │ ├── For │ │ │ │ │ ├── NextStatement.cs │ │ │ │ │ ├── ForEachStatement.cs │ │ │ │ │ └── ForOnIntStatement.cs │ │ │ │ └── While │ │ │ │ │ ├── WendStatement.cs │ │ │ │ │ └── WhileStatement.cs │ │ │ │ ├── DataSection │ │ │ │ ├── RestoreStatement.cs │ │ │ │ └── DataReadStatement.cs │ │ │ │ ├── If │ │ │ │ ├── EndIfStatement.cs │ │ │ │ ├── ElseStatement.cs │ │ │ │ ├── IfStatement.cs │ │ │ │ └── ElseIfStatement.cs │ │ │ │ ├── Object │ │ │ │ ├── DeleteEachStatement.cs │ │ │ │ ├── DestructorStatement.cs │ │ │ │ ├── InsertAfterStatement.cs │ │ │ │ └── InsertBeforeStatement.cs │ │ │ │ ├── Select │ │ │ │ ├── EndSelectStatement.cs │ │ │ │ ├── DefaultStatement.cs │ │ │ │ ├── SelectStatement.cs │ │ │ │ └── CaseStatement.cs │ │ │ │ ├── ReturnStatement.cs │ │ │ │ ├── FreeStandingExpressionStatement.cs │ │ │ │ ├── AllocateDimStatement.cs │ │ │ │ ├── Statement.cs │ │ │ │ └── AssignmentStatement.cs │ │ ├── LowLevel │ │ │ ├── AssemblySection.cs │ │ │ └── Instruction.cs │ │ └── BuiltIns │ │ │ └── BlitzSymbol.cs │ ├── CurrentCompiler.cs │ ├── StringConstants.cs │ ├── Variables │ │ ├── VariableWithOwnType.cs │ │ ├── FieldVariable.cs │ │ ├── ArrayElementVariable.cs │ │ ├── GlobalVariable.cs │ │ └── Variable.cs │ ├── DebugTrace.cs │ ├── LibSymbols.cs │ ├── CustomType.cs │ ├── Registers.cs │ ├── DeclType.cs │ └── DimArray.cs ├── DecompilerSteps │ ├── Step0 │ │ ├── LoadGlobalList.cs │ │ ├── LoadDimArrays.cs │ │ ├── CountArguments.cs │ │ ├── IngestLibInfo.cs │ │ ├── IngestStringConstants.cs │ │ ├── TypeDecompiler.cs │ │ ├── IngestDecls.cs │ │ ├── IngestCodeFiles.cs │ │ └── CountLocals.cs │ ├── Step5 │ │ ├── CleanupUselessGoto.cs │ │ ├── GenerateLibDecls.cs │ │ ├── CleanupElseIf.cs │ │ ├── CleanupExit.cs │ │ ├── DecompileData.cs │ │ ├── CleanupIfs.cs │ │ ├── CleanupWhile.cs │ │ ├── CleanupRepeat.cs │ │ ├── RemoveTrivialNoops.cs │ │ ├── CleanupElse.cs │ │ ├── CleanupBooleanExpressions.cs │ │ ├── CleanupLogicalBooleanExpressions.cs │ │ ├── FixDimIndexer.cs │ │ ├── CleanupForOnInt.cs │ │ ├── ConvertFunctionCallsToFinalRepresentation.cs │ │ └── RemoveSingleUseTemps.cs │ ├── Step1 │ │ ├── DimArrayAccessRewrite.cs │ │ ├── LibCallCleanup.cs │ │ └── CollectCalls.cs │ ├── Step4 │ │ ├── GuessFloatsFromStoreInstructions.cs │ │ ├── GuessIntFromInstructions.cs │ │ ├── GuessIntFromNothing.cs │ │ └── GuessFloatsFromConstants.cs │ ├── Step2 │ │ ├── HandleUnambiguousIntegerInstructions.cs │ │ ├── VectorTypeDeduction.cs │ │ └── HandleFloatInstructions.cs │ └── Step3 │ │ ├── HandleIntegerSub.cs │ │ ├── CalleeReturnTypePropagation.cs │ │ ├── CalleeArgumentTypePropagation.cs │ │ ├── SelfReturnTypePropagation.cs │ │ ├── BbArrayAccessRewrite.cs │ │ ├── BbCustomTypeFieldAccessRewrite.cs │ │ └── VariableTypePropagation.cs ├── Blitz3DDecomp.csproj └── Utils │ └── CleanupIndexer.cs ├── .gitignore ├── README.md └── Blitz3DDecomp.sln /SamplePrograms/Sample5/Sample5.bb: -------------------------------------------------------------------------------- 1 | Global myArray%[0] 2 | -------------------------------------------------------------------------------- /SamplePrograms/Sample2/Sample2.bb: -------------------------------------------------------------------------------- 1 | validProgram = "look at this -------------------------------------------------------------------------------- /SamplePrograms/.gitignore: -------------------------------------------------------------------------------- 1 | *.bb_bak* 2 | *.exe 3 | *.dat 4 | *_disasm 5 | *_decomp -------------------------------------------------------------------------------- /B3DDecompUtils/Compiler.cs: -------------------------------------------------------------------------------- 1 | namespace B3DDecompUtils; 2 | 3 | public enum Compiler 4 | { 5 | Blitz3d, 6 | BlitzPlus 7 | } 8 | -------------------------------------------------------------------------------- /Blitz3DDisasm/DataMember.cs: -------------------------------------------------------------------------------- 1 | namespace Blitz3DDecomp; 2 | 3 | struct DataMember 4 | { 5 | public int Type; 6 | public uint Value; 7 | } 8 | -------------------------------------------------------------------------------- /SamplePrograms/Sample4/Sample4.bb: -------------------------------------------------------------------------------- 1 | Const Constant$ = "Hello world!" 2 | 3 | Local a$ = Constant 4 | Local b$ = Constant 5 | Local c$ = "Hello world!" 6 | -------------------------------------------------------------------------------- /Blitz3DDecomp/Basics/Function/HighLevel/Expression/Access/AccessExpression.cs: -------------------------------------------------------------------------------- 1 | namespace Blitz3DDecomp.HighLevel; 2 | 3 | abstract record AccessExpression : Expression; -------------------------------------------------------------------------------- /Blitz3DDisasm/LibFunction.cs: -------------------------------------------------------------------------------- 1 | namespace Blitz3DDecomp; 2 | 3 | struct LibFunction 4 | { 5 | public string LibName; 6 | public string FunctionName; 7 | public int LookupAddress; 8 | } 9 | -------------------------------------------------------------------------------- /Blitz3DDecomp/Basics/CurrentCompiler.cs: -------------------------------------------------------------------------------- 1 | using B3DDecompUtils; 2 | 3 | namespace Blitz3DDecomp; 4 | 5 | static class CurrentCompiler 6 | { 7 | public static Compiler Value = Compiler.Blitz3d; 8 | } -------------------------------------------------------------------------------- /Blitz3DDecomp/Basics/StringConstants.cs: -------------------------------------------------------------------------------- 1 | namespace Blitz3DDecomp; 2 | 3 | static class StringConstants 4 | { 5 | public static readonly Dictionary SymbolToValue = new Dictionary(); 6 | } -------------------------------------------------------------------------------- /Blitz3DDisasm/Reloc.cs: -------------------------------------------------------------------------------- 1 | namespace Blitz3DDecomp; 2 | 3 | struct Reloc 4 | { 5 | public int RelocAddress; 6 | public int OffsetFromSymbol; 7 | public int OriginalValue; 8 | public string SymbolName; 9 | } 10 | -------------------------------------------------------------------------------- /Blitz3DDecomp/Basics/Function/HighLevel/Expression/ComparisonResults/BooleanExpression.cs: -------------------------------------------------------------------------------- 1 | namespace Blitz3DDecomp.HighLevel.ComparisonResults; 2 | 3 | abstract record BooleanExpression : Expression 4 | { 5 | public abstract BooleanExpression Negated { get; } 6 | } -------------------------------------------------------------------------------- /B3DDecompUtils/DirectoryUtils.cs: -------------------------------------------------------------------------------- 1 | namespace B3DDecompUtils; 2 | 3 | public static class DirectoryUtils 4 | { 5 | public static void RecreateDirectory(string path) 6 | { 7 | if (Directory.Exists(path)) { Directory.Delete(path, recursive: true); } 8 | Directory.CreateDirectory(path); 9 | } 10 | } -------------------------------------------------------------------------------- /SamplePrograms/Sample3/Sample3.bb: -------------------------------------------------------------------------------- 1 | Type Child1 2 | End Type 3 | 4 | Type Child2 5 | End Type 6 | 7 | Type Base 8 | Field ChildPtr% 9 | End Type 10 | 11 | Local myBase.Base = New Base 12 | Local myChild1.Child1 = New Child1 13 | myBase\ChildPtr = Handle myChild1 14 | Local deref.Child1 = Object.Child1(myBase\ChildPtr) 15 | -------------------------------------------------------------------------------- /Blitz3DDecomp/Basics/Function/HighLevel/Expression/Expression.cs: -------------------------------------------------------------------------------- 1 | namespace Blitz3DDecomp.HighLevel; 2 | 3 | abstract record Expression 4 | { 5 | public abstract string StringRepresentation { get; } 6 | public abstract Expression Map(Func mapper); 7 | public abstract IEnumerable InnerExpressions { get; } 8 | } -------------------------------------------------------------------------------- /Blitz3DDecomp/Basics/Function/HighLevel/Statement/Jumps/JumpStatement.cs: -------------------------------------------------------------------------------- 1 | namespace Blitz3DDecomp.HighLevel; 2 | 3 | abstract record JumpStatement(string SectionName, Function Function) : Statement 4 | { 5 | public bool PointsToUserSection => SectionName.StartsWith("_l_", StringComparison.Ordinal); 6 | 7 | public override string StringRepresentation => $"Goto {HighLevelSection.CleanupSectionName(SectionName, Function)}"; 8 | } -------------------------------------------------------------------------------- /Blitz3DDecomp/Basics/Variables/VariableWithOwnType.cs: -------------------------------------------------------------------------------- 1 | namespace Blitz3DDecomp; 2 | 3 | abstract class VariableWithOwnType : Variable 4 | { 5 | private DeclType declType = DeclType.Unknown; 6 | public sealed override DeclType DeclType 7 | { 8 | get => declType; 9 | set 10 | { 11 | declType = value; 12 | fields = null; 13 | } 14 | } 15 | 16 | protected VariableWithOwnType(string name) : base(name) { } 17 | } -------------------------------------------------------------------------------- /Blitz3DDecomp/Basics/Function/HighLevel/Expression/Atoms/ConstantExpression.cs: -------------------------------------------------------------------------------- 1 | namespace Blitz3DDecomp.HighLevel; 2 | 3 | sealed record ConstantExpression(string Value) : Expression 4 | { 5 | public override string StringRepresentation 6 | => Value; 7 | 8 | public override Expression Map(Func mapper) 9 | => mapper(this); 10 | 11 | public override IEnumerable InnerExpressions 12 | => Enumerable.Empty(); 13 | } -------------------------------------------------------------------------------- /Blitz3DDecomp/Basics/Function/HighLevel/Expression/Access/VariableExpression.cs: -------------------------------------------------------------------------------- 1 | namespace Blitz3DDecomp.HighLevel; 2 | 3 | sealed record VariableExpression(Variable Variable) : AccessExpression 4 | { 5 | public override string StringRepresentation 6 | => Variable.Name; 7 | 8 | public override Expression Map(Func mapper) 9 | => mapper(this); 10 | 11 | public override IEnumerable InnerExpressions 12 | => Enumerable.Empty(); 13 | } -------------------------------------------------------------------------------- /B3DDecompUtils/BinaryReaderExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | 3 | namespace B3DDecompUtils; 4 | 5 | public static class BinaryReaderExtensions 6 | { 7 | public static string ReadCStr(this BinaryReader reader) 8 | { 9 | var bytes = new List(); 10 | while (true) 11 | { 12 | byte b = reader.ReadByte(); 13 | if (b == '\0') { break; } 14 | bytes.Add(b); 15 | } 16 | return Encoding.ASCII.GetString(bytes.ToArray()); 17 | } 18 | } -------------------------------------------------------------------------------- /Blitz3DDecomp/Basics/Function/HighLevel/Statement/CommentStatement.cs: -------------------------------------------------------------------------------- 1 | namespace Blitz3DDecomp.HighLevel; 2 | 3 | sealed record CommentStatement(string Text) : Statement 4 | { 5 | public override string StringRepresentation => $";{Text}"; 6 | public override IEnumerable InnerExpressions => Enumerable.Empty(); 7 | protected override Statement MapImplementation(Func statementMapper, Func expressionMapper) 8 | => statementMapper(this); 9 | } -------------------------------------------------------------------------------- /Blitz3DDecomp/DecompilerSteps/Step0/LoadGlobalList.cs: -------------------------------------------------------------------------------- 1 | using B3DDecompUtils; 2 | 3 | namespace Blitz3DDecomp; 4 | 5 | static class LoadGlobalList 6 | { 7 | public static void FromDir(string inputDir) 8 | { 9 | inputDir = inputDir.AppendToPath("Variable"); 10 | if (!Directory.Exists(inputDir)) { return; } 11 | foreach (var filePath in Directory.GetFiles(inputDir)) 12 | { 13 | _ = new GlobalVariable(Path.GetFileNameWithoutExtension(filePath)[2..]); 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /Blitz3DDecomp/DecompilerSteps/Step0/LoadDimArrays.cs: -------------------------------------------------------------------------------- 1 | using B3DDecompUtils; 2 | 3 | namespace Blitz3DDecomp; 4 | 5 | static class LoadDimArrays 6 | { 7 | public static void FromDir(string inputDir) 8 | { 9 | inputDir = inputDir.AppendToPath("DimArray"); 10 | if (!Directory.Exists(inputDir)) { return; } 11 | foreach (var filePath in Directory.GetFiles(inputDir)) 12 | { 13 | _ = DimArray.TryCreateFromSymbolName(Path.GetFileNameWithoutExtension(filePath)); 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /Blitz3DDecomp/Basics/Function/HighLevel/Expression/Object/ConstructorExpression.cs: -------------------------------------------------------------------------------- 1 | namespace Blitz3DDecomp.HighLevel; 2 | 3 | sealed record ConstructorExpression(CustomType Type) : Expression 4 | { 5 | public override string StringRepresentation 6 | => $"(New {Type.Name})"; 7 | 8 | public override Expression Map(Func mapper) 9 | { 10 | return mapper(this); 11 | } 12 | 13 | public override IEnumerable InnerExpressions 14 | => Enumerable.Empty(); 15 | } -------------------------------------------------------------------------------- /Blitz3DDecomp/Basics/Function/HighLevel/Expression/Object/FirstOfTypeExpression.cs: -------------------------------------------------------------------------------- 1 | namespace Blitz3DDecomp.HighLevel; 2 | 3 | sealed record FirstOfTypeExpression(CustomType ObjectType) : Expression 4 | { 5 | public override string StringRepresentation 6 | => $"(First {ObjectType.Name})"; 7 | 8 | public override Expression Map(Func mapper) 9 | { 10 | return mapper(this); 11 | } 12 | 13 | public override IEnumerable InnerExpressions 14 | => Enumerable.Empty(); 15 | } -------------------------------------------------------------------------------- /Blitz3DDecomp/Basics/Function/HighLevel/Expression/Object/LastOfTypeExpression.cs: -------------------------------------------------------------------------------- 1 | namespace Blitz3DDecomp.HighLevel; 2 | 3 | 4 | sealed record LastOfTypeExpression(CustomType ObjectType) : Expression 5 | { 6 | public override string StringRepresentation 7 | => $"(Last {ObjectType.Name})"; 8 | 9 | public override Expression Map(Func mapper) 10 | { 11 | return mapper(this); 12 | } 13 | 14 | public override IEnumerable InnerExpressions 15 | => Enumerable.Empty(); 16 | } -------------------------------------------------------------------------------- /Blitz3DDecomp/Basics/Function/HighLevel/Statement/Jumps/UnconditionalJumpStatement.cs: -------------------------------------------------------------------------------- 1 | namespace Blitz3DDecomp.HighLevel; 2 | 3 | sealed record UnconditionalJumpStatement(string SectionName, Function Function) : JumpStatement(SectionName, Function) 4 | { 5 | public override IEnumerable InnerExpressions => Enumerable.Empty(); 6 | 7 | protected override Statement MapImplementation( 8 | Func statementMapper, 9 | Func expressionMapper) 10 | => statementMapper(this); 11 | } -------------------------------------------------------------------------------- /Blitz3DDecomp/Basics/Function/HighLevel/Statement/Loops/ExitStatement.cs: -------------------------------------------------------------------------------- 1 | namespace Blitz3DDecomp.HighLevel.Loops; 2 | 3 | sealed record ExitStatement : Statement 4 | { 5 | public override string StringRepresentation 6 | => "Exit"; 7 | 8 | public override IEnumerable InnerExpressions => Enumerable.Empty(); 9 | 10 | protected override Statement MapImplementation( 11 | Func statementMapper, 12 | Func expressionMapper) 13 | => statementMapper(this); 14 | } -------------------------------------------------------------------------------- /Blitz3DDecomp/Basics/Function/HighLevel/Expression/Access/FieldAccessExpression.cs: -------------------------------------------------------------------------------- 1 | namespace Blitz3DDecomp.HighLevel; 2 | 3 | sealed record FieldAccessExpression(Expression Owner, CustomType.Field Field) : AccessExpression 4 | { 5 | public override string StringRepresentation 6 | => $"{Owner.StringRepresentation}\\{Field.Name}"; 7 | 8 | public override Expression Map(Func mapper) 9 | => mapper(new FieldAccessExpression(Owner.Map(mapper), Field)); 10 | 11 | public override IEnumerable InnerExpressions { get; } = new[] { Owner }; 12 | } -------------------------------------------------------------------------------- /Blitz3DDecomp/Basics/Function/HighLevel/Statement/DataSection/RestoreStatement.cs: -------------------------------------------------------------------------------- 1 | namespace Blitz3DDecomp.HighLevel; 2 | 3 | sealed record RestoreStatement(string Offset) : Statement 4 | { 5 | public override string StringRepresentation 6 | => $"Restore {Offset}"; 7 | 8 | public override IEnumerable InnerExpressions => Enumerable.Empty(); 9 | 10 | protected override Statement MapImplementation( 11 | Func statementMapper, 12 | Func expressionMapper) 13 | => statementMapper(this); 14 | } -------------------------------------------------------------------------------- /Blitz3DDecomp/Basics/Function/HighLevel/Expression/Arithmetic/AbsExpression.cs: -------------------------------------------------------------------------------- 1 | namespace Blitz3DDecomp.HighLevel; 2 | 3 | sealed record AbsExpression(Expression OriginalExpression) : Expression 4 | { 5 | public override string StringRepresentation 6 | => $"(Abs {OriginalExpression.StringRepresentation})"; 7 | 8 | public override Expression Map(Func mapper) 9 | { 10 | return mapper(new AbsExpression(OriginalExpression.Map(mapper))); 11 | } 12 | 13 | public override IEnumerable InnerExpressions { get; } = new[] { OriginalExpression }; 14 | } -------------------------------------------------------------------------------- /Blitz3DDecomp/Basics/Function/HighLevel/Expression/Arithmetic/AddExpression.cs: -------------------------------------------------------------------------------- 1 | namespace Blitz3DDecomp.HighLevel; 2 | 3 | sealed record AddExpression(Expression Lhs, Expression Rhs) : Expression 4 | { 5 | public override string StringRepresentation 6 | => $"({Lhs.StringRepresentation} + {Rhs.StringRepresentation})"; 7 | 8 | public override Expression Map(Func mapper) 9 | { 10 | return mapper(new AddExpression(Lhs.Map(mapper), Rhs.Map(mapper))); 11 | } 12 | 13 | public override IEnumerable InnerExpressions { get; } = new[] { Lhs, Rhs }; 14 | } -------------------------------------------------------------------------------- /Blitz3DDecomp/Basics/Function/HighLevel/Expression/Arithmetic/AndExpression.cs: -------------------------------------------------------------------------------- 1 | namespace Blitz3DDecomp.HighLevel; 2 | 3 | sealed record AndExpression(Expression Lhs, Expression Rhs) : Expression 4 | { 5 | public override string StringRepresentation 6 | => $"({Lhs.StringRepresentation} And {Rhs.StringRepresentation})"; 7 | 8 | public override Expression Map(Func mapper) 9 | { 10 | return mapper(new AndExpression(Lhs.Map(mapper), Rhs.Map(mapper))); 11 | } 12 | 13 | public override IEnumerable InnerExpressions { get; } = new[] { Lhs, Rhs }; 14 | } -------------------------------------------------------------------------------- /Blitz3DDecomp/Basics/Function/HighLevel/Expression/Arithmetic/OrExpression.cs: -------------------------------------------------------------------------------- 1 | namespace Blitz3DDecomp.HighLevel; 2 | 3 | sealed record OrExpression(Expression Lhs, Expression Rhs) : Expression 4 | { 5 | public override string StringRepresentation 6 | => $"({Lhs.StringRepresentation} Or {Rhs.StringRepresentation})"; 7 | 8 | public override Expression Map(Func mapper) 9 | { 10 | return mapper(new OrExpression(Lhs.Map(mapper), Rhs.Map(mapper))); 11 | } 12 | 13 | public override IEnumerable InnerExpressions { get; } = new[] { Lhs, Rhs }; 14 | } -------------------------------------------------------------------------------- /Blitz3DDecomp/Basics/Function/HighLevel/Expression/Arithmetic/SignExpression.cs: -------------------------------------------------------------------------------- 1 | namespace Blitz3DDecomp.HighLevel; 2 | 3 | sealed record SignExpression(Expression OriginalExpression) : Expression 4 | { 5 | public override string StringRepresentation 6 | => $"(Sgn {OriginalExpression.StringRepresentation})"; 7 | 8 | public override Expression Map(Func mapper) 9 | { 10 | return mapper(new SignExpression(OriginalExpression.Map(mapper))); 11 | } 12 | 13 | public override IEnumerable InnerExpressions { get; } = new[] { OriginalExpression }; 14 | } -------------------------------------------------------------------------------- /Blitz3DDecomp/Basics/Function/HighLevel/Expression/Arithmetic/XorExpression.cs: -------------------------------------------------------------------------------- 1 | namespace Blitz3DDecomp.HighLevel; 2 | 3 | sealed record XorExpression(Expression Lhs, Expression Rhs) : Expression 4 | { 5 | public override string StringRepresentation 6 | => $"({Lhs.StringRepresentation} Xor {Rhs.StringRepresentation})"; 7 | 8 | public override Expression Map(Func mapper) 9 | { 10 | return mapper(new XorExpression(Lhs.Map(mapper), Rhs.Map(mapper))); 11 | } 12 | 13 | public override IEnumerable InnerExpressions { get; } = new[] { Lhs, Rhs }; 14 | } -------------------------------------------------------------------------------- /Blitz3DDecomp/Basics/Function/HighLevel/Expression/Object/AfterExpression.cs: -------------------------------------------------------------------------------- 1 | namespace Blitz3DDecomp.HighLevel; 2 | 3 | sealed record AfterExpression(Expression OriginalExpression) : Expression 4 | { 5 | public override string StringRepresentation 6 | => $"(After {OriginalExpression.StringRepresentation})"; 7 | 8 | public override Expression Map(Func mapper) 9 | { 10 | return mapper(new AfterExpression(OriginalExpression.Map(mapper))); 11 | } 12 | 13 | public override IEnumerable InnerExpressions { get; } = new[] { OriginalExpression }; 14 | } -------------------------------------------------------------------------------- /Blitz3DDecomp/Basics/Function/HighLevel/Statement/Loops/Repeat/RepeatStatement.cs: -------------------------------------------------------------------------------- 1 | namespace Blitz3DDecomp.HighLevel.Loops.DoWhile; 2 | 3 | sealed record RepeatStatement : Statement 4 | { 5 | public override string StringRepresentation 6 | => "Repeat"; 7 | 8 | public override IEnumerable InnerExpressions => Enumerable.Empty(); 9 | 10 | protected override Statement MapImplementation(Func statementMapper, Func expressionMapper) 11 | => statementMapper(this); 12 | 13 | public override int IndentationToAdd => 1; 14 | } -------------------------------------------------------------------------------- /B3DDecompUtils/B3DDecompUtils.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | true 11 | 12 | 13 | 14 | true 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /Blitz3DDecomp/Basics/Function/HighLevel/Expression/Object/BeforeExpression.cs: -------------------------------------------------------------------------------- 1 | namespace Blitz3DDecomp.HighLevel; 2 | 3 | sealed record BeforeExpression(Expression OriginalExpression) : Expression 4 | { 5 | public override string StringRepresentation 6 | => $"(Before {OriginalExpression.StringRepresentation})"; 7 | 8 | public override Expression Map(Func mapper) 9 | { 10 | return mapper(new BeforeExpression(OriginalExpression.Map(mapper))); 11 | } 12 | 13 | public override IEnumerable InnerExpressions { get; } = new[] { OriginalExpression }; 14 | } -------------------------------------------------------------------------------- /Blitz3DDecomp/Basics/Function/HighLevel/Expression/Arithmetic/DivideExpression.cs: -------------------------------------------------------------------------------- 1 | namespace Blitz3DDecomp.HighLevel; 2 | 3 | sealed record DivideExpression(Expression Lhs, Expression Rhs) : Expression 4 | { 5 | public override string StringRepresentation 6 | => $"({Lhs.StringRepresentation} / {Rhs.StringRepresentation})"; 7 | 8 | public override Expression Map(Func mapper) 9 | { 10 | return mapper(new DivideExpression(Lhs.Map(mapper), Rhs.Map(mapper))); 11 | } 12 | 13 | public override IEnumerable InnerExpressions { get; } = new[] { Lhs, Rhs }; 14 | } -------------------------------------------------------------------------------- /Blitz3DDecomp/Basics/Function/HighLevel/Expression/Arithmetic/ModuloExpression.cs: -------------------------------------------------------------------------------- 1 | namespace Blitz3DDecomp.HighLevel; 2 | 3 | sealed record ModuloExpression(Expression Lhs, Expression Rhs) : Expression 4 | { 5 | public override string StringRepresentation 6 | => $"({Lhs.StringRepresentation} Mod {Rhs.StringRepresentation})"; 7 | 8 | public override Expression Map(Func mapper) 9 | { 10 | return mapper(new ModuloExpression(Lhs.Map(mapper), Rhs.Map(mapper))); 11 | } 12 | 13 | public override IEnumerable InnerExpressions { get; } = new[] { Lhs, Rhs }; 14 | } -------------------------------------------------------------------------------- /Blitz3DDecomp/Basics/Function/HighLevel/Expression/Arithmetic/SignFlipExpression.cs: -------------------------------------------------------------------------------- 1 | namespace Blitz3DDecomp.HighLevel; 2 | 3 | sealed record SignFlipExpression(Expression OriginalExpression) : Expression 4 | { 5 | public override string StringRepresentation 6 | => $"(- {OriginalExpression.StringRepresentation})"; 7 | 8 | public override Expression Map(Func mapper) 9 | { 10 | return mapper(new SignFlipExpression(OriginalExpression.Map(mapper))); 11 | } 12 | 13 | public override IEnumerable InnerExpressions { get; } = new[] { OriginalExpression }; 14 | } -------------------------------------------------------------------------------- /Blitz3DDecomp/Basics/Function/HighLevel/Expression/Access/ArrayAccessExpression.cs: -------------------------------------------------------------------------------- 1 | namespace Blitz3DDecomp.HighLevel; 2 | 3 | sealed record ArrayAccessExpression(Expression Owner, Expression Index) : AccessExpression 4 | { 5 | public override string StringRepresentation 6 | => $"{Owner.StringRepresentation}[{Index.StringRepresentation}]"; 7 | 8 | public override Expression Map(Func mapper) 9 | => mapper(new ArrayAccessExpression(Owner.Map(mapper), Index.Map(mapper))); 10 | 11 | public override IEnumerable InnerExpressions { get; } = new[] { Owner, Index }; 12 | } -------------------------------------------------------------------------------- /Blitz3DDecomp/Basics/Function/HighLevel/Expression/Arithmetic/MultiplyExpression.cs: -------------------------------------------------------------------------------- 1 | namespace Blitz3DDecomp.HighLevel; 2 | 3 | sealed record MultiplyExpression(Expression Lhs, Expression Rhs) : Expression 4 | { 5 | public override string StringRepresentation 6 | => $"({Lhs.StringRepresentation} * {Rhs.StringRepresentation})"; 7 | 8 | public override Expression Map(Func mapper) 9 | { 10 | return mapper(new MultiplyExpression(Lhs.Map(mapper), Rhs.Map(mapper))); 11 | } 12 | 13 | public override IEnumerable InnerExpressions { get; } = new[] { Lhs, Rhs }; 14 | } -------------------------------------------------------------------------------- /Blitz3DDecomp/Basics/Function/HighLevel/Expression/Arithmetic/SubtractExpression.cs: -------------------------------------------------------------------------------- 1 | namespace Blitz3DDecomp.HighLevel; 2 | 3 | sealed record SubtractExpression(Expression Lhs, Expression Rhs) : Expression 4 | { 5 | public override string StringRepresentation 6 | => $"({Lhs.StringRepresentation} - {Rhs.StringRepresentation})"; 7 | 8 | public override Expression Map(Func mapper) 9 | { 10 | return mapper(new SubtractExpression(Lhs.Map(mapper), Rhs.Map(mapper))); 11 | } 12 | 13 | public override IEnumerable InnerExpressions { get; } = new[] { Lhs, Rhs }; 14 | } -------------------------------------------------------------------------------- /Blitz3DDecomp/Basics/Function/HighLevel/Statement/If/EndIfStatement.cs: -------------------------------------------------------------------------------- 1 | namespace Blitz3DDecomp.HighLevel; 2 | 3 | sealed record EndIfStatement : Statement 4 | { 5 | public override string StringRepresentation 6 | => "EndIf"; 7 | 8 | public override IEnumerable InnerExpressions => Enumerable.Empty(); 9 | 10 | protected override Statement MapImplementation( 11 | Func statementMapper, 12 | Func expressionMapper) 13 | => statementMapper(this); 14 | 15 | public override int IndentationToSubtract => 1; 16 | } -------------------------------------------------------------------------------- /Blitz3DDecomp/Basics/Function/HighLevel/Statement/Object/DeleteEachStatement.cs: -------------------------------------------------------------------------------- 1 | namespace Blitz3DDecomp.HighLevel; 2 | 3 | sealed record DeleteEachStatement(CustomType ObjectType) : Statement 4 | { 5 | public override string StringRepresentation 6 | => $"Delete Each {ObjectType.Name}"; 7 | 8 | public override IEnumerable InnerExpressions => Enumerable.Empty(); 9 | 10 | protected override Statement MapImplementation( 11 | Func statementMapper, 12 | Func expressionMapper) 13 | => statementMapper(this); 14 | } -------------------------------------------------------------------------------- /Blitz3DDecomp/Basics/Function/HighLevel/Expression/Arithmetic/LogicalOrExpression.cs: -------------------------------------------------------------------------------- 1 | namespace Blitz3DDecomp.HighLevel; 2 | 3 | sealed record LogicalOrExpression(Expression Lhs, Expression Rhs) : Expression 4 | { 5 | public override string StringRepresentation 6 | => $"({Lhs.StringRepresentation} Lor {Rhs.StringRepresentation})"; 7 | 8 | public override Expression Map(Func mapper) 9 | { 10 | return mapper(new LogicalOrExpression(Lhs.Map(mapper), Rhs.Map(mapper))); 11 | } 12 | 13 | public override IEnumerable InnerExpressions { get; } = new[] { Lhs, Rhs }; 14 | } -------------------------------------------------------------------------------- /Blitz3DDecomp/Basics/Function/HighLevel/Expression/Arithmetic/ShiftLeftExpression.cs: -------------------------------------------------------------------------------- 1 | namespace Blitz3DDecomp.HighLevel; 2 | 3 | sealed record ShiftLeftExpression(Expression Lhs, Expression Rhs) : Expression 4 | { 5 | public override string StringRepresentation 6 | => $"({Lhs.StringRepresentation} Shl {Rhs.StringRepresentation})"; 7 | 8 | public override Expression Map(Func mapper) 9 | { 10 | return mapper(new ShiftLeftExpression(Lhs.Map(mapper), Rhs.Map(mapper))); 11 | } 12 | 13 | public override IEnumerable InnerExpressions { get; } = new[] { Lhs, Rhs }; 14 | } -------------------------------------------------------------------------------- /Blitz3DDecomp/Basics/Function/HighLevel/Statement/Loops/For/NextStatement.cs: -------------------------------------------------------------------------------- 1 | namespace Blitz3DDecomp.HighLevel; 2 | 3 | sealed record NextStatement : Statement 4 | { 5 | public override string StringRepresentation 6 | => "Next"; 7 | 8 | public override IEnumerable InnerExpressions => Enumerable.Empty(); 9 | 10 | protected override Statement MapImplementation( 11 | Func statementMapper, 12 | Func expressionMapper) 13 | => statementMapper(this); 14 | 15 | public override int IndentationToSubtract => 1; 16 | } -------------------------------------------------------------------------------- /Blitz3DDecomp/Basics/Function/HighLevel/Statement/Loops/While/WendStatement.cs: -------------------------------------------------------------------------------- 1 | namespace Blitz3DDecomp.HighLevel; 2 | 3 | sealed record WendStatement : Statement 4 | { 5 | public override string StringRepresentation 6 | => "Wend"; 7 | 8 | public override IEnumerable InnerExpressions => Enumerable.Empty(); 9 | 10 | protected override Statement MapImplementation( 11 | Func statementMapper, 12 | Func expressionMapper) 13 | => statementMapper(this); 14 | 15 | public override int IndentationToSubtract => 1; 16 | } -------------------------------------------------------------------------------- /Blitz3DDecomp/Basics/Function/HighLevel/Expression/Arithmetic/LogicalAndExpression.cs: -------------------------------------------------------------------------------- 1 | namespace Blitz3DDecomp.HighLevel; 2 | 3 | sealed record LogicalAndExpression(Expression Lhs, Expression Rhs) : Expression 4 | { 5 | public override string StringRepresentation 6 | => $"({Lhs.StringRepresentation} And {Rhs.StringRepresentation})"; 7 | 8 | public override Expression Map(Func mapper) 9 | { 10 | return mapper(new LogicalAndExpression(Lhs.Map(mapper), Rhs.Map(mapper))); 11 | } 12 | 13 | public override IEnumerable InnerExpressions { get; } = new[] { Lhs, Rhs }; 14 | } -------------------------------------------------------------------------------- /Blitz3DDecomp/Basics/Function/HighLevel/Expression/Conversions/ConvertToIntExpression.cs: -------------------------------------------------------------------------------- 1 | namespace Blitz3DDecomp.HighLevel; 2 | 3 | sealed record ConvertToIntExpression(Expression OriginalExpression) : Expression 4 | { 5 | public override string StringRepresentation 6 | => $"(Int {OriginalExpression.StringRepresentation})"; 7 | 8 | public override Expression Map(Func mapper) 9 | { 10 | return mapper(new ConvertToIntExpression(OriginalExpression.Map(mapper))); 11 | } 12 | 13 | public override IEnumerable InnerExpressions { get; } = new[] { OriginalExpression }; 14 | } -------------------------------------------------------------------------------- /Blitz3DDecomp/Basics/Function/HighLevel/Expression/Conversions/ConvertToFloatExpression.cs: -------------------------------------------------------------------------------- 1 | namespace Blitz3DDecomp.HighLevel; 2 | 3 | sealed record ConvertToFloatExpression(Expression OriginalExpression) : Expression 4 | { 5 | public override string StringRepresentation 6 | => $"(Float {OriginalExpression.StringRepresentation})"; 7 | 8 | public override Expression Map(Func mapper) 9 | { 10 | return mapper(new ConvertToFloatExpression(OriginalExpression.Map(mapper))); 11 | } 12 | 13 | public override IEnumerable InnerExpressions { get; } = new[] { OriginalExpression }; 14 | } -------------------------------------------------------------------------------- /Blitz3DDecomp/Basics/Function/HighLevel/Expression/Conversions/ConvertToStringExpression.cs: -------------------------------------------------------------------------------- 1 | namespace Blitz3DDecomp.HighLevel; 2 | 3 | sealed record ConvertToStringExpression(Expression OriginalExpression) : Expression 4 | { 5 | public override string StringRepresentation 6 | => $"(Str {OriginalExpression.StringRepresentation})"; 7 | 8 | public override Expression Map(Func mapper) 9 | { 10 | return mapper(new ConvertToStringExpression(OriginalExpression.Map(mapper))); 11 | } 12 | 13 | public override IEnumerable InnerExpressions { get; } = new[] { OriginalExpression }; 14 | } -------------------------------------------------------------------------------- /Blitz3DDecomp/Basics/Function/HighLevel/Statement/Loops/Repeat/ForeverStatement.cs: -------------------------------------------------------------------------------- 1 | namespace Blitz3DDecomp.HighLevel.Loops.DoWhile; 2 | 3 | sealed record ForeverStatement : Statement 4 | { 5 | public override string StringRepresentation 6 | => "Forever"; 7 | 8 | public override IEnumerable InnerExpressions => Enumerable.Empty(); 9 | 10 | protected override Statement MapImplementation( 11 | Func statementMapper, 12 | Func expressionMapper) 13 | => statementMapper(this); 14 | 15 | public override int IndentationToSubtract => 1; 16 | } -------------------------------------------------------------------------------- /B3DDecompUtils/PathUtils.cs: -------------------------------------------------------------------------------- 1 | namespace B3DDecompUtils; 2 | public static class PathUtils 3 | { 4 | public static string CleanupPath(this string path) 5 | => path.Replace("\\", "/"); 6 | 7 | public static string AppendToPath(this string path, string fsEntry) 8 | { 9 | path = path.CleanupPath(); 10 | if (!path.EndsWith("/")) { path += "/"; } 11 | return path + fsEntry; 12 | } 13 | 14 | public static bool IsDisasmPath(this string path) 15 | => path.EndsWith("_disasm/", StringComparison.OrdinalIgnoreCase) 16 | || path.EndsWith("_disasm", StringComparison.OrdinalIgnoreCase); 17 | } 18 | -------------------------------------------------------------------------------- /Blitz3DDecomp/Basics/Function/HighLevel/Expression/Access/DimAccessExpression.cs: -------------------------------------------------------------------------------- 1 | namespace Blitz3DDecomp.HighLevel; 2 | 3 | sealed record DimAccessExpression(DimArray Owner, params Expression[] Indices) : AccessExpression 4 | { 5 | public override string StringRepresentation 6 | => $"{Owner.Name}({string.Join(", ", Indices.Select(i => i.StringRepresentation))})"; 7 | 8 | public override Expression Map(Func mapper) 9 | => mapper(new DimAccessExpression(Owner, Indices.Select(i => i.Map(mapper)).ToArray())); 10 | 11 | public override IEnumerable InnerExpressions 12 | => Indices; 13 | } -------------------------------------------------------------------------------- /Blitz3DDecomp/Basics/Function/HighLevel/Expression/Arithmetic/ShiftRightSignedExpression.cs: -------------------------------------------------------------------------------- 1 | namespace Blitz3DDecomp.HighLevel; 2 | 3 | sealed record ShiftRightSignedExpression(Expression Lhs, Expression Rhs) : Expression 4 | { 5 | public override string StringRepresentation 6 | => $"({Lhs.StringRepresentation} Sar {Rhs.StringRepresentation})"; 7 | 8 | public override Expression Map(Func mapper) 9 | { 10 | return mapper(new ShiftRightSignedExpression(Lhs.Map(mapper), Rhs.Map(mapper))); 11 | } 12 | 13 | public override IEnumerable InnerExpressions { get; } = new[] { Lhs, Rhs }; 14 | } -------------------------------------------------------------------------------- /Blitz3DDecomp/Basics/Function/HighLevel/Statement/Select/EndSelectStatement.cs: -------------------------------------------------------------------------------- 1 | namespace Blitz3DDecomp.HighLevel.Select; 2 | 3 | sealed record EndSelectStatement : Statement 4 | { 5 | public override string StringRepresentation 6 | => "End Select"; 7 | 8 | public override IEnumerable InnerExpressions => Enumerable.Empty(); 9 | 10 | protected override Statement MapImplementation( 11 | Func statementMapper, 12 | Func expressionMapper) 13 | => statementMapper(this); 14 | 15 | public override int IndentationToSubtract => 2; 16 | } -------------------------------------------------------------------------------- /B3DDecompUtils/SpanExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace B3DDecompUtils; 2 | 3 | public static class SpanExtensions 4 | { 5 | public static bool Any(this ReadOnlySpan span, Predicate predicate) 6 | { 7 | foreach (var item in span) 8 | { 9 | if (predicate(item)) { return true; } 10 | } 11 | return false; 12 | } 13 | 14 | public static int Count(this ReadOnlySpan span, Predicate predicate) 15 | { 16 | int counter = 0; 17 | foreach (var item in span) 18 | { 19 | if (predicate(item)) { counter++; } 20 | } 21 | return counter; 22 | } 23 | } -------------------------------------------------------------------------------- /Blitz3DDecomp/Basics/Function/HighLevel/Expression/Arithmetic/ShiftRightUnsignedExpression.cs: -------------------------------------------------------------------------------- 1 | namespace Blitz3DDecomp.HighLevel; 2 | 3 | sealed record ShiftRightUnsignedExpression(Expression Lhs, Expression Rhs) : Expression 4 | { 5 | public override string StringRepresentation 6 | => $"({Lhs.StringRepresentation} Shr {Rhs.StringRepresentation})"; 7 | 8 | public override Expression Map(Func mapper) 9 | { 10 | return mapper(new ShiftRightUnsignedExpression(Lhs.Map(mapper), Rhs.Map(mapper))); 11 | } 12 | 13 | public override IEnumerable InnerExpressions { get; } = new[] { Lhs, Rhs }; 14 | } -------------------------------------------------------------------------------- /Blitz3DDecomp/Basics/Function/HighLevel/Expression/Conversions/ConvertObjectToHandleExpression.cs: -------------------------------------------------------------------------------- 1 | namespace Blitz3DDecomp.HighLevel; 2 | 3 | sealed record ConvertObjectToHandleExpression(Expression ObjectExpression) : Expression 4 | { 5 | public override string StringRepresentation 6 | => $"(Handle {ObjectExpression.StringRepresentation})"; 7 | 8 | public override Expression Map(Func mapper) 9 | { 10 | return mapper(new ConvertObjectToHandleExpression(ObjectExpression.Map(mapper))); 11 | } 12 | 13 | public override IEnumerable InnerExpressions { get; } = new[] { ObjectExpression }; 14 | } -------------------------------------------------------------------------------- /Blitz3DDecomp/Basics/Function/HighLevel/Statement/Select/DefaultStatement.cs: -------------------------------------------------------------------------------- 1 | namespace Blitz3DDecomp.HighLevel.Select; 2 | 3 | sealed record DefaultStatement : Statement 4 | { 5 | public override string StringRepresentation 6 | => $"Default"; 7 | 8 | public override IEnumerable InnerExpressions => Enumerable.Empty(); 9 | 10 | protected override Statement MapImplementation(Func statementMapper, Func expressionMapper) 11 | => statementMapper(this); 12 | 13 | public override int IndentationToAdd => 1; 14 | public override int IndentationToSubtract => 1; 15 | } -------------------------------------------------------------------------------- /Blitz3DDecomp/Basics/Function/HighLevel/Expression/Arithmetic/ExponentiationExpression.cs: -------------------------------------------------------------------------------- 1 | namespace Blitz3DDecomp.HighLevel; 2 | 3 | sealed record ExponentiationExpression(Expression Base, Expression Exponent) : Expression 4 | { 5 | public override string StringRepresentation 6 | => $"({Base.StringRepresentation} ^ {Exponent.StringRepresentation})"; 7 | 8 | public override Expression Map(Func mapper) 9 | { 10 | return mapper(new ExponentiationExpression(Base.Map(mapper), Exponent.Map(mapper))); 11 | } 12 | 13 | public override IEnumerable InnerExpressions { get; } = new[] { Base, Exponent }; 14 | } -------------------------------------------------------------------------------- /Blitz3DDecomp/Basics/Function/HighLevel/Statement/ReturnStatement.cs: -------------------------------------------------------------------------------- 1 | namespace Blitz3DDecomp.HighLevel; 2 | 3 | sealed record ReturnStatement(Expression Expression) : Statement 4 | { 5 | public override string StringRepresentation 6 | => $"Return {Expression.StringRepresentation}"; 7 | 8 | public override IEnumerable InnerExpressions { get; } = new[] { Expression }; 9 | 10 | protected override Statement MapImplementation( 11 | Func statementMapper, 12 | Func expressionMapper) 13 | => statementMapper(new ReturnStatement(Expression.Map(expressionMapper))); 14 | } -------------------------------------------------------------------------------- /Blitz3DDecomp/Basics/DebugTrace.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Immutable; 2 | using System.Diagnostics; 3 | using B3DDecompUtils; 4 | 5 | namespace Blitz3DDecomp; 6 | 7 | public readonly struct DebugTrace 8 | { 9 | private readonly ImmutableArray? messages; 10 | public ImmutableArray Messages => messages ?? ImmutableArray.Empty; 11 | 12 | private DebugTrace(ImmutableArray messages) 13 | { 14 | this.messages = messages; 15 | } 16 | 17 | public DebugTrace Append(string msg) 18 | { 19 | Logger.WriteLine(msg); 20 | return new DebugTrace(messages: Messages.Insert(0, msg)); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /B3DDecompUtils/Logger.cs: -------------------------------------------------------------------------------- 1 | //#define LOGGER_ENABLED 2 | using System.Text; 3 | 4 | namespace B3DDecompUtils; 5 | 6 | public static class Logger 7 | { 8 | #if LOGGER_ENABLED 9 | private static FileStream? stream; 10 | 11 | public static void WriteLine(string line) 12 | { 13 | stream ??= File.Create("log.txt"); 14 | 15 | Console.WriteLine(line); 16 | stream.Write(Encoding.UTF8.GetBytes(line+"\n")); 17 | } 18 | 19 | public static void End() 20 | { 21 | stream?.Flush(); 22 | stream?.Dispose(); 23 | } 24 | #else 25 | public static void WriteLine(string _) { } 26 | public static void End() { } 27 | #endif 28 | } -------------------------------------------------------------------------------- /Blitz3DDecomp/Basics/Function/HighLevel/Statement/If/ElseStatement.cs: -------------------------------------------------------------------------------- 1 | namespace Blitz3DDecomp.HighLevel; 2 | 3 | sealed record ElseStatement : Statement 4 | { 5 | public override string StringRepresentation 6 | => "Else"; 7 | 8 | public override IEnumerable InnerExpressions => Enumerable.Empty(); 9 | 10 | protected override Statement MapImplementation( 11 | Func statementMapper, 12 | Func expressionMapper) 13 | => statementMapper(this); 14 | 15 | public override int IndentationToAdd => 1; 16 | public override int IndentationToSubtract => 1; 17 | } 18 | -------------------------------------------------------------------------------- /Blitz3DDecomp/Basics/Function/HighLevel/Statement/If/IfStatement.cs: -------------------------------------------------------------------------------- 1 | namespace Blitz3DDecomp.HighLevel; 2 | 3 | sealed record IfStatement(Expression Condition) : Statement 4 | { 5 | public override string StringRepresentation 6 | => $"If {Condition.StringRepresentation} Then"; 7 | 8 | public override IEnumerable InnerExpressions { get; } = new[] { Condition }; 9 | 10 | protected override Statement MapImplementation(Func statementMapper, Func expressionMapper) 11 | => statementMapper(new IfStatement(Condition.Map(expressionMapper))); 12 | 13 | public override int IndentationToAdd => 1; 14 | } -------------------------------------------------------------------------------- /Blitz3DDecomp/Basics/Variables/FieldVariable.cs: -------------------------------------------------------------------------------- 1 | namespace Blitz3DDecomp; 2 | 3 | sealed class FieldVariable : Variable 4 | { 5 | public readonly Variable Owner; 6 | public readonly CustomType.Field TypeField; 7 | 8 | public FieldVariable(Variable owner, CustomType.Field field) : base($"{owner.Name}\\{field.Name}") 9 | { 10 | Owner = owner; 11 | TypeField = field; 12 | } 13 | 14 | public override DeclType DeclType 15 | { 16 | get => TypeField.DeclType; 17 | set => throw new InvalidOperationException("Cannot set field decltype!"); 18 | } 19 | 20 | public override string ToInstructionArg() 21 | => Name; 22 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Build results 2 | [Dd]ebug/ 3 | [Dd]ebugPublic/ 4 | [Rr]elease/ 5 | [Rr]eleases/ 6 | x64/ 7 | x86/ 8 | build/ 9 | bld/ 10 | [Bb]in/ 11 | [Oo]bj/ 12 | [Dd]ebugWindows/ 13 | [Rr]eleaseWindows/ 14 | [Dd]ebugMac/ 15 | [Rr]eleaseMac/ 16 | [Dd]ebuLinux/ 17 | [Rr]eleaseLinux/ 18 | *.o 19 | 20 | # Misc vs crap 21 | *.v12.suo 22 | *.suo 23 | *.csproj.user 24 | *.shproj.user 25 | *.vcxproj.user 26 | 27 | # Rider 28 | .idea/ 29 | *.DotSettings.user 30 | 31 | #performance reports & sessions 32 | *.vsp 33 | *.psess 34 | .vs/ 35 | *.diagsession 36 | 37 | # Mac 38 | *.DS_Store 39 | 40 | # Win 41 | desktop.ini 42 | 43 | #Rider 44 | *.DotSettings.user 45 | 46 | # Misc other crap 47 | ignore/ 48 | -------------------------------------------------------------------------------- /Blitz3DDecomp/Basics/Function/HighLevel/Statement/Loops/While/WhileStatement.cs: -------------------------------------------------------------------------------- 1 | namespace Blitz3DDecomp.HighLevel; 2 | 3 | sealed record WhileStatement(Expression Condition) : Statement 4 | { 5 | public override string StringRepresentation 6 | => $"While {Condition.StringRepresentation}"; 7 | 8 | public override IEnumerable InnerExpressions { get; } = new[] { Condition }; 9 | 10 | protected override Statement MapImplementation(Func statementMapper, Func expressionMapper) 11 | => statementMapper(new WhileStatement(Condition.Map(expressionMapper))); 12 | 13 | public override int IndentationToAdd => 1; 14 | } -------------------------------------------------------------------------------- /Blitz3DDecomp/Basics/Function/HighLevel/Statement/FreeStandingExpressionStatement.cs: -------------------------------------------------------------------------------- 1 | namespace Blitz3DDecomp.HighLevel; 2 | 3 | sealed record FreeStandingExpressionStatement(Expression Expression) : Statement 4 | { 5 | public override string StringRepresentation 6 | => Expression.StringRepresentation; 7 | 8 | public override IEnumerable InnerExpressions { get; } = new[] { Expression }; 9 | 10 | protected override Statement MapImplementation( 11 | Func statementMapper, 12 | Func expressionMapper) 13 | => statementMapper(new FreeStandingExpressionStatement(Expression.Map(expressionMapper))); 14 | } -------------------------------------------------------------------------------- /Blitz3DDecomp/Basics/Function/HighLevel/Statement/Select/SelectStatement.cs: -------------------------------------------------------------------------------- 1 | namespace Blitz3DDecomp.HighLevel.Select; 2 | 3 | sealed record SelectStatement(Expression Expression) : Statement 4 | { 5 | public override string StringRepresentation 6 | => $"Select {Expression.StringRepresentation}"; 7 | 8 | public override IEnumerable InnerExpressions { get; } = new[] { Expression }; 9 | 10 | protected override Statement MapImplementation(Func statementMapper, Func expressionMapper) 11 | => statementMapper(new SelectStatement(Expression.Map(expressionMapper))); 12 | 13 | public override int IndentationToAdd => 2; 14 | } -------------------------------------------------------------------------------- /Blitz3DDecomp/Basics/Function/HighLevel/Expression/Conversions/ConvertHandleToObjectExpression.cs: -------------------------------------------------------------------------------- 1 | namespace Blitz3DDecomp.HighLevel; 2 | 3 | sealed record ConvertHandleToObjectExpression(Expression HandleExpression, CustomType ObjectType) : Expression 4 | { 5 | public override string StringRepresentation 6 | => $"(Object.{ObjectType.Name} {HandleExpression.StringRepresentation})"; 7 | 8 | public override Expression Map(Func mapper) 9 | { 10 | return mapper(new ConvertHandleToObjectExpression(HandleExpression.Map(mapper), ObjectType)); 11 | } 12 | 13 | public override IEnumerable InnerExpressions { get; } = new[] { HandleExpression }; 14 | } -------------------------------------------------------------------------------- /Blitz3DDecomp/Basics/Function/HighLevel/Statement/Loops/Repeat/UntilStatement.cs: -------------------------------------------------------------------------------- 1 | namespace Blitz3DDecomp.HighLevel.Loops.DoWhile; 2 | 3 | sealed record UntilStatement(Expression Condition) : Statement 4 | { 5 | public override string StringRepresentation 6 | => $"Until {Condition.StringRepresentation}"; 7 | 8 | public override IEnumerable InnerExpressions { get; } = new[] { Condition }; 9 | protected override Statement MapImplementation(Func statementMapper, Func expressionMapper) 10 | => statementMapper(new UntilStatement(Condition.Map(expressionMapper))); 11 | 12 | public override int IndentationToSubtract => 1; 13 | } -------------------------------------------------------------------------------- /Blitz3DDecomp/Basics/Function/HighLevel/Statement/Object/DestructorStatement.cs: -------------------------------------------------------------------------------- 1 | namespace Blitz3DDecomp.HighLevel; 2 | 3 | sealed record DestructorStatement(Expression ObjectExpression) : Statement 4 | { 5 | public override string StringRepresentation 6 | => $"Delete {ObjectExpression.StringRepresentation}"; 7 | 8 | public override IEnumerable InnerExpressions { get; } = new[] { ObjectExpression }; 9 | 10 | protected override Statement MapImplementation( 11 | Func statementMapper, 12 | Func expressionMapper) 13 | => statementMapper(new DestructorStatement(ObjectExpression.Map(expressionMapper))); 14 | } -------------------------------------------------------------------------------- /B3DDecompUtils/ParsingExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | 3 | namespace B3DDecompUtils; 4 | 5 | public static class ParsingExtensions 6 | { 7 | public static UInt32 HexToUint32(this string s) 8 | { 9 | if (s.StartsWith("0x")) { s = s[2..]; } 10 | return UInt32.Parse(s, NumberStyles.HexNumber); 11 | } 12 | 13 | public static bool TryHexToUint32(this string s, out UInt32 value) 14 | { 15 | if (s.StartsWith("0x")) { s = s[2..]; } 16 | return UInt32.TryParse(s, NumberStyles.HexNumber, null, out value); 17 | } 18 | 19 | public static bool IsHexDigit(this char c) 20 | => c is (>= '0' and <= '9') or (>= 'A' and <= 'F') or (>= 'a' and <= 'f'); 21 | } -------------------------------------------------------------------------------- /Blitz3DDecomp/Basics/Function/HighLevel/Statement/If/ElseIfStatement.cs: -------------------------------------------------------------------------------- 1 | namespace Blitz3DDecomp.HighLevel; 2 | 3 | sealed record ElseIfStatement(Expression Condition) : Statement 4 | { 5 | public override string StringRepresentation 6 | => $"ElseIf {Condition.StringRepresentation} Then"; 7 | 8 | public override IEnumerable InnerExpressions { get; } = new[] { Condition }; 9 | 10 | protected override Statement MapImplementation(Func statementMapper, Func expressionMapper) 11 | => statementMapper(new IfStatement(Condition.Map(expressionMapper))); 12 | 13 | public override int IndentationToAdd => 1; 14 | public override int IndentationToSubtract => 1; 15 | } -------------------------------------------------------------------------------- /Blitz3DDecomp/Basics/Function/HighLevel/Statement/AllocateDimStatement.cs: -------------------------------------------------------------------------------- 1 | namespace Blitz3DDecomp.HighLevel; 2 | 3 | sealed record AllocateDimStatement(DimArray Dim, params Expression[] Dimensions) : Statement 4 | { 5 | public override string StringRepresentation 6 | => $"Dim {Dim.Name}{Dim.ElementDeclType.Suffix}({string.Join(", ", Dimensions.Select(d => d.StringRepresentation))})"; 7 | 8 | public override IEnumerable InnerExpressions => Dimensions; 9 | 10 | protected override Statement MapImplementation( 11 | Func statementMapper, 12 | Func expressionMapper) 13 | => statementMapper(new AllocateDimStatement(Dim, Dimensions.Select(e => e.Map(expressionMapper)).ToArray())); 14 | } -------------------------------------------------------------------------------- /Blitz3DDecomp/Basics/Variables/ArrayElementVariable.cs: -------------------------------------------------------------------------------- 1 | namespace Blitz3DDecomp; 2 | 3 | sealed class ArrayElementVariable : Variable 4 | { 5 | public readonly Variable Owner; 6 | public readonly string Index; 7 | 8 | public ArrayElementVariable(Variable owner, string index) : base($"{owner.Name}[{index}]") 9 | { 10 | Owner = owner; 11 | Index = index; 12 | } 13 | 14 | public override DeclType DeclType 15 | { 16 | get => Owner.DeclType.GetElementType() 17 | ?? throw new Exception($"Invalid array element variable from owner {Owner}"); 18 | set => throw new InvalidOperationException("Can't set the type of an array element variable"); 19 | } 20 | 21 | public override string ToInstructionArg() 22 | => Name; 23 | } -------------------------------------------------------------------------------- /Blitz3DDecomp/Basics/Function/HighLevel/Statement/Select/CaseStatement.cs: -------------------------------------------------------------------------------- 1 | namespace Blitz3DDecomp.HighLevel.Select; 2 | 3 | sealed record CaseStatement(Expression[] Expressions) : Statement 4 | { 5 | public override string StringRepresentation 6 | => $"Case {string.Join(",",Expressions.Select(expr => expr.StringRepresentation))}"; 7 | 8 | public override IEnumerable InnerExpressions => Expressions; 9 | 10 | protected override Statement MapImplementation(Func statementMapper, Func expressionMapper) 11 | => statementMapper(new CaseStatement(Expressions.Select(expr => expr.Map(expressionMapper)).ToArray())); 12 | 13 | public override int IndentationToAdd => 1; 14 | public override int IndentationToSubtract => 1; 15 | } -------------------------------------------------------------------------------- /Blitz3DDecomp/DecompilerSteps/Step0/CountArguments.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | 3 | namespace Blitz3DDecomp; 4 | 5 | static class CountArguments 6 | { 7 | public static void Process(Function function) 8 | { 9 | if (!function.AssemblySections.Any()) { return; } 10 | var leaveSection = function.AssemblySections.Last(s => s.Name.Contains($"_leave{function.CoreSymbolName}", StringComparison.Ordinal)); 11 | var retInstruction = leaveSection.Instructions[^1]; 12 | var retValueStr = retInstruction.DestArg[2..]; 13 | var retValue = int.Parse(retValueStr, NumberStyles.HexNumber); 14 | function.Parameters.AddRange(Enumerable.Range(0, retValue / 4).Select(i => new Function.Parameter(function, $"arg{i}", i) { DeclType = DeclType.Unknown })); 15 | } 16 | } -------------------------------------------------------------------------------- /Blitz3DDecomp/Basics/Function/HighLevel/Statement/Jumps/JumpIfExpressionStatement.cs: -------------------------------------------------------------------------------- 1 | namespace Blitz3DDecomp.HighLevel; 2 | 3 | sealed record JumpIfExpressionStatement(Expression Condition, string SectionName, Function Function) : JumpStatement(SectionName, Function) 4 | { 5 | public override string StringRepresentation 6 | => $"If {Condition.StringRepresentation} Then {base.StringRepresentation}"; 7 | 8 | public override IEnumerable InnerExpressions { get; } = new[] { Condition }; 9 | 10 | protected override Statement MapImplementation( 11 | Func statementMapper, 12 | Func expressionMapper) 13 | => statementMapper(new JumpIfExpressionStatement(Condition.Map(expressionMapper), SectionName, Function)); 14 | } -------------------------------------------------------------------------------- /Blitz3DDecomp/Basics/Function/HighLevel/Statement/DataSection/DataReadStatement.cs: -------------------------------------------------------------------------------- 1 | namespace Blitz3DDecomp.HighLevel; 2 | 3 | sealed record DataReadStatement(AccessExpression Destination) : Statement 4 | { 5 | public override string StringRepresentation 6 | => $"Read {Destination.StringRepresentation}"; 7 | 8 | public override IEnumerable InnerExpressions { get; } = new[] { Destination }; 9 | 10 | protected override Statement MapImplementation(Func statementMapper, Func expressionMapper) 11 | { 12 | return statementMapper(new DataReadStatement( 13 | Destination.Map(expressionMapper) as AccessExpression 14 | ?? throw new Exception("expressionMapper did not return an AccessExpression"))); 15 | } 16 | } -------------------------------------------------------------------------------- /Blitz3DDecomp/Basics/Function/HighLevel/Expression/ComparisonResults/CompareToZero/OneIfZeroExpression.cs: -------------------------------------------------------------------------------- 1 | using Blitz3DDecomp.HighLevel.ComparisonResults; 2 | 3 | namespace Blitz3DDecomp.HighLevel; 4 | 5 | sealed record OneIfZeroExpression(Expression OriginalExpression) : BooleanExpression 6 | { 7 | public override string StringRepresentation 8 | => $"({OriginalExpression.StringRepresentation} = 0)"; 9 | 10 | public override Expression Map(Func mapper) 11 | { 12 | return mapper(new OneIfZeroExpression(OriginalExpression.Map(mapper))); 13 | } 14 | 15 | public override IEnumerable InnerExpressions { get; } = new[] { OriginalExpression }; 16 | 17 | public override BooleanExpression Negated 18 | => new OneIfNotZeroExpression(OriginalExpression); 19 | } -------------------------------------------------------------------------------- /Blitz3DDecomp/Basics/Function/HighLevel/Expression/ComparisonResults/CompareToZero/OneIfNotZeroExpression.cs: -------------------------------------------------------------------------------- 1 | using Blitz3DDecomp.HighLevel.ComparisonResults; 2 | 3 | namespace Blitz3DDecomp.HighLevel; 4 | 5 | sealed record OneIfNotZeroExpression(Expression OriginalExpression) : BooleanExpression 6 | { 7 | public override string StringRepresentation 8 | => $"({OriginalExpression.StringRepresentation} <> 0)"; 9 | 10 | public override Expression Map(Func mapper) 11 | { 12 | return mapper(new OneIfNotZeroExpression(OriginalExpression.Map(mapper))); 13 | } 14 | 15 | public override IEnumerable InnerExpressions { get; } = new[] { OriginalExpression }; 16 | 17 | public override BooleanExpression Negated 18 | => new OneIfZeroExpression(OriginalExpression); 19 | } -------------------------------------------------------------------------------- /Blitz3DDecomp/Basics/LibSymbols.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Immutable; 2 | 3 | namespace Blitz3DDecomp; 4 | 5 | static class LibSymbols 6 | { 7 | public readonly record struct Dll(string DllName, ImmutableArray Entries); 8 | 9 | public readonly record struct Entry(string DisasmName, string DllSymbolName) 10 | { 11 | public int? ParameterCount 12 | => DllSymbolName.LastIndexOf('@') is >= 0 and var argCountIndex 13 | && int.TryParse(DllSymbolName[(argCountIndex + 1)..], out var argCount) 14 | && (argCount % 4 == 0) 15 | ? (argCount >> 2) 16 | : null; 17 | 18 | public string BlitzName => DisasmName[2..]; 19 | public string DecompName => BlitzName + "__LIBS"; 20 | } 21 | 22 | public static readonly List Dlls = new List(); 23 | } -------------------------------------------------------------------------------- /Blitz3DDecomp/Basics/Function/HighLevel/Statement/Statement.cs: -------------------------------------------------------------------------------- 1 | namespace Blitz3DDecomp.HighLevel; 2 | 3 | abstract record Statement 4 | { 5 | public abstract string StringRepresentation { get; } 6 | public abstract IEnumerable InnerExpressions { get; } 7 | 8 | protected abstract Statement MapImplementation( 9 | Func statementMapper, 10 | Func expressionMapper); 11 | 12 | public Statement Map( 13 | Func statementMapper, 14 | Func? expressionMapper = null) 15 | { 16 | expressionMapper ??= expr => expr; 17 | return MapImplementation(statementMapper, expressionMapper); 18 | } 19 | 20 | public virtual int IndentationToAdd => 0; 21 | public virtual int IndentationToSubtract => 0; 22 | } -------------------------------------------------------------------------------- /Blitz3DDecomp/Basics/Function/HighLevel/Expression/ComparisonResults/CompareToOther/OneIfExpressionsEqualExpression.cs: -------------------------------------------------------------------------------- 1 | using Blitz3DDecomp.HighLevel.ComparisonResults; 2 | 3 | namespace Blitz3DDecomp.HighLevel; 4 | 5 | sealed record OneIfExpressionsEqualExpression(Expression Lhs, Expression Rhs) : BooleanExpression 6 | { 7 | public override string StringRepresentation 8 | => $"({Lhs.StringRepresentation} = {Rhs.StringRepresentation})"; 9 | 10 | public override Expression Map(Func mapper) 11 | { 12 | return mapper(new OneIfExpressionsEqualExpression(Lhs.Map(mapper), Rhs.Map(mapper))); 13 | } 14 | 15 | public override IEnumerable InnerExpressions { get; } = new[] { Lhs, Rhs }; 16 | 17 | public override BooleanExpression Negated 18 | => new OneIfExpressionsNotEqualExpression(Lhs, Rhs); 19 | } -------------------------------------------------------------------------------- /Blitz3DDecomp/Basics/Function/HighLevel/Expression/ComparisonResults/CompareToZero/OneIfLessThanZeroExpression.cs: -------------------------------------------------------------------------------- 1 | using Blitz3DDecomp.HighLevel.ComparisonResults; 2 | 3 | namespace Blitz3DDecomp.HighLevel; 4 | 5 | sealed record OneIfLessThanZeroExpression(Expression OriginalExpression) : BooleanExpression 6 | { 7 | public override string StringRepresentation 8 | => $"({OriginalExpression.StringRepresentation} < 0)"; 9 | 10 | public override Expression Map(Func mapper) 11 | { 12 | return mapper(new OneIfLessThanZeroExpression(OriginalExpression.Map(mapper))); 13 | } 14 | 15 | public override IEnumerable InnerExpressions { get; } = new[] { OriginalExpression }; 16 | 17 | public override BooleanExpression Negated 18 | => new OneIfGreaterThanOrEqualToZeroExpression(OriginalExpression); 19 | } -------------------------------------------------------------------------------- /Blitz3DDecomp/Basics/Function/HighLevel/Expression/ComparisonResults/CompareToZero/OneIfGreaterThanZeroExpression.cs: -------------------------------------------------------------------------------- 1 | using Blitz3DDecomp.HighLevel.ComparisonResults; 2 | 3 | namespace Blitz3DDecomp.HighLevel; 4 | 5 | sealed record OneIfGreaterThanZeroExpression(Expression OriginalExpression) : BooleanExpression 6 | { 7 | public override string StringRepresentation 8 | => $"({OriginalExpression.StringRepresentation} > 0)"; 9 | 10 | public override Expression Map(Func mapper) 11 | { 12 | return mapper(new OneIfGreaterThanZeroExpression(OriginalExpression.Map(mapper))); 13 | } 14 | 15 | public override IEnumerable InnerExpressions { get; } = new[] { OriginalExpression }; 16 | 17 | public override BooleanExpression Negated 18 | => new OneIfLessThanOrEqualToZeroExpression(OriginalExpression); 19 | } -------------------------------------------------------------------------------- /Blitz3DDecomp/Basics/Function/HighLevel/Expression/ComparisonResults/CompareToOther/OneIfExpressionsNotEqualExpression.cs: -------------------------------------------------------------------------------- 1 | using Blitz3DDecomp.HighLevel.ComparisonResults; 2 | 3 | namespace Blitz3DDecomp.HighLevel; 4 | 5 | sealed record OneIfExpressionsNotEqualExpression(Expression Lhs, Expression Rhs) : BooleanExpression 6 | { 7 | public override string StringRepresentation 8 | => $"({Lhs.StringRepresentation} <> {Rhs.StringRepresentation})"; 9 | 10 | public override Expression Map(Func mapper) 11 | { 12 | return mapper(new OneIfExpressionsNotEqualExpression(Lhs.Map(mapper), Rhs.Map(mapper))); 13 | } 14 | 15 | public override IEnumerable InnerExpressions { get; } = new[] { Lhs, Rhs }; 16 | 17 | public override BooleanExpression Negated 18 | => new OneIfExpressionsEqualExpression(Lhs, Rhs); 19 | } -------------------------------------------------------------------------------- /Blitz3DDecomp/Basics/Function/HighLevel/Expression/ComparisonResults/CompareToZero/OneIfLessThanOrEqualToZeroExpression.cs: -------------------------------------------------------------------------------- 1 | using Blitz3DDecomp.HighLevel.ComparisonResults; 2 | 3 | namespace Blitz3DDecomp.HighLevel; 4 | 5 | sealed record OneIfLessThanOrEqualToZeroExpression(Expression OriginalExpression) : BooleanExpression 6 | { 7 | public override string StringRepresentation 8 | => $"({OriginalExpression.StringRepresentation} <= 0)"; 9 | 10 | public override Expression Map(Func mapper) 11 | { 12 | return mapper(new OneIfLessThanOrEqualToZeroExpression(OriginalExpression.Map(mapper))); 13 | } 14 | 15 | public override IEnumerable InnerExpressions { get; } = new[] { OriginalExpression }; 16 | 17 | public override BooleanExpression Negated 18 | => new OneIfGreaterThanZeroExpression(OriginalExpression); 19 | } -------------------------------------------------------------------------------- /Blitz3DDecomp/Basics/Function/HighLevel/Statement/Object/InsertAfterStatement.cs: -------------------------------------------------------------------------------- 1 | namespace Blitz3DDecomp.HighLevel; 2 | 3 | sealed record InsertAfterStatement(Expression ObjectToInsert, Expression ObjectThatComesBefore) : Statement 4 | { 5 | public override string StringRepresentation 6 | => $"Insert {ObjectToInsert.StringRepresentation} After {ObjectThatComesBefore.StringRepresentation}"; 7 | 8 | public override IEnumerable InnerExpressions { get; } = new[] { ObjectToInsert, ObjectThatComesBefore }; 9 | 10 | protected override Statement MapImplementation( 11 | Func statementMapper, 12 | Func expressionMapper) 13 | => statementMapper(new InsertAfterStatement( 14 | ObjectToInsert.Map(expressionMapper), 15 | ObjectThatComesBefore.Map(expressionMapper))); 16 | } -------------------------------------------------------------------------------- /Blitz3DDecomp/Basics/Function/HighLevel/Statement/Object/InsertBeforeStatement.cs: -------------------------------------------------------------------------------- 1 | namespace Blitz3DDecomp.HighLevel; 2 | 3 | sealed record InsertBeforeStatement(Expression ObjectToInsert, Expression ObjectThatComesAfter) : Statement 4 | { 5 | public override string StringRepresentation 6 | => $"Insert {ObjectToInsert.StringRepresentation} Before {ObjectThatComesAfter.StringRepresentation}"; 7 | 8 | public override IEnumerable InnerExpressions { get; } = new[] { ObjectToInsert, ObjectThatComesAfter }; 9 | 10 | protected override Statement MapImplementation( 11 | Func statementMapper, 12 | Func expressionMapper) 13 | => statementMapper(new InsertBeforeStatement( 14 | ObjectToInsert.Map(expressionMapper), 15 | ObjectThatComesAfter.Map(expressionMapper))); 16 | } -------------------------------------------------------------------------------- /Blitz3DDecomp/Basics/Function/HighLevel/Expression/ComparisonResults/CompareToZero/OneIfGreaterThanOrEqualToZeroExpression.cs: -------------------------------------------------------------------------------- 1 | using Blitz3DDecomp.HighLevel.ComparisonResults; 2 | 3 | namespace Blitz3DDecomp.HighLevel; 4 | 5 | sealed record OneIfGreaterThanOrEqualToZeroExpression(Expression OriginalExpression) : BooleanExpression 6 | { 7 | public override string StringRepresentation 8 | => $"({OriginalExpression.StringRepresentation} >= 0)"; 9 | 10 | public override Expression Map(Func mapper) 11 | { 12 | return mapper(new OneIfGreaterThanOrEqualToZeroExpression(OriginalExpression.Map(mapper))); 13 | } 14 | 15 | public override IEnumerable InnerExpressions { get; } = new[] { OriginalExpression }; 16 | 17 | public override BooleanExpression Negated 18 | => new OneIfLessThanZeroExpression(OriginalExpression); 19 | } -------------------------------------------------------------------------------- /Blitz3DDecomp/DecompilerSteps/Step5/CleanupUselessGoto.cs: -------------------------------------------------------------------------------- 1 | using Blitz3DDecomp.HighLevel; 2 | 3 | namespace Blitz3DDecomp.DecompilerSteps.Step5; 4 | 5 | static class CleanupUselessGoto 6 | { 7 | public static void Process(Function function) 8 | { 9 | foreach (var section in function.HighLevelSections) 10 | { 11 | var sectionStartIndex = section.StartIndex; 12 | if (sectionStartIndex <= 0) { continue; } 13 | if (function.HighLevelStatements[sectionStartIndex - 1] is UnconditionalJumpStatement { SectionName: var sectionName } 14 | && section.Name == sectionName) 15 | { 16 | function.FindSectionForStatementIndex(sectionStartIndex - 1, out var prevSection, out var indexInSection); 17 | prevSection.Statements.RemoveAt(indexInSection); 18 | } 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /Blitz3DDecomp/Basics/Function/HighLevel/Statement/AssignmentStatement.cs: -------------------------------------------------------------------------------- 1 | namespace Blitz3DDecomp.HighLevel; 2 | 3 | sealed record AssignmentStatement(AccessExpression Destination, Expression Source) : Statement 4 | { 5 | public override string StringRepresentation 6 | => $"{Destination.StringRepresentation} = {Source.StringRepresentation}"; 7 | 8 | public override IEnumerable InnerExpressions { get; } = new[] { Destination, Source }; 9 | 10 | protected override Statement MapImplementation( 11 | Func statementMapper, 12 | Func expressionMapper) 13 | => statementMapper(new AssignmentStatement( 14 | Destination.Map(expressionMapper) as AccessExpression ?? throw new Exception("expressionMapper did not return an AccessExpression"), 15 | Source.Map(expressionMapper))); 16 | } -------------------------------------------------------------------------------- /Blitz3DDecomp/Basics/Function/HighLevel/Statement/Loops/For/ForEachStatement.cs: -------------------------------------------------------------------------------- 1 | namespace Blitz3DDecomp.HighLevel; 2 | 3 | sealed record ForEachStatement(AccessExpression Iterator, CustomType Type) : Statement 4 | { 5 | public override string StringRepresentation 6 | => $"For {Iterator.StringRepresentation} = Each {Type.Name}"; 7 | 8 | public override IEnumerable InnerExpressions { get; } = new[] { Iterator }; 9 | 10 | protected override Statement MapImplementation( 11 | Func statementMapper, 12 | Func expressionMapper) 13 | => statementMapper(new ForEachStatement( 14 | Iterator.Map(expressionMapper) as AccessExpression ?? throw new Exception($"expressionMapper did not return an AccessExpression"), 15 | Type)); 16 | 17 | public override int IndentationToAdd => 1; 18 | } -------------------------------------------------------------------------------- /Blitz3DDecomp/Basics/Function/HighLevel/Expression/ComparisonResults/CompareToOther/OneIfExpressionIsLessThanOtherExpression.cs: -------------------------------------------------------------------------------- 1 | using Blitz3DDecomp.HighLevel.ComparisonResults; 2 | 3 | namespace Blitz3DDecomp.HighLevel; 4 | 5 | sealed record OneIfExpressionIsLessThanOtherExpression(Expression Lhs, Expression Rhs) : BooleanExpression 6 | { 7 | public override string StringRepresentation 8 | => $"({Lhs.StringRepresentation} < {Rhs.StringRepresentation})"; 9 | 10 | public override Expression Map(Func mapper) 11 | { 12 | return mapper(new OneIfExpressionIsLessThanOtherExpression(Lhs.Map(mapper), Rhs.Map(mapper))); 13 | } 14 | 15 | public override IEnumerable InnerExpressions { get; } = new[] { Lhs, Rhs }; 16 | 17 | public override BooleanExpression Negated 18 | => new OneIfExpressionIsGreaterThanOrEqualToOtherExpression(Lhs, Rhs); 19 | } -------------------------------------------------------------------------------- /Blitz3DDecomp/Basics/Function/HighLevel/Expression/ComparisonResults/CompareToOther/OneIfExpressionIsGreaterThanOtherExpression.cs: -------------------------------------------------------------------------------- 1 | using Blitz3DDecomp.HighLevel.ComparisonResults; 2 | 3 | namespace Blitz3DDecomp.HighLevel; 4 | 5 | sealed record OneIfExpressionIsGreaterThanOtherExpression(Expression Lhs, Expression Rhs) : BooleanExpression 6 | { 7 | public override string StringRepresentation 8 | => $"({Lhs.StringRepresentation} > {Rhs.StringRepresentation})"; 9 | 10 | public override Expression Map(Func mapper) 11 | { 12 | return mapper(new OneIfExpressionIsGreaterThanOtherExpression(Lhs.Map(mapper), Rhs.Map(mapper))); 13 | } 14 | 15 | public override IEnumerable InnerExpressions { get; } = new[] { Lhs, Rhs }; 16 | 17 | public override BooleanExpression Negated 18 | => new OneIfExpressionIsLessThanOrEqualToOtherExpression(Lhs, Rhs); 19 | } -------------------------------------------------------------------------------- /Blitz3DDecomp/Blitz3DDecomp.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net8.0 6 | enable 7 | enable 8 | 12 9 | 10 | 11 | 12 | true 13 | ..\bin\Debug\ 14 | 15 | 16 | 17 | true 18 | ..\bin\Release\ 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /Blitz3DDecomp/Basics/Function/HighLevel/Expression/ComparisonResults/CompareToOther/OneIfExpressionIsLessThanOrEqualToOtherExpression.cs: -------------------------------------------------------------------------------- 1 | using Blitz3DDecomp.HighLevel.ComparisonResults; 2 | 3 | namespace Blitz3DDecomp.HighLevel; 4 | 5 | sealed record OneIfExpressionIsLessThanOrEqualToOtherExpression(Expression Lhs, Expression Rhs) : BooleanExpression 6 | { 7 | public override string StringRepresentation 8 | => $"({Lhs.StringRepresentation} <= {Rhs.StringRepresentation})"; 9 | 10 | public override Expression Map(Func mapper) 11 | { 12 | return mapper(new OneIfExpressionIsLessThanOrEqualToOtherExpression(Lhs.Map(mapper), Rhs.Map(mapper))); 13 | } 14 | 15 | public override IEnumerable InnerExpressions { get; } = new[] { Lhs, Rhs }; 16 | 17 | public override BooleanExpression Negated 18 | => new OneIfExpressionIsGreaterThanOtherExpression(Lhs, Rhs); 19 | } -------------------------------------------------------------------------------- /Blitz3DDecomp/Basics/Function/HighLevel/Expression/ComparisonResults/CompareToOther/OneIfExpressionIsGreaterThanOrEqualToOtherExpression.cs: -------------------------------------------------------------------------------- 1 | using Blitz3DDecomp.HighLevel.ComparisonResults; 2 | 3 | namespace Blitz3DDecomp.HighLevel; 4 | 5 | sealed record OneIfExpressionIsGreaterThanOrEqualToOtherExpression(Expression Lhs, Expression Rhs) : BooleanExpression 6 | { 7 | public override string StringRepresentation 8 | => $"({Lhs.StringRepresentation} >= {Rhs.StringRepresentation})"; 9 | 10 | public override Expression Map(Func mapper) 11 | { 12 | return mapper(new OneIfExpressionIsGreaterThanOrEqualToOtherExpression(Lhs.Map(mapper), Rhs.Map(mapper))); 13 | } 14 | 15 | public override IEnumerable InnerExpressions { get; } = new[] { Lhs, Rhs }; 16 | 17 | public override BooleanExpression Negated 18 | => new OneIfExpressionIsLessThanOtherExpression(Lhs, Rhs); 19 | } -------------------------------------------------------------------------------- /B3DDecompUtils/Primitives/Either.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | 3 | namespace B3DDecompUtils.Primitives; 4 | 5 | public readonly struct Either 6 | where T1 : notnull 7 | where T2 : notnull 8 | { 9 | private readonly Option option1; 10 | private readonly Option option2; 11 | 12 | private Either(Option option1, Option option2) 13 | { 14 | this.option1 = option1; 15 | this.option2 = option2; 16 | } 17 | 18 | public static Either From1(T1 value1) 19 | => new Either(Option.Some(value1), Option.None); 20 | 21 | public static Either From2(T2 value2) 22 | => new Either(Option.None, Option.Some(value2)); 23 | 24 | public bool TryUnwrap1([NotNullWhen(returnValue: true)] out T1? value1) 25 | => option1.TryUnwrap(out value1); 26 | 27 | public bool TryUnwrap2([NotNullWhen(returnValue: true)] out T2? value2) 28 | => option2.TryUnwrap(out value2); 29 | } 30 | -------------------------------------------------------------------------------- /Blitz3DDecomp/Basics/Variables/GlobalVariable.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | 3 | namespace Blitz3DDecomp; 4 | 5 | sealed class GlobalVariable : VariableWithOwnType 6 | { 7 | public static ICollection AllGlobals => lookupDictionary.Values; 8 | private static Dictionary lookupDictionary = new(); 9 | 10 | public static GlobalVariable? FindByName(string name) 11 | { 12 | name = name.ToLowerInvariant(); 13 | if (name.Length >= 1 && name[0] == '@') { name = name[1..]; } 14 | if (name.Length >= 2 && name[0] == '_' && name[1] == 'v') { name = name[2..]; } 15 | 16 | if (lookupDictionary.TryGetValue(name, out var global)) { return global; } 17 | return null; 18 | } 19 | 20 | public GlobalVariable(string name) : base(name) 21 | { 22 | lookupDictionary.Add(name.ToLowerInvariant(), this); 23 | } 24 | 25 | public override string ToInstructionArg() 26 | => $"[@_v{Name.ToLowerInvariant()}]"; 27 | } 28 | -------------------------------------------------------------------------------- /Blitz3DDecomp/Basics/CustomType.cs: -------------------------------------------------------------------------------- 1 | namespace Blitz3DDecomp; 2 | 3 | sealed class CustomType 4 | { 5 | public sealed record Field( 6 | CustomType Owner, 7 | string Name, 8 | DeclType DeclType) 9 | { 10 | public override string ToString() 11 | => $"{Name}{DeclType.Suffix}"; 12 | } 13 | 14 | public static readonly List AllTypes = new List(); 15 | 16 | public static CustomType GetTypeWithName(string name) 17 | { 18 | if (name[0] =='.') { name = name[1..]; } 19 | return AllTypes.FirstOrDefault(t => t.Name == name) 20 | ?? throw new Exception($"Custom type of name {name} was not loaded from symbols"); 21 | } 22 | 23 | public static CustomType GetTypeMatchingDeclType(DeclType declType) 24 | => GetTypeWithName(declType.Suffix); 25 | 26 | public readonly string Name; 27 | public readonly List Fields = new List(); 28 | 29 | public CustomType(string name) 30 | { 31 | Name = name; 32 | AllTypes.Add(this); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Blitz3DDecomp/Basics/Function/HighLevel/Statement/Loops/For/ForOnIntStatement.cs: -------------------------------------------------------------------------------- 1 | namespace Blitz3DDecomp.HighLevel; 2 | 3 | sealed record ForOnIntStatement(AccessExpression Iterator, Expression Start, Expression End, Expression Step) : Statement 4 | { 5 | public override string StringRepresentation 6 | => $"For {Iterator.StringRepresentation} = {Start.StringRepresentation} To {End.StringRepresentation} Step {Step.StringRepresentation}"; 7 | 8 | public override IEnumerable InnerExpressions { get; } = new[] { Iterator }; 9 | 10 | protected override Statement MapImplementation( 11 | Func statementMapper, 12 | Func expressionMapper) 13 | => statementMapper(new ForOnIntStatement( 14 | Iterator.Map(expressionMapper) as AccessExpression ?? throw new Exception($"expressionMapper did not return an AccessExpression"), 15 | Start.Map(expressionMapper), 16 | End.Map(expressionMapper), 17 | Step.Map(expressionMapper))); 18 | 19 | public override int IndentationToAdd => 1; 20 | } -------------------------------------------------------------------------------- /Blitz3DDecomp/DecompilerSteps/Step0/IngestLibInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Immutable; 2 | using B3DDecompUtils; 3 | 4 | namespace Blitz3DDecomp; 5 | 6 | static class IngestLibInfo 7 | { 8 | public static void FromDir(string disasmPath) 9 | { 10 | var inputDir = disasmPath.AppendToPath("Libs"); 11 | if (!Directory.Exists(inputDir)) { return; } 12 | foreach (var filePath in Directory.GetFiles(inputDir)) 13 | { 14 | var lines = File.ReadAllLines(filePath); 15 | var dllName = lines[0].Trim(); 16 | var entries = new List(); 17 | foreach (var line in lines.Skip(1)) 18 | { 19 | var split = line.Split(':', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); 20 | var disasmName = split[0]; 21 | var dllSymbolName = split[1]; 22 | 23 | entries.Add(new LibSymbols.Entry(disasmName, dllSymbolName)); 24 | } 25 | LibSymbols.Dlls.Add(new LibSymbols.Dll(dllName, entries.ToImmutableArray())); 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /Blitz3DDisasm/Blitz3DDisasm.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net8.0 6 | enable 7 | enable 8 | Blitz3DDecomp 9 | 10 | 11 | 12 | true 13 | ..\bin\Debug\ 14 | 15 | 16 | 17 | true 18 | ..\bin\Release\ 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /Blitz3DDecomp/Basics/Function/HighLevel/Expression/CallExpression.cs: -------------------------------------------------------------------------------- 1 | namespace Blitz3DDecomp.HighLevel; 2 | 3 | sealed record CallExpression(Function Callee, params Expression[] Arguments) : Expression 4 | { 5 | public override string StringRepresentation 6 | { 7 | get 8 | { 9 | string calleeName = Callee.Name; 10 | if (calleeName.StartsWith("_builtIn_f", StringComparison.Ordinal)) 11 | { 12 | calleeName = calleeName["_builtIn_f".Length..]; 13 | } 14 | else if (calleeName.EndsWith("__LIBS", StringComparison.Ordinal)) 15 | { 16 | calleeName = calleeName[..^"__LIBS".Length]; 17 | } 18 | return $"{calleeName}({string.Join(", ", Arguments.Select(a => a.StringRepresentation))})"; 19 | } 20 | } 21 | 22 | public override Expression Map(Func mapper) 23 | { 24 | return mapper(new CallExpression(Callee, Arguments.Select(a => a.Map(mapper)).ToArray())); 25 | } 26 | 27 | public override IEnumerable InnerExpressions 28 | => Arguments; 29 | } -------------------------------------------------------------------------------- /Blitz3DDecomp/Basics/Registers.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Immutable; 2 | 3 | namespace Blitz3DDecomp; 4 | 5 | static class Registers 6 | { 7 | public static readonly ImmutableArray Names 8 | = new[] { "eax", "ebx", "ecx", "edx", "edi", "esi", "esp", "ebp" }.ToImmutableArray(); 9 | 10 | public static bool IsRegister(this string s) 11 | => Names.Contains(s); 12 | 13 | public static bool ContainsRegister(this string s) 14 | => Names.Any(n => s.StartsWith(n) || s.StartsWith($"[{n}")); 15 | 16 | public static bool ContainsRegister(this string s, string register) 17 | => s.ContainsRegister() && Names.Contains(register) && s.Contains(register); 18 | 19 | public static string StripDeref(this string s) 20 | { 21 | if (s.StartsWith("dword [", StringComparison.Ordinal) && s.EndsWith("]", StringComparison.Ordinal)) 22 | { 23 | return s[7..^1]; 24 | } 25 | 26 | if (s.StartsWith("[", StringComparison.Ordinal) && s.EndsWith("]", StringComparison.Ordinal)) 27 | { 28 | return s[1..^1]; 29 | } 30 | 31 | return s; 32 | } 33 | } -------------------------------------------------------------------------------- /Blitz3DDecomp/Basics/Function/LowLevel/AssemblySection.cs: -------------------------------------------------------------------------------- 1 | namespace Blitz3DDecomp.LowLevel; 2 | 3 | sealed class AssemblySection 4 | { 5 | public readonly Function Owner; 6 | public readonly string Name; 7 | 8 | public readonly Range InstructionRange; 9 | public ReadOnlySpan Instructions 10 | => Owner.Instructions.AsSpan()[InstructionRange]; 11 | 12 | public int PreambleEndIndex = -1; 13 | 14 | public AssemblySection(Function owner, string name, Range instructionRange) 15 | { 16 | Owner = owner; 17 | Name = name; 18 | InstructionRange = instructionRange; 19 | } 20 | 21 | public IEnumerable ReferencedGlobals 22 | => ReferencedVariables 23 | .OfType(); 24 | 25 | public IEnumerable ReferencedVariables 26 | => Instructions 27 | .ToArray() 28 | .SelectMany(i => new[] { i.DestArg, i.SrcArg1, i.SrcArg2 }) 29 | .Select(s => s.StripDeref()) 30 | .Select(Owner.InstructionArgumentToVariable) 31 | .OfType() // Removes null entries 32 | .Distinct(); 33 | } 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # B3DDecomp 2 | 3 | Blitz3D and BlitzPlus game disassembler and decompiler 4 | 5 | This isn't 100% functional yet so the UX sucks, but here are some usage instructions if you're curious: 6 | - Build Blitz3DDisasm and Blitz3DDecomp 7 | - Navigate to the bin directory at the root of the repository 8 | - Drag and drop a Blitz3D or BlitzPlus executable onto Blitz3DDecomp 9 | 10 | ### Note For SCP - Containment Breach modders 11 | 12 | If you're seeing an exception and the decompiler fails to generate a `Main.bb` and `Functions` folder, you probably need to give the game's [`fmod.decls`](https://raw.githubusercontent.com/Regalis11/scpcb/refs/heads/master/fmod.decls) as input, like so: 13 | ``` 14 | ./Blitz3DDecomp [GAME_EXE] fmod.decls 15 | ``` 16 | 17 | This is a bug in SCP - Containment Breach. The decls, for whatever reason, incorrectly remove a parameter from `FSOUND_Stream_Open`. This breaks the decompiler because it makes the assumption that the compiler had a correct understanding of the number of parameters in the functions it's using, but in this case it did not. I'm not entirely sure how this doesn't completely break the game because it does screw up the stack, but so far nobody's really noticed so idk lol 18 | -------------------------------------------------------------------------------- /Blitz3DDecomp/DecompilerSteps/Step1/DimArrayAccessRewrite.cs: -------------------------------------------------------------------------------- 1 | using Blitz3DDecomp.LowLevel; 2 | 3 | namespace Blitz3DDecomp.DecompilerSteps.Step1; 4 | 5 | static class DimArrayAccessRewrite 6 | { 7 | private static void ProcessSection(AssemblySection section) 8 | { 9 | for (int i = 0; i < section.Instructions.Length; i++) 10 | { 11 | var instruction = section.Instructions[i]; 12 | if (instruction.Name != "add" 13 | || !instruction.SrcArg1[..3].IsRegister() 14 | || !instruction.SrcArg2.StartsWith("[@_a", StringComparison.Ordinal)) 15 | { 16 | continue; 17 | } 18 | 19 | var dimArray = DimArray.TryFindByName(instruction.SrcArg2.StripDeref()) 20 | ?? throw new Exception($"Could not find dim array matching instruction arg {instruction.SrcArg2}"); 21 | instruction.Name = "mov"; 22 | instruction.SrcArg1 = $"{dimArray.Name}[{instruction.SrcArg1}>>2]"; 23 | instruction.SrcArg2 = ""; 24 | } 25 | } 26 | 27 | public static void Process(Function function) 28 | { 29 | foreach (var section in function.AssemblySections) 30 | { 31 | ProcessSection(section); 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /Blitz3DDecomp/DecompilerSteps/Step4/GuessFloatsFromStoreInstructions.cs: -------------------------------------------------------------------------------- 1 | using B3DDecompUtils; 2 | using Blitz3DDecomp.LowLevel; 3 | 4 | namespace Blitz3DDecomp.DecompilerSteps.Step4; 5 | 6 | static class GuessFloatsFromStoreInstructions 7 | { 8 | private static void ProcessSection(AssemblySection section) 9 | { 10 | for (var i = 0; i < section.Instructions.Length - 1; i++) 11 | { 12 | var instruction = section.Instructions[i]; 13 | var nextInstruction = section.Instructions[i + 1]; 14 | if (instruction.Name == "push" && nextInstruction.Name is "fstp" or "fistp") 15 | { 16 | var pushVar = section.Owner.InstructionArgumentToVariable(instruction.DestArg); 17 | if (pushVar?.DeclType == DeclType.Unknown) 18 | { 19 | pushVar.DeclType = DeclType.Float; 20 | pushVar.Trace = pushVar.Trace.Append($"{section.Owner}: {pushVar.Name} is probably {DeclType.Float} because {instruction}"); 21 | } 22 | } 23 | } 24 | } 25 | 26 | public static void Process(Function function) 27 | { 28 | foreach (var section in function.AssemblySections) 29 | { 30 | ProcessSection(section); 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /Blitz3DDecomp/DecompilerSteps/Step2/HandleUnambiguousIntegerInstructions.cs: -------------------------------------------------------------------------------- 1 | using B3DDecompUtils; 2 | using Blitz3DDecomp.LowLevel; 3 | 4 | namespace Blitz3DDecomp.DecompilerSteps.Step2; 5 | 6 | static class HandleUnambiguousIntegerInstructions 7 | { 8 | private static void ProcessSection(AssemblySection section) 9 | { 10 | foreach (var instruction in section.Instructions) 11 | { 12 | if (instruction.Name is not ("shl" or "shr" or "sar")) { continue; } 13 | 14 | var destVar = section.Owner.InstructionArgumentToVariable(instruction.DestArg); 15 | var srcVar = section.Owner.InstructionArgumentToVariable(instruction.SrcArg1); 16 | 17 | void trySetVarToInt(Variable? variable) 18 | { 19 | if (variable?.DeclType != DeclType.Unknown) { return; } 20 | variable.DeclType = DeclType.Int; 21 | variable.Trace = variable.Trace.Append($"{section.Owner}: {variable.Name} is {DeclType.Int} because {instruction}"); 22 | } 23 | 24 | trySetVarToInt(destVar); 25 | trySetVarToInt(srcVar); 26 | } 27 | } 28 | 29 | public static void Process(Function function) 30 | { 31 | foreach (var section in function.AssemblySections) 32 | { 33 | ProcessSection(section); 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /Blitz3DDecomp/DecompilerSteps/Step0/IngestStringConstants.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | using System.Text.RegularExpressions; 3 | using B3DDecompUtils; 4 | 5 | namespace Blitz3DDecomp; 6 | 7 | static class IngestStringConstants 8 | { 9 | public static void FromDir(string inputDir) 10 | { 11 | var symbolDescRegex = new Regex("@([0-9A-F]+): (.+)"); 12 | var symbolValueRegex = new Regex(" \"(.*)\" 00"); 13 | 14 | var filePath = inputDir.AppendToPath("Strings.txt"); 15 | if (!File.Exists(filePath)) { return; } 16 | 17 | var lines = File.ReadAllLines(filePath); 18 | for (int i = 0; i < lines.Length - 1; i++) 19 | { 20 | var line = lines[i]; 21 | var nextLine = lines[i+1]; 22 | var symbolDescMatch = symbolDescRegex.Match(line); 23 | if (!symbolDescMatch.Success) { continue; } 24 | //var symbolAddressStr = symbolDescMatch.Groups[1].Value; 25 | //var symbolAddress = int.Parse(symbolAddressStr, NumberStyles.HexNumber); 26 | var symbolName = symbolDescMatch.Groups[2].Value; 27 | 28 | var symbolValueMatch = symbolValueRegex.Match(nextLine); 29 | if (!symbolValueMatch.Success) { continue; } 30 | var symbolValue = symbolValueMatch.Groups[1].Value; 31 | StringConstants.SymbolToValue[symbolName] = symbolValue; 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /Blitz3DDecomp/DecompilerSteps/Step5/GenerateLibDecls.cs: -------------------------------------------------------------------------------- 1 | using B3DDecompUtils; 2 | 3 | namespace Blitz3DDecomp.DecompilerSteps.Step5; 4 | 5 | static class GenerateLibDecls 6 | { 7 | public static void ToDir(string decompPath) 8 | { 9 | var outputDir = decompPath.AppendToPath("Decls"); 10 | Directory.CreateDirectory(outputDir); 11 | 12 | foreach (var dll in LibSymbols.Dlls) 13 | { 14 | var outputLines = new List(); 15 | 16 | outputLines.Add($".lib \"{dll.DllName}\""); 17 | outputLines.Add(""); 18 | 19 | foreach (var entry in dll.Entries) 20 | { 21 | var function = Function.GetFunctionByName(entry.DecompName); 22 | 23 | string extractSuffixForBlitzSignature(DeclType declType) 24 | { 25 | if (declType.IsCustomType) { return "*"; } 26 | return declType.Suffix; 27 | } 28 | var blitzSignature = 29 | $"{entry.BlitzName}{extractSuffixForBlitzSignature(function.ReturnType)}({string.Join(", ", function.Parameters.Select(p => p.Name + extractSuffixForBlitzSignature(p.DeclType)))})"; 30 | outputLines.Add($"{blitzSignature}:\"{entry.DllSymbolName}\""); 31 | } 32 | File.WriteAllLines(outputDir.AppendToPath(dll.DllName.CleanupPath().Replace("/", "_").Replace(".dll", "") + ".decls"), outputLines); 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /B3DDecompUtils/Primitives/Option.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | 3 | namespace B3DDecompUtils.Primitives; 4 | 5 | public readonly struct Option where T : notnull 6 | { 7 | private readonly bool hasValue; 8 | private readonly T value; 9 | 10 | private Option(bool hasValue, T value) 11 | { 12 | this.hasValue = hasValue; 13 | this.value = value; 14 | } 15 | 16 | public static Option Some(T value) => new Option(hasValue: true, value: value); 17 | public static Option None => default; 18 | 19 | public static implicit operator Option(Option.UnspecifiedNone _) => None; 20 | 21 | public bool IsSome => hasValue; 22 | public bool IsNone => !IsSome; 23 | 24 | public bool TryUnwrap([NotNullWhen(returnValue: true)] out T? outValue) 25 | { 26 | outValue = hasValue ? value : default; 27 | return hasValue; 28 | } 29 | } 30 | 31 | public static class Option 32 | { 33 | public struct UnspecifiedNone {} 34 | public static UnspecifiedNone None => default; 35 | public static Option Some(T value) where T : notnull => Option.Some(value: value); 36 | public static Option FromNullable(T? value) where T : notnull => value is null ? None : Some(value); 37 | 38 | public static Option FirstOrNone(this IEnumerable enumerable, Predicate predicate) where T : notnull 39 | { 40 | foreach (var item in enumerable) 41 | { 42 | if (predicate(item)) { return Some(item); } 43 | } 44 | return None; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Blitz3DDecomp/DecompilerSteps/Step1/LibCallCleanup.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using Blitz3DDecomp.LowLevel; 3 | 4 | namespace Blitz3DDecomp; 5 | 6 | static class LibCallCleanup 7 | { 8 | private static void CrawlUp(Function function, int startIndex) 9 | { 10 | var startInstruction = function.Instructions[startIndex]; 11 | 12 | var register = startInstruction.DestArg; 13 | for (int i = startIndex - 1; i >= 0; i--) 14 | { 15 | var instruction = function.Instructions[i]; 16 | if ((instruction.Name == "mov" && instruction.DestArg == register) 17 | || (instruction.Name == "xchg" && (instruction.DestArg == register || instruction.SrcArg1 == register))) 18 | { 19 | var source = instruction.SrcArg1; 20 | if (source.ContainsRegister()) 21 | { 22 | register = source; 23 | } 24 | else 25 | { 26 | source = source[1..^1]; 27 | startInstruction.DestArg = source; 28 | break; 29 | } 30 | } 31 | } 32 | } 33 | 34 | public static void Process(Function function) 35 | { 36 | for (int i = 0; i < function.Instructions.Length; i++) 37 | { 38 | var instruction = function.Instructions[i]; 39 | if (instruction.Name != "call") { continue; } 40 | if (!instruction.DestArg.ContainsRegister()) { continue; } 41 | CrawlUp(function, i); 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /Blitz3DDecomp/DecompilerSteps/Step3/HandleIntegerSub.cs: -------------------------------------------------------------------------------- 1 | using B3DDecompUtils; 2 | using Blitz3DDecomp.LowLevel; 3 | 4 | namespace Blitz3DDecomp.DecompilerSteps.Step3; 5 | 6 | static class HandleIntegerSub 7 | { 8 | private static bool ProcessSection(AssemblySection section) 9 | { 10 | bool somethingChanged = false; 11 | foreach (var instruction in section.Instructions) 12 | { 13 | if (instruction.Name is not "sub") { continue; } 14 | 15 | var destVar = section.Owner.InstructionArgumentToVariable(instruction.DestArg); 16 | var srcVar = section.Owner.InstructionArgumentToVariable(instruction.SrcArg1); 17 | 18 | void trySetVarToInt(Variable? variable) 19 | { 20 | if (variable?.DeclType != DeclType.Unknown) { return; } 21 | variable.DeclType = DeclType.Int; 22 | variable.Trace = variable.Trace.Append($"{section.Owner}: {variable.Name} is {DeclType.Int} because {instruction}"); 23 | somethingChanged = true; 24 | } 25 | 26 | if (destVar?.DeclType == DeclType.Int 27 | || srcVar?.DeclType == DeclType.Int) 28 | { 29 | trySetVarToInt(destVar); 30 | trySetVarToInt(srcVar); 31 | } 32 | } 33 | return somethingChanged; 34 | } 35 | 36 | public static bool Process(Function function) 37 | { 38 | bool somethingChanged = false; 39 | foreach (var section in function.AssemblySections) 40 | { 41 | somethingChanged |= ProcessSection(section); 42 | } 43 | return somethingChanged; 44 | } 45 | } -------------------------------------------------------------------------------- /Blitz3DDecomp/DecompilerSteps/Step5/CleanupElseIf.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using Blitz3DDecomp.HighLevel; 3 | 4 | namespace Blitz3DDecomp.DecompilerSteps.Step5; 5 | 6 | static class CleanupElseIf 7 | { 8 | public static void Process(Function function) 9 | { 10 | var sectionsByName = function.HighLevelSectionsByName; 11 | for (int i = 0; i < function.HighLevelStatements.Count - 1; i++) 12 | { 13 | if (function.HighLevelStatements[i] is not ElseStatement 14 | || function.HighLevelStatements[i + 1] is not IfStatement ifStatement) { continue; } 15 | int indent = 0; 16 | int j = i + 2; 17 | for (; j < function.HighLevelStatements.Count; j++) 18 | { 19 | indent -= function.HighLevelStatements[j].IndentationToSubtract; 20 | if (indent < 0 && function.HighLevelStatements[j] is not ElseStatement) { break; } 21 | indent += function.HighLevelStatements[j].IndentationToAdd; 22 | } 23 | if (indent != -1 24 | || function.HighLevelStatements[j] is not EndIfStatement 25 | || function.HighLevelStatements[j + 1] is not EndIfStatement) { continue; } 26 | 27 | function.FindSectionForStatementIndex(j, out var endIfSection, out var indexInEndIfSection); 28 | endIfSection.Statements.RemoveAt(indexInEndIfSection); 29 | function.FindSectionForStatementIndex(i, out var elseIfSection, out var indexInElseIfSection); 30 | elseIfSection.Statements.RemoveAt(indexInElseIfSection); 31 | elseIfSection.Statements[indexInElseIfSection] = new ElseIfStatement(ifStatement.Condition); 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /Blitz3DDecomp/Basics/Function/LowLevel/Instruction.cs: -------------------------------------------------------------------------------- 1 | namespace Blitz3DDecomp; 2 | 3 | sealed class Instruction 4 | { 5 | public string Name; 6 | public string DestArg; 7 | public string SrcArg1; 8 | public string SrcArg2; 9 | 10 | public int[]? CallParameterAssignmentIndices; 11 | public Function.DecompGeneratedTempVariable? ReturnOutputVar = null; 12 | 13 | public Function.DecompGeneratedTempVariable? XchgLhsPost = null; 14 | public Function.DecompGeneratedTempVariable? XchgRhsPost = null; 15 | 16 | public Function.DecompGeneratedTempVariable? DivResultVar = null; 17 | public Function.DecompGeneratedTempVariable? DivRemainderVar = null; 18 | 19 | public Function.DecompGeneratedTempVariable? SignExtensionValueVar = null; 20 | public Function.DecompGeneratedTempVariable? SignExtensionSignVar = null; 21 | 22 | public Instruction(string name, string destArg = "", string srcArg1 = "", string srcArg2 = "") 23 | { 24 | Name = name; 25 | DestArg = destArg; 26 | SrcArg1 = srcArg1; 27 | SrcArg2 = srcArg2; 28 | } 29 | 30 | public bool IsJumpOrCall 31 | => Name is 32 | "call" or "jmp" or "je" or "jz" 33 | or "jne" or "jnz" or "jg" or "jge" 34 | or "jl" or "jle"; 35 | 36 | public override string ToString() 37 | { 38 | var retVal = Name; 39 | 40 | if (string.IsNullOrWhiteSpace(DestArg)) { return retVal; } 41 | retVal += " " + DestArg; 42 | 43 | if (string.IsNullOrWhiteSpace(SrcArg1)) { return retVal; } 44 | retVal += ", " + SrcArg1; 45 | 46 | if (string.IsNullOrWhiteSpace(SrcArg2)) { return retVal; } 47 | retVal += ", " + SrcArg2; 48 | 49 | return retVal; 50 | } 51 | } -------------------------------------------------------------------------------- /Blitz3DDecomp/DecompilerSteps/Step4/GuessIntFromInstructions.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using B3DDecompUtils; 3 | using Blitz3DDecomp.LowLevel; 4 | 5 | namespace Blitz3DDecomp.DecompilerSteps.Step4; 6 | 7 | static class GuessIntFromInstructions 8 | { 9 | private static void GuessForVariable(Function function, Variable? variable, string reason) 10 | { 11 | if (variable?.DeclType != DeclType.Unknown) { return; } 12 | 13 | variable.DeclType = DeclType.Int; 14 | variable.Trace = variable.Trace.Append($"{function}: {variable.Name} is probably {variable.DeclType} because {reason}"); 15 | } 16 | 17 | private static void ProcessSection(AssemblySection section) 18 | { 19 | foreach (var instruction in section.Instructions) 20 | { 21 | switch (instruction.Name) 22 | { 23 | case "add" or "cmp" or "xor" or "and" or "or": 24 | var destVar = section.Owner.InstructionArgumentToVariable(instruction.DestArg); 25 | var srcVar1 = section.Owner.InstructionArgumentToVariable(instruction.SrcArg1); 26 | var srcVar2 = section.Owner.InstructionArgumentToVariable(instruction.SrcArg2); 27 | 28 | GuessForVariable(section.Owner, destVar, instruction.ToString()); 29 | GuessForVariable(section.Owner, srcVar1, instruction.ToString()); 30 | GuessForVariable(section.Owner, srcVar2, instruction.ToString()); 31 | break; 32 | } 33 | } 34 | } 35 | 36 | public static void Process(Function function) 37 | { 38 | foreach (var section in function.AssemblySections) 39 | { 40 | ProcessSection(section); 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /Blitz3DDecomp/DecompilerSteps/Step0/TypeDecompiler.cs: -------------------------------------------------------------------------------- 1 | using System.Text.RegularExpressions; 2 | using B3DDecompUtils; 3 | 4 | namespace Blitz3DDecomp; 5 | 6 | static class TypeDecompiler 7 | { 8 | public static void FromDir(string inputDir, string outputDir) 9 | { 10 | var symbolDescRegex = new Regex("@([0-9A-F]+): _t(.+)"); 11 | var symbolValueRegex = new Regex(" Field (.+): (.+)"); 12 | 13 | inputDir = inputDir.AppendToPath("Type"); 14 | if (!Directory.Exists(inputDir)) { return; } 15 | outputDir = outputDir.AppendToPath("Types"); 16 | Directory.CreateDirectory(outputDir); 17 | foreach (var filePath in Directory.GetFiles(inputDir)) 18 | { 19 | var lines = File.ReadAllLines(filePath); 20 | var symbolDescMatch = symbolDescRegex.Match(lines[0]); 21 | var typeName = symbolDescMatch.Groups[2].Value; 22 | var newType = new CustomType(typeName); 23 | foreach (var fieldLine in lines.Skip(1).Where(l => !string.IsNullOrWhiteSpace(l))) 24 | { 25 | var symbolValueMatch = symbolValueRegex.Match(fieldLine); 26 | var fieldName = $"Field{symbolValueMatch.Groups[1].Value}"; 27 | var fieldType = DeclType.FromDesc(symbolValueMatch.Groups[2].Value); 28 | newType.Fields.Add(new CustomType.Field(newType, fieldName, fieldType)); 29 | } 30 | 31 | var outputPath = outputDir.AppendToPath($"{typeName}.bb"); 32 | File.AppendAllText(outputPath, $"Type {typeName}\n"); 33 | File.AppendAllLines(outputPath, newType.Fields.Select(f => $" Field {f.Name}{f.DeclType.Suffix}")); 34 | File.AppendAllText(outputPath, $"End Type\n"); 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /Blitz3DDecomp/Utils/CleanupIndexer.cs: -------------------------------------------------------------------------------- 1 | using B3DDecompUtils; 2 | using Blitz3DDecomp.HighLevel; 3 | 4 | namespace Blitz3DDecomp.Utils; 5 | 6 | static class CleanupIndexer 7 | { 8 | public static Expression Process(Expression expression) 9 | { 10 | switch (expression) 11 | { 12 | case ShiftRightUnsignedExpression { Lhs: ConstantExpression lhs, Rhs: ConstantExpression rhs } 13 | when lhs.Value.TryHexToUint32(out var lhsInt) && rhs.Value.TryHexToUint32(out var rhsInt): 14 | return new ConstantExpression($"0x{(lhsInt >> (int)rhsInt):X1}"); 15 | case ShiftLeftExpression { Lhs: ConstantExpression lhs, Rhs: ConstantExpression rhs } 16 | when lhs.Value.TryHexToUint32(out var lhsInt) && rhs.Value.TryHexToUint32(out var rhsInt): 17 | return new ConstantExpression($"0x{(lhsInt << (int)rhsInt):X1}"); 18 | case ShiftRightUnsignedExpression 19 | { 20 | Lhs: ShiftLeftExpression { Lhs: var lhs2, Rhs: ConstantExpression { Value: "0x2" } }, 21 | Rhs: ConstantExpression { Value: "0x2" } 22 | }: 23 | return lhs2; 24 | case MultiplyExpression { Lhs: ConstantExpression { Value: "0x1" }, Rhs: var rhs }: 25 | return rhs; 26 | case MultiplyExpression { Lhs: var lhs, Rhs: ConstantExpression { Value: "0x1" } }: 27 | return lhs; 28 | case MultiplyExpression { Lhs: ConstantExpression lhs, Rhs: ConstantExpression rhs } 29 | when lhs.Value.TryHexToUint32(out var lhsInt) && rhs.Value.TryHexToUint32(out var rhsInt): 30 | return new ConstantExpression($"0x{(lhsInt * rhsInt):X1}"); 31 | } 32 | 33 | return expression; 34 | } 35 | } -------------------------------------------------------------------------------- /Blitz3DDecomp/DecompilerSteps/Step5/CleanupExit.cs: -------------------------------------------------------------------------------- 1 | using Blitz3DDecomp.HighLevel; 2 | using Blitz3DDecomp.HighLevel.Loops; 3 | using Blitz3DDecomp.HighLevel.Loops.DoWhile; 4 | 5 | namespace Blitz3DDecomp.DecompilerSteps.Step5; 6 | 7 | static class CleanupExit 8 | { 9 | public static void Process(Function function) 10 | { 11 | var sectionsByName = function.HighLevelSectionsByName; 12 | for (int i = 0; i < function.HighLevelStatements.Count; i++) 13 | { 14 | var statement = function.HighLevelStatements[i]; 15 | if (statement is not UnconditionalJumpStatement { PointsToUserSection: false } jumpStatement) { continue; } 16 | 17 | var jumpStatementSection = sectionsByName[jumpStatement.SectionName]; 18 | if (jumpStatementSection.StartIndex < i) { continue; } 19 | 20 | int exitIndex = 0; 21 | int innerLoops = 0; 22 | for (int j = i; j < jumpStatementSection.StartIndex; j++) 23 | { 24 | if (function.HighLevelStatements[j] is WhileStatement or ForEachStatement or ForOnIntStatement or RepeatStatement) 25 | { 26 | innerLoops++; 27 | } 28 | else if (function.HighLevelStatements[j] is NextStatement or WendStatement or UntilStatement or ForeverStatement) 29 | { 30 | innerLoops--; 31 | if (innerLoops < 0) 32 | { 33 | exitIndex = j; 34 | break; 35 | } 36 | } 37 | } 38 | 39 | if (exitIndex != jumpStatementSection.StartIndex - 1) { continue; } 40 | 41 | function.HighLevelStatements[i] = new ExitStatement(); 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /Blitz3DDecomp/DecompilerSteps/Step4/GuessIntFromNothing.cs: -------------------------------------------------------------------------------- 1 | using B3DDecompUtils; 2 | 3 | namespace Blitz3DDecomp.DecompilerSteps.Step4; 4 | 5 | public class GuessIntFromNothing 6 | { 7 | private static void ProcessFunction(Function function) 8 | { 9 | void processVariables(IEnumerable variables) 10 | { 11 | foreach (var variable in variables) 12 | { 13 | if (variable.DeclType == DeclType.Unknown) 14 | { 15 | variable.DeclType = DeclType.Int; 16 | variable.Trace = variable.Trace.Append($"{function}: {variable.Name} is probably {DeclType.Int} because type deduction failed on all prior steps"); 17 | } 18 | } 19 | } 20 | 21 | if (function.ReturnType == DeclType.Unknown) 22 | { 23 | function.ReturnType = DeclType.Int; 24 | function.Trace = function.Trace.Append($"{function}: returns {DeclType.Int} because type deduction failed on all prior steps"); 25 | } 26 | processVariables(function.LocalVariables); 27 | processVariables(function.Parameters); 28 | processVariables(function.DecompGeneratedTempVars.Values); 29 | } 30 | 31 | public static void Execute() 32 | { 33 | foreach (var function in Function.AllFunctions) 34 | { 35 | ProcessFunction(function); 36 | } 37 | 38 | foreach (var global in GlobalVariable.AllGlobals) 39 | { 40 | if (global.DeclType == DeclType.Unknown) 41 | { 42 | global.DeclType = DeclType.Int; 43 | global.Trace = global.Trace.Append($"Global variable {global.Name} is probably {DeclType.Int} because type deduction failed on all prior steps"); 44 | } 45 | } 46 | } 47 | } -------------------------------------------------------------------------------- /SamplePrograms/Sample1/Sample1.bb: -------------------------------------------------------------------------------- 1 | Function IntArithmeticAndCasting%(Arg0%, Arg1$, Arg2#) 2 | Return Arg0 + Int(Arg1) + Int(Arg2) 3 | End Function 4 | 5 | Function SelectStatement$(Arg0%) 6 | Select Arg0 7 | Case 0, 1 8 | Return "Zero or one" 9 | Case 2 10 | Return "Two" 11 | Case 3 12 | Return "Three" 13 | Default 14 | Return "Something else" 15 | End Select 16 | End Function 17 | 18 | Function SelectStatementWithALocal$(Arg0%) 19 | Local Local0% = Arg0 20 | Select Local0 21 | Case 0 22 | Return "Zero" 23 | Default 24 | Return "Something else" 25 | End Select 26 | End Function 27 | 28 | Function IfElseChain$(Arg0%) 29 | If (Arg0 = 0) Or (Arg0 = 1) Then 30 | Return "Zero or one" 31 | ElseIf Arg0 = 2 Then 32 | Return "Two" 33 | ElseIf Arg0 = 3 Then 34 | Return "Three" 35 | Else 36 | Return "Something else" 37 | EndIf 38 | End Function 39 | 40 | Function IfElseChainWithWeirdLocals$(Arg0%) 41 | Local Local0$ 42 | If (Arg0 = 0) Or (Arg0 = 1) Then 43 | Return "Zero or one" 44 | ElseIf Arg0 = 2 Then 45 | Return "Two" 46 | ElseIf Arg0 = 3 Then 47 | Return "Three" 48 | Else 49 | Return "Something else" 50 | EndIf 51 | Local Local1% 52 | End Function 53 | 54 | Type SomeType 55 | Field Field0% 56 | Field Field1$ 57 | Field Field2# 58 | Field Field3.SomeType 59 | End Type 60 | 61 | Function IntArithmeticAndCastingFromSomeType.SomeType(Arg.SomeType) 62 | Local RetVal.SomeType = New SomeType 63 | RetVal\Field0 = IntArithmeticAndCasting(Arg\Field0, Arg\Field1, Arg\Field2) 64 | Return RetVal 65 | End Function 66 | 67 | Function EmptyFunction$() 68 | End Function 69 | 70 | Function EmptyFunction2$() 71 | End Function 72 | 73 | Function ReturnEmptyString$() 74 | Return "" 75 | End Function 76 | 77 | Print SelectStatement(0) 78 | Print SelectStatement(1) 79 | Print SelectStatement(2) 80 | Delay 1000 -------------------------------------------------------------------------------- /Blitz3DDecomp/DecompilerSteps/Step5/DecompileData.cs: -------------------------------------------------------------------------------- 1 | using B3DDecompUtils; 2 | using Blitz3DDecomp.HighLevel; 3 | 4 | namespace Blitz3DDecomp.DecompilerSteps.Step5; 5 | 6 | static class DecompileData 7 | { 8 | public static void Process(string inputDir, string outputDir, HashSet restoreStatements) 9 | { 10 | var inputPath = inputDir.AppendToPath("Data").AppendToPath("__DATA.txt"); 11 | var outputPath = outputDir.AppendToPath("Data.bb"); 12 | if (!File.Exists(inputPath)) { return; } 13 | 14 | var inputLines = File 15 | .ReadAllLines(inputPath) 16 | .Skip(1) 17 | .Select(l => l.Trim()) 18 | .Where(l => !string.IsNullOrEmpty(l)) 19 | .ToArray(); 20 | int currOffset = 0; 21 | var outputLines = new List(); 22 | foreach (var line in inputLines) 23 | { 24 | if (restoreStatements.Any(stmt => stmt.Offset == $"DATA_{currOffset:X8}")) 25 | { 26 | if (outputLines.Count > 0) 27 | { 28 | outputLines.Add(""); 29 | } 30 | 31 | outputLines.Add($".DATA_{currOffset:X8}"); 32 | } 33 | 34 | var value = line[4..]; 35 | var type = line[..4] switch 36 | { 37 | "STR:" => DeclType.String, 38 | "FLT:" => DeclType.Float, 39 | "INT:" => DeclType.Int, 40 | _ => throw new ArgumentOutOfRangeException() 41 | }; 42 | outputLines.Add($"Data {ConvertConstantsToFinalRepresentation.ConvertConstant(new ConstantExpression(value), type).StringRepresentation}"); 43 | currOffset += 8; 44 | } 45 | 46 | if (outputLines.Count > 0) 47 | { 48 | File.WriteAllLines(outputPath, outputLines); 49 | } 50 | } 51 | } -------------------------------------------------------------------------------- /Blitz3DDecomp/DecompilerSteps/Step2/VectorTypeDeduction.cs: -------------------------------------------------------------------------------- 1 | using B3DDecompUtils; 2 | using Blitz3DDecomp.LowLevel; 3 | 4 | namespace Blitz3DDecomp.DecompilerSteps.Step2; 5 | 6 | static class VectorTypeDeduction 7 | { 8 | private static void ProcessSection(Function function, AssemblySection section) 9 | { 10 | for (int i = 2; i < section.Instructions.Length - 1; i++) 11 | { 12 | var instruction = section.Instructions[i]; 13 | if (instruction.Name != "call" 14 | || !instruction.DestArg.Contains("_builtIn__bbVecAlloc", StringComparison.Ordinal)) 15 | { 16 | continue; 17 | } 18 | 19 | var vecTypeToRegister = section.Instructions[i - 2]; 20 | var vecType = DeclType.FromDesc(vecTypeToRegister.SrcArg1[1..]); 21 | 22 | var registerToArg = section.Instructions[i - 1]; 23 | 24 | var resultToVariable = section.Instructions[i + 1]; 25 | var variable = function.InstructionArgumentToVariable(resultToVariable.DestArg); 26 | 27 | if (vecTypeToRegister.Name == "mov" 28 | && vecType.IsArrayType 29 | && vecTypeToRegister.DestArg == registerToArg.SrcArg1 30 | && resultToVariable is { Name: "mov" } 31 | && resultToVariable.SrcArg1.StartsWith("eax", StringComparison.OrdinalIgnoreCase) 32 | && variable != null 33 | && variable.DeclType == DeclType.Unknown) 34 | { 35 | variable.Trace = variable.Trace.Append($"{function}: {variable.Name} is {vecType} because {vecTypeToRegister}"); 36 | variable.DeclType = vecType; 37 | } 38 | } 39 | } 40 | 41 | public static void Process(Function function) 42 | { 43 | foreach (var section in function.AssemblySections) 44 | { 45 | ProcessSection(function, section); 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /Blitz3DDecomp/DecompilerSteps/Step5/CleanupIfs.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using Blitz3DDecomp.HighLevel; 3 | using Blitz3DDecomp.HighLevel.ComparisonResults; 4 | using Blitz3DDecomp.HighLevel.Loops.DoWhile; 5 | 6 | namespace Blitz3DDecomp.DecompilerSteps.Step5; 7 | 8 | static class CleanupIfs 9 | { 10 | public static void Process(Function function) 11 | { 12 | var sectionsByName = function.HighLevelSectionsByName; 13 | for (int i = 0; i < function.HighLevelStatements.Count; i++) 14 | { 15 | if (function.HighLevelStatements[i] is not JumpIfExpressionStatement 16 | { 17 | PointsToUserSection: false, 18 | Condition: BooleanExpression condition 19 | } conditionalJumpStatement) 20 | { 21 | continue; 22 | } 23 | 24 | var jumpStatementSection = sectionsByName[conditionalJumpStatement.SectionName]; 25 | if (jumpStatementSection.StartIndex < i) { continue; } 26 | 27 | int indent = 0; 28 | for (int j = i; j < jumpStatementSection.StartIndex; j++) 29 | { 30 | indent -= function.HighLevelStatements[j].IndentationToSubtract; 31 | if (indent < 0) { break; } 32 | indent += function.HighLevelStatements[j].IndentationToAdd; 33 | } 34 | 35 | var endIfSection = jumpStatementSection; 36 | while (endIfSection is { IsEmpty: true, NextSection: { } nextSection }) 37 | { 38 | endIfSection = nextSection; 39 | } 40 | 41 | function.FindSectionForStatementIndex(i, out var jumpSection, out var indexInJumpSection); 42 | jumpSection.Statements.RemoveAt(indexInJumpSection); 43 | jumpSection.Statements.Insert(indexInJumpSection, new IfStatement(condition.Negated)); 44 | endIfSection.Statements.Insert(0, new EndIfStatement()); 45 | } 46 | } 47 | } -------------------------------------------------------------------------------- /Blitz3DDecomp/DecompilerSteps/Step3/CalleeReturnTypePropagation.cs: -------------------------------------------------------------------------------- 1 | using B3DDecompUtils; 2 | using Blitz3DDecomp.LowLevel; 3 | 4 | namespace Blitz3DDecomp.DecompilerSteps.Step3; 5 | 6 | static class CalleeReturnTypePropagation 7 | { 8 | private static bool ProcessSection(AssemblySection section) 9 | { 10 | bool somethingChanged = false; 11 | for (int i = 0; i < section.Instructions.Length - 1; i++) 12 | { 13 | var instruction = section.Instructions[i]; 14 | if (instruction.Name != "call") { continue; } 15 | 16 | var callee = Function.GetFunctionByName(instruction.DestArg); 17 | if (callee.IsBuiltIn && callee.ReturnType == DeclType.Unknown) { continue; } 18 | 19 | var variable = instruction.ReturnOutputVar; 20 | if (variable is null) { continue; } 21 | 22 | if (variable.DeclType == DeclType.Unknown && callee.CanReturnTypeBeSourceOfPropagation()) 23 | { 24 | variable.DeclType = callee.ReturnType; 25 | somethingChanged = true; 26 | variable.Trace = callee.Trace.Append($"{section.Owner}: {variable.Name} is {variable.DeclType} because {callee.Name}'s return type"); 27 | } 28 | else if (callee.ReturnType == DeclType.Unknown && variable.CanBeSourceOfPropagation()) 29 | { 30 | callee.ReturnType = variable.DeclType; 31 | somethingChanged = true; 32 | variable.Trace = callee.Trace.Append($"{section.Owner}: {callee.Name}'s return type is {callee.ReturnType} because {variable.Name}"); 33 | } 34 | } 35 | 36 | return somethingChanged; 37 | } 38 | 39 | public static bool Process(Function function) 40 | { 41 | bool somethingChanged = false; 42 | foreach (var section in function.AssemblySections) 43 | { 44 | somethingChanged |= ProcessSection(section); 45 | } 46 | return somethingChanged; 47 | } 48 | } -------------------------------------------------------------------------------- /Blitz3DDecomp/DecompilerSteps/Step5/CleanupWhile.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using Blitz3DDecomp.HighLevel; 3 | 4 | namespace Blitz3DDecomp.DecompilerSteps.Step5; 5 | 6 | static class CleanupWhile 7 | { 8 | public static void Process(Function function) 9 | { 10 | var sectionsByName = function.HighLevelSectionsByName; 11 | for (int i = 0; i < function.HighLevelStatements.Count; i++) 12 | { 13 | var statement = function.HighLevelStatements[i]; 14 | if (statement is not UnconditionalJumpStatement { PointsToUserSection: false } jumpStatement) { continue; } 15 | var jumpStatementSection = sectionsByName[jumpStatement.SectionName]; 16 | if (jumpStatementSection.StartIndex < i) { continue; } 17 | while (jumpStatementSection.Statements.Count == 0) 18 | { 19 | if (jumpStatementSection.NextSection is not { } nextSection) { break; } 20 | jumpStatementSection = nextSection; 21 | } 22 | if (jumpStatementSection.Statements.Count < 1) { continue; } 23 | 24 | if (jumpStatementSection.Statements[0] is not JumpIfExpressionStatement conditionalJumpStatement) { continue; } 25 | var conditionalJumpStatementSection = sectionsByName[conditionalJumpStatement.SectionName]; 26 | if (conditionalJumpStatementSection.StartIndex != i + 1) { continue; } 27 | 28 | int indent = 0; 29 | for (int j = i; j < jumpStatementSection.StartIndex; j++) 30 | { 31 | indent -= function.HighLevelStatements[j].IndentationToSubtract; 32 | if (indent < 0) { break; } 33 | indent += function.HighLevelStatements[j].IndentationToAdd; 34 | } 35 | if (indent != 0) { continue; } 36 | 37 | jumpStatementSection.Statements[0] = new WendStatement(); 38 | function.HighLevelStatements[i] = new WhileStatement(conditionalJumpStatement.Condition); 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /Blitz3DDisasm/BuiltInSymbolExtractor.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Immutable; 2 | using System.Text; 3 | using AsmResolver; 4 | using AsmResolver.PE.File; 5 | 6 | namespace Blitz3DDecomp; 7 | 8 | static class BuiltInSymbolExtractor 9 | { 10 | public static ImmutableArray FromFile(string exePath) 11 | { 12 | var peFile = PEFile.FromFile(exePath); 13 | 14 | var result = new List(); 15 | var stringBuilder = new StringBuilder(); 16 | foreach (var section in peFile.Sections) 17 | { 18 | if (section.Name is not (".data" or ".rdata")) { continue; } 19 | var data = (section.Contents as VirtualSegment)?.PhysicalContents?.WriteIntoArray(); 20 | if (data is null) 21 | { 22 | return ImmutableArray.Empty; 23 | } 24 | 25 | for (int i = 0; i < data.Length; i++) 26 | { 27 | byte asByte = data[i]; 28 | if (asByte == 0) 29 | { 30 | if (stringBuilder.Length >= 2) 31 | { 32 | var builtString = stringBuilder.ToString(); 33 | if (builtString.Contains('%') 34 | || builtString.Contains('$') 35 | || builtString.Contains('#') 36 | || builtString.Contains('*')) 37 | { 38 | result.Add(builtString); 39 | } 40 | } 41 | stringBuilder.Clear(); 42 | continue; 43 | } 44 | 45 | char asChar = (char)asByte; 46 | if ((char.IsLetterOrDigit(asChar) 47 | || asChar is ('_' or '%' or '$' or '#' or '*' or '=' or '.' or '"')) 48 | && asByte <= (byte)'z') 49 | { 50 | stringBuilder.Append(asChar); 51 | } 52 | else 53 | { 54 | stringBuilder.Clear(); 55 | } 56 | } 57 | } 58 | return result.ToImmutableArray(); 59 | } 60 | } -------------------------------------------------------------------------------- /Blitz3DDecomp/DecompilerSteps/Step5/CleanupRepeat.cs: -------------------------------------------------------------------------------- 1 | using Blitz3DDecomp.HighLevel; 2 | using Blitz3DDecomp.HighLevel.ComparisonResults; 3 | using Blitz3DDecomp.HighLevel.Loops.DoWhile; 4 | 5 | namespace Blitz3DDecomp.DecompilerSteps.Step5; 6 | 7 | static class CleanupRepeat 8 | { 9 | public static void Process(Function function) 10 | { 11 | for (int i = 0; i < function.HighLevelStatements.Count; i++) 12 | { 13 | if (function.HighLevelStatements[i] is not JumpStatement { PointsToUserSection: false } statement) { continue; } 14 | var (sectionName, replacementStatement) = statement switch 15 | { 16 | UnconditionalJumpStatement unconditionalJumpStatement 17 | => (unconditionalJumpStatement.SectionName, new ForeverStatement()), 18 | JumpIfExpressionStatement { Condition: BooleanExpression condition } jumpIfExpressionStatement 19 | => (jumpIfExpressionStatement.SectionName, new UntilStatement(condition.Negated)), 20 | _ 21 | => ("", (Statement?)null) 22 | }; 23 | if (string.IsNullOrEmpty(sectionName) || replacementStatement is null) { continue; } 24 | 25 | var jumpStatementSection = function.HighLevelSectionsByName[sectionName]; 26 | if (jumpStatementSection.StartIndex > i) { continue; } 27 | 28 | int indent = 0; 29 | for (int j = jumpStatementSection.StartIndex; j < i; j++) 30 | { 31 | indent -= function.HighLevelStatements[j].IndentationToSubtract; 32 | if (indent < 0) { break; } 33 | indent += function.HighLevelStatements[j].IndentationToAdd; 34 | } 35 | if (indent != 0) { continue; } 36 | 37 | function.HighLevelStatements[i] = replacementStatement; 38 | var repeatSection = jumpStatementSection; 39 | while (repeatSection is { IsEmpty: true, NextSection: { } nextSection }) 40 | { 41 | repeatSection = nextSection; 42 | } 43 | repeatSection.Statements.Insert(0, new RepeatStatement()); 44 | i++; 45 | } 46 | } 47 | } -------------------------------------------------------------------------------- /Blitz3DDecomp.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.5.33516.290 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Blitz3DDisasm", "Blitz3DDisasm\Blitz3DDisasm.csproj", "{B5D64B9E-4EB0-4C6F-8309-8B02A30909FA}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Blitz3DDecomp", "Blitz3DDecomp\Blitz3DDecomp.csproj", "{24B54F7F-F4DF-4472-B17E-D181CA39A4A4}" 9 | EndProject 10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "B3DDecompUtils", "B3DDecompUtils\B3DDecompUtils.csproj", "{CB04A3B4-109C-43AD-9F51-BE9F1217D025}" 11 | EndProject 12 | Global 13 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 14 | Debug|Any CPU = Debug|Any CPU 15 | Release|Any CPU = Release|Any CPU 16 | EndGlobalSection 17 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 18 | {B5D64B9E-4EB0-4C6F-8309-8B02A30909FA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 19 | {B5D64B9E-4EB0-4C6F-8309-8B02A30909FA}.Debug|Any CPU.Build.0 = Debug|Any CPU 20 | {B5D64B9E-4EB0-4C6F-8309-8B02A30909FA}.Release|Any CPU.ActiveCfg = Release|Any CPU 21 | {B5D64B9E-4EB0-4C6F-8309-8B02A30909FA}.Release|Any CPU.Build.0 = Release|Any CPU 22 | {24B54F7F-F4DF-4472-B17E-D181CA39A4A4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 23 | {24B54F7F-F4DF-4472-B17E-D181CA39A4A4}.Debug|Any CPU.Build.0 = Debug|Any CPU 24 | {24B54F7F-F4DF-4472-B17E-D181CA39A4A4}.Release|Any CPU.ActiveCfg = Release|Any CPU 25 | {24B54F7F-F4DF-4472-B17E-D181CA39A4A4}.Release|Any CPU.Build.0 = Release|Any CPU 26 | {CB04A3B4-109C-43AD-9F51-BE9F1217D025}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 27 | {CB04A3B4-109C-43AD-9F51-BE9F1217D025}.Debug|Any CPU.Build.0 = Debug|Any CPU 28 | {CB04A3B4-109C-43AD-9F51-BE9F1217D025}.Release|Any CPU.ActiveCfg = Release|Any CPU 29 | {CB04A3B4-109C-43AD-9F51-BE9F1217D025}.Release|Any CPU.Build.0 = Release|Any CPU 30 | EndGlobalSection 31 | GlobalSection(SolutionProperties) = preSolution 32 | HideSolutionNode = FALSE 33 | EndGlobalSection 34 | GlobalSection(ExtensibilityGlobals) = postSolution 35 | SolutionGuid = {C760B230-7ECA-4653-96B1-3DFA6B42102B} 36 | EndGlobalSection 37 | EndGlobal 38 | -------------------------------------------------------------------------------- /Blitz3DDecomp/Basics/DeclType.cs: -------------------------------------------------------------------------------- 1 | using System.Text.RegularExpressions; 2 | 3 | namespace Blitz3DDecomp; 4 | 5 | readonly record struct DeclType(string Suffix) 6 | { 7 | public static readonly DeclType Unknown = new DeclType("????"); 8 | public static readonly DeclType Int = new DeclType("%"); 9 | public static readonly DeclType Float = new DeclType("#"); 10 | public static readonly DeclType String = new DeclType("$"); 11 | public static readonly DeclType Pointer = new DeclType("*"); 12 | 13 | public static DeclType FromDesc(string descStr) 14 | { 15 | if (descStr.StartsWith("Vector_")) 16 | { 17 | return MakeVector(descStr); 18 | } 19 | 20 | if (descStr == "_builtIn__bbStrType") { return String; } 21 | if (descStr == "_builtIn__bbIntType") { return Int; } 22 | if (descStr == "_builtIn__bbFltType") { return Float; } 23 | 24 | if (descStr.StartsWith("_t")) 25 | { 26 | return new DeclType($".{descStr[2..]}"); 27 | } 28 | return Unknown; 29 | } 30 | 31 | public static DeclType MakeVector(string descStr) 32 | { 33 | var descRegex = new Regex("Vector_[0-9]+(_.+)_sz([0-9]+)"); 34 | var match = descRegex.Match(descStr); 35 | var baseDesc = FromDesc(match.Groups[1].Value); 36 | 37 | var sz = int.Parse(match.Groups[2].Value); 38 | 39 | // BlitzBasic is dumb so the following: 40 | // Local myVar$[10] 41 | // is actually an 11 element array, 42 | // so we need to decrement the size here :) 43 | return new DeclType($"{baseDesc.Suffix}[{sz - 1}]"); 44 | } 45 | 46 | public bool IsCustomType 47 | => Suffix[0] == '.'; 48 | 49 | public bool IsArrayType 50 | => Suffix[^1] == ']'; 51 | 52 | public DeclType? GetElementType() 53 | { 54 | if (!IsArrayType) { return null; } 55 | 56 | for (int i = Suffix.Length - 1; i >= 0; i--) 57 | { 58 | if (Suffix[i] == '[') 59 | { 60 | return new DeclType(Suffix[..i]); 61 | } 62 | } 63 | 64 | return null; 65 | } 66 | 67 | public override string ToString() 68 | => $"DeclType({Suffix})"; 69 | } -------------------------------------------------------------------------------- /Blitz3DDecomp/DecompilerSteps/Step0/IngestDecls.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | 3 | namespace Blitz3DDecomp; 4 | 5 | static class IngestDecls 6 | { 7 | public static void FromFiles(string[] declsFiles) 8 | { 9 | foreach (var declsFile in declsFiles) 10 | { 11 | var lines = File.ReadAllLines(declsFile); 12 | foreach (var line in lines) 13 | { 14 | var signatureString = line; 15 | if (line.IndexOf(":", StringComparison.Ordinal) is (var colonIndex and > 0)) 16 | { 17 | signatureString = line[..colonIndex]; 18 | } 19 | 20 | signatureString = new string(signatureString.SelectMany(chr => char.IsWhiteSpace(chr) ? [] : new[]{chr}).ToArray()); 21 | 22 | if (signatureString.IndexOf("(", StringComparison.Ordinal) is not (var parenIndex and > 0)) { continue; } 23 | 24 | string flipTypeAnnotationLocation(string str, string defaultType) 25 | { 26 | if (string.IsNullOrWhiteSpace(str)) { return ""; } 27 | if (str[^1] is '%' or '#' or '$' or '*') 28 | { 29 | return str[^1] + str[..^1]; 30 | } 31 | return defaultType + str; 32 | } 33 | 34 | string[] parameters = signatureString[(parenIndex+1)..].Replace(")", "") 35 | .Split(",") 36 | .Select(parameter => flipTypeAnnotationLocation(parameter, "%")) 37 | .ToArray(); 38 | string nameString = flipTypeAnnotationLocation(signatureString[..parenIndex], ""); 39 | 40 | var symbolOption = BlitzSymbol.FromString(nameString + string.Join("", parameters)); 41 | if (!symbolOption.TryUnwrap(out var symbol)) { continue; } 42 | 43 | Console.WriteLine(nameString + string.Join("", parameters)); 44 | 45 | var newFunction = new Function($"{symbol.FunctionName}__LIBS", 0) { ReturnType = symbol.ReturnType }; 46 | newFunction.Parameters.Clear(); 47 | newFunction.Parameters.AddRange(symbol.Parameters.Select((p, i) => new Function.Parameter(newFunction, p.Name, i) { DeclType = p.DeclType })); 48 | } 49 | } 50 | } 51 | } -------------------------------------------------------------------------------- /Blitz3DDecomp/Basics/Variables/Variable.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Immutable; 2 | using B3DDecompUtils; 3 | 4 | namespace Blitz3DDecomp; 5 | 6 | abstract class Variable 7 | { 8 | public readonly string Name; 9 | 10 | public abstract DeclType DeclType { get; set; } 11 | 12 | public DebugTrace Trace = default; 13 | 14 | public ArrayElementVariable? GetArrayElement(string index) 15 | => DeclType.GetElementType() is not null 16 | ? new ArrayElementVariable(this, index) 17 | : null; 18 | 19 | protected FieldVariable[]? fields = null; 20 | public IReadOnlyList Fields 21 | { 22 | get 23 | { 24 | if (fields is not { Length: > 0 } && DeclType is { IsArrayType: false, IsCustomType: true }) 25 | { 26 | var customType = CustomType.GetTypeMatchingDeclType(DeclType); 27 | fields = customType.Fields.Select(f => new FieldVariable(this, f)).ToArray(); 28 | } 29 | 30 | return fields ?? Array.Empty(); 31 | } 32 | } 33 | 34 | public bool CanBeSourceOfPropagation() 35 | { 36 | if (DeclType == DeclType.Unknown) { return false; } 37 | 38 | if (this is Function.Parameter { Owner: var owner } 39 | && DeclType == DeclType.Pointer 40 | && owner.Name.EndsWith("__LIBS", StringComparison.Ordinal)) 41 | { 42 | return false; 43 | } 44 | 45 | return true; 46 | } 47 | 48 | protected Variable(string name) 49 | { 50 | Name = name; 51 | } 52 | 53 | public abstract string ToInstructionArg(); 54 | 55 | public override string ToString() 56 | => $"{Name}{DeclType.Suffix}"; 57 | 58 | public override bool Equals(object? obj) 59 | { 60 | return obj is Variable otherVariable && otherVariable == this; 61 | } 62 | 63 | public override int GetHashCode() 64 | { 65 | return HashCode.Combine(Name, DeclType); 66 | } 67 | 68 | public static bool operator ==(Variable? a, Variable? b) 69 | { 70 | if (a is null) { return b is null; } 71 | if (b is null) { return false; } 72 | return a.Name == b.Name && a.DeclType == b.DeclType; 73 | } 74 | 75 | public static bool operator !=(Variable? a, Variable? b) 76 | { 77 | return !(a == b); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /Blitz3DDecomp/DecompilerSteps/Step5/RemoveTrivialNoops.cs: -------------------------------------------------------------------------------- 1 | using Blitz3DDecomp.HighLevel; 2 | 3 | namespace Blitz3DDecomp.DecompilerSteps.Step5; 4 | 5 | static class RemoveTrivialNoops 6 | { 7 | public static void Process(Function function) 8 | { 9 | Expression expressionMapper(Expression expression) 10 | { 11 | Expression? lhs = null; 12 | Expression? rhs = null; 13 | switch (expression) 14 | { 15 | case AndExpression { Lhs: var tempLhs, Rhs: var tempRhs }: 16 | lhs = tempLhs; 17 | rhs = tempRhs; 18 | break; 19 | case LogicalAndExpression { Lhs: var tempLhs, Rhs: var tempRhs }: 20 | lhs = tempLhs; 21 | rhs = tempRhs; 22 | break; 23 | case OrExpression { Lhs: var tempLhs, Rhs: var tempRhs }: 24 | lhs = tempLhs; 25 | rhs = tempRhs; 26 | break; 27 | case LogicalOrExpression { Lhs: var tempLhs, Rhs: var tempRhs }: 28 | lhs = tempLhs; 29 | rhs = tempRhs; 30 | break; 31 | case XorExpression { Lhs: VariableExpression { Variable: Function.DecompGeneratedTempVariable } tempLhs, Rhs: var tempRhs } when tempLhs == tempRhs: 32 | return new ConstantExpression(Value: "0x0"); 33 | } 34 | 35 | if (lhs is VariableExpression { Variable: Function.DecompGeneratedTempVariable } 36 | && lhs == rhs) 37 | { 38 | return lhs; 39 | } 40 | 41 | return expression; 42 | } 43 | 44 | for (int i = 0; i < function.HighLevelStatements.Count; i++) 45 | { 46 | function.HighLevelStatements[i] = function.HighLevelStatements[i] 47 | .Map(statement => statement, expressionMapper); 48 | 49 | if (function.HighLevelStatements[i] is AssignmentStatement 50 | { 51 | Destination: VariableExpression { Variable: Function.DecompGeneratedTempVariable lhs }, 52 | Source: VariableExpression { Variable: var rhs } 53 | } 54 | && lhs == rhs) 55 | { 56 | function.FindSectionForStatementIndex(i, out var section, out var indexInSection); 57 | section.Statements.RemoveAt(indexInSection); 58 | } 59 | } 60 | } 61 | } -------------------------------------------------------------------------------- /Blitz3DDecomp/DecompilerSteps/Step3/CalleeArgumentTypePropagation.cs: -------------------------------------------------------------------------------- 1 | using B3DDecompUtils; 2 | using Blitz3DDecomp.LowLevel; 3 | 4 | namespace Blitz3DDecomp.DecompilerSteps.Step3; 5 | 6 | static class CalleeArgumentTypePropagation 7 | { 8 | private static bool ProcessSection(AssemblySection section) 9 | { 10 | bool somethingChanged = false; 11 | foreach (var instruction in section.Instructions) 12 | { 13 | if (instruction.Name != "call") { continue; } 14 | if (instruction.CallParameterAssignmentIndices is not { } assignmentIndices) { continue; } 15 | 16 | var callee = Function.GetFunctionByName(instruction.DestArg); 17 | 18 | for (int i = 0; i < assignmentIndices.Length; i++) 19 | { 20 | var assignmentInstruction = section.Owner.Instructions[assignmentIndices[i]]; 21 | if (section.Owner.InstructionArgumentToVariable(assignmentInstruction.SrcArg1) is { } variable) 22 | { 23 | if (variable.DeclType != DeclType.Unknown) 24 | { 25 | if (!variable.CanBeSourceOfPropagation()) { continue; } 26 | if (callee.IsBuiltIn) { continue; } 27 | if (callee.Parameters[i].DeclType != DeclType.Unknown) { continue; } 28 | 29 | callee.Parameters[i].DeclType = variable.DeclType; 30 | somethingChanged = true; 31 | callee.Parameters[i].Trace = variable.Trace.Append($"{section.Owner}: callee {callee.Name} arg {i} is {callee.Parameters[i].DeclType} because {variable}"); 32 | } 33 | else 34 | { 35 | if (!callee.Parameters[i].CanBeSourceOfPropagation()) { continue; } 36 | 37 | variable.DeclType = callee.Parameters[i].DeclType; 38 | somethingChanged = true; 39 | variable.Trace = callee.Parameters[i].Trace.Append($"{section.Owner}: {variable.Name} is {variable.DeclType} because {callee.Name} arg {i}"); 40 | } 41 | } 42 | } 43 | } 44 | return somethingChanged; 45 | } 46 | 47 | public static bool Process(Function function) 48 | { 49 | bool somethingChanged = false; 50 | foreach (var section in function.AssemblySections) 51 | { 52 | somethingChanged |= ProcessSection(section); 53 | } 54 | return somethingChanged; 55 | } 56 | } -------------------------------------------------------------------------------- /Blitz3DDisasm/Symbol.cs: -------------------------------------------------------------------------------- 1 | namespace Blitz3DDecomp; 2 | 3 | public enum SymbolType 4 | { 5 | Other = 0, 6 | Code, 7 | Variable, 8 | Type, 9 | DimArray, 10 | Vector, 11 | Data, 12 | Libs, 13 | BuiltIn 14 | } 15 | 16 | sealed class Symbol 17 | { 18 | public int Address; 19 | public string Name; 20 | public string? NewName; 21 | 22 | public string NameToPrint => NewName ?? Name; 23 | 24 | public string? OwnerName { get; private set; } 25 | 26 | private SymbolType inferredType; 27 | 28 | public Symbol(string name) 29 | { 30 | Name = name; 31 | } 32 | 33 | public void TrySetInferredType(SymbolType type, string? ownerName) 34 | { 35 | if (Type != SymbolType.Other) { return; } 36 | ForceSetInferredType(type, ownerName); 37 | } 38 | 39 | public void ForceSetInferredType(SymbolType type, string? ownerName) 40 | { 41 | inferredType = type; 42 | OwnerName = ownerName; 43 | } 44 | 45 | public SymbolType Type 46 | { 47 | get 48 | { 49 | if (inferredType != SymbolType.Other) 50 | { 51 | return inferredType; 52 | } 53 | if (Name == "__DATA") 54 | { 55 | OwnerName = Name; 56 | inferredType = SymbolType.Data; 57 | return SymbolType.Data; 58 | } 59 | if (Name == "__LIBS") 60 | { 61 | OwnerName = Name; 62 | inferredType = SymbolType.Libs; 63 | return SymbolType.Libs; 64 | } 65 | if (Name.StartsWith("_v")) 66 | { 67 | OwnerName = Name; 68 | inferredType = SymbolType.Variable; 69 | return SymbolType.Variable; 70 | } 71 | if (Name.StartsWith("_t")) 72 | { 73 | OwnerName = Name; 74 | inferredType = SymbolType.Type; 75 | return SymbolType.Type; 76 | } 77 | if (Name.StartsWith("_a")) 78 | { 79 | OwnerName = Name; 80 | inferredType = SymbolType.DimArray; 81 | return SymbolType.DimArray; 82 | } 83 | if (Name == "__MAIN" 84 | || Name.StartsWith("_f")) 85 | { 86 | OwnerName = Name; 87 | inferredType = SymbolType.Code; 88 | return SymbolType.Code; 89 | } 90 | return SymbolType.Other; 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /Blitz3DDecomp/DecompilerSteps/Step3/SelfReturnTypePropagation.cs: -------------------------------------------------------------------------------- 1 | using B3DDecompUtils; 2 | using B3DDecompUtils.Primitives; 3 | using Blitz3DDecomp.LowLevel; 4 | 5 | namespace Blitz3DDecomp.DecompilerSteps.Step3; 6 | 7 | static class SelfReturnTypePropagation 8 | { 9 | private static void ProcessSection(AssemblySection section) 10 | { 11 | for (int i = 1; i < section.Instructions.Length; i++) 12 | { 13 | var instruction = section.Instructions[i]; 14 | 15 | if (instruction.Name != "jmp") { continue; } 16 | if (!instruction.DestArg.Contains($"_leave{section.Owner.CoreSymbolName}", StringComparison.OrdinalIgnoreCase)) { continue; } 17 | 18 | for (int j = i - 1; j >= 0; j--) 19 | { 20 | var prevInstruction = section.Instructions[j]; 21 | if (prevInstruction.IsJumpOrCall) 22 | { 23 | if (prevInstruction.Name != "call") { return; } 24 | 25 | var callee = Function.GetFunctionByName(prevInstruction.DestArg); 26 | if (!callee.CanReturnTypeBeSourceOfPropagation()) { return; } 27 | 28 | section.Owner.ReturnType = callee.ReturnType; 29 | section.Owner.Trace = section.Owner.Trace.Append($"{section.Owner}: returns {section.Owner.ReturnType} because {prevInstruction}"); 30 | return; 31 | } 32 | 33 | var argsToCheck = new[] { prevInstruction.DestArg, prevInstruction.SrcArg1, prevInstruction.SrcArg2 }; 34 | var matchingArgOption = argsToCheck.FirstOrNone(a => a.StripDeref().StartsWith("eax", StringComparison.OrdinalIgnoreCase)); 35 | if (!matchingArgOption.TryUnwrap(out var matchingArg)) { continue; } 36 | 37 | var variable = section.Owner.InstructionArgumentToVariable(matchingArg); 38 | if (variable is null || !variable.CanBeSourceOfPropagation()) { break; } 39 | 40 | section.Owner.ReturnType = variable.DeclType; 41 | section.Owner.Trace = section.Owner.Trace.Append($"{section.Owner}: returns {section.Owner.ReturnType} because {prevInstruction}"); 42 | return; 43 | } 44 | } 45 | } 46 | 47 | public static bool Process(Function function) 48 | { 49 | if (function.ReturnType != DeclType.Unknown) { return false; } 50 | foreach (var section in function.AssemblySections) 51 | { 52 | ProcessSection(section); 53 | if (function.ReturnType != DeclType.Unknown) { return true; } 54 | } 55 | return false; 56 | } 57 | } -------------------------------------------------------------------------------- /Blitz3DDecomp/Basics/Function/BuiltIns/BlitzSymbol.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Immutable; 2 | using System.Diagnostics; 3 | using B3DDecompUtils.Primitives; 4 | 5 | namespace Blitz3DDecomp; 6 | 7 | readonly record struct BlitzSymbol( 8 | string FunctionName, 9 | ImmutableArray Parameters, 10 | DeclType ReturnType) 11 | { 12 | public readonly record struct Parameter(string Name, DeclType DeclType); 13 | 14 | public static Option FromString(string str) 15 | { 16 | static DeclType ripTypeFromStr(ref string str, DeclType defaultType) 17 | { 18 | str = str.Replace(" ", ""); 19 | if (str[0] == '#') 20 | { 21 | str = str[1..]; 22 | return DeclType.Float; 23 | } 24 | if (str[0] == '%') 25 | { 26 | str = str[1..]; 27 | return DeclType.Int; 28 | } 29 | if (str[0] == '$') 30 | { 31 | str = str[1..]; 32 | return DeclType.String; 33 | } 34 | if (str[0] == '*') 35 | { 36 | str = str[1..]; 37 | return DeclType.Pointer; 38 | } 39 | return defaultType; 40 | } 41 | 42 | DeclType returnType = ripTypeFromStr(ref str, DeclType.Unknown); 43 | 44 | str = str 45 | .Replace("%", " %") 46 | .Replace("#", " #") 47 | .Replace("$", " $") 48 | .Replace("*", " *"); 49 | var split = str.Split(" "); 50 | var parameters = new List(); 51 | var funcName = split[0].ToLowerInvariant(); 52 | split = split.Skip(1).ToArray(); 53 | for (var argIndex = 0; argIndex < split.Length; argIndex++) 54 | { 55 | var argName = split[argIndex]; 56 | var argType = ripTypeFromStr(ref argName, DeclType.Int); 57 | parameters.Add(new Parameter(argName, argType)); 58 | } 59 | 60 | if (funcName.Length == 0 || !char.IsLetter(funcName[0])) { return Option.None; } 61 | 62 | return Option.Some(new BlitzSymbol( 63 | FunctionName: funcName, 64 | Parameters: parameters.ToImmutableArray(), 65 | ReturnType: returnType)); 66 | } 67 | 68 | public static readonly Dictionary SymbolsDeclaredByExecutable = new Dictionary(); 69 | 70 | public static void Init(string symbolsPath) 71 | { 72 | if (!File.Exists(symbolsPath)) { return; } 73 | 74 | var symbolStrings = File.ReadAllLines(symbolsPath); 75 | foreach (var s in symbolStrings) 76 | { 77 | if (FromString(s).TryUnwrap(out var symbol)) 78 | { 79 | SymbolsDeclaredByExecutable[symbol.FunctionName] = symbol; 80 | } 81 | } 82 | } 83 | } -------------------------------------------------------------------------------- /Blitz3DDecomp/DecompilerSteps/Step3/BbArrayAccessRewrite.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using B3DDecompUtils; 3 | using Blitz3DDecomp.LowLevel; 4 | 5 | namespace Blitz3DDecomp.DecompilerSteps.Step3; 6 | 7 | static class BbArrayAccessRewrite 8 | { 9 | private static bool ProcessSection(AssemblySection section) 10 | { 11 | bool somethingChanged = false; 12 | for (int i = 0; i < section.Instructions.Length; i++) 13 | { 14 | var instruction = section.Instructions[i]; 15 | if (instruction.Name != "add" 16 | || !instruction.DestArg[..3].IsRegister()) 17 | { 18 | continue; 19 | } 20 | 21 | var srcVar1 = section.Owner.InstructionArgumentToVariable(instruction.SrcArg1); 22 | var srcVar2 = section.Owner.InstructionArgumentToVariable(instruction.SrcArg2); 23 | 24 | var array = srcVar2 is { DeclType.IsArrayType : true } 25 | ? srcVar2 26 | : srcVar1 is { DeclType.IsArrayType : true } 27 | ? srcVar1 28 | : null; 29 | if (array is null) { continue; } 30 | 31 | string arrayIndex = ""; 32 | void tryExtractArrayIndex(string instructionArg, Variable? variable) 33 | { 34 | if (!string.IsNullOrEmpty(arrayIndex)) { return; } 35 | 36 | if (variable?.DeclType == DeclType.Int || variable?.DeclType == DeclType.Unknown) 37 | { 38 | arrayIndex = $"{variable.Name}>>2"; 39 | if (variable.DeclType == DeclType.Unknown) 40 | { 41 | variable.DeclType = DeclType.Int; 42 | variable.Trace = variable.Trace.Append($"{section.Owner}: {variable.Name} is {DeclType.Int} because {instruction}"); 43 | } 44 | } 45 | else if (instructionArg.TryHexToUint32(out var constantIndex)) 46 | { 47 | arrayIndex = "0x" + (constantIndex >> 2).ToString("X1"); 48 | } 49 | } 50 | tryExtractArrayIndex(instruction.SrcArg1, srcVar1); 51 | tryExtractArrayIndex(instruction.SrcArg2, srcVar2); 52 | 53 | if (string.IsNullOrEmpty(arrayIndex)) { continue; } 54 | 55 | instruction.Name = "mov"; 56 | instruction.SrcArg1 = $"{array.Name}[{arrayIndex}]"; 57 | instruction.SrcArg2 = ""; 58 | Logger.WriteLine($"{section.Owner}: {array.Name} access at {section.Name}:{i}"); 59 | somethingChanged = true; 60 | } 61 | return somethingChanged; 62 | } 63 | 64 | public static bool Process(Function function) 65 | { 66 | bool somethingChanged = false; 67 | foreach (var section in function.AssemblySections) 68 | { 69 | somethingChanged |= ProcessSection(section); 70 | } 71 | return somethingChanged; 72 | } 73 | } -------------------------------------------------------------------------------- /Blitz3DDecomp/DecompilerSteps/Step3/BbCustomTypeFieldAccessRewrite.cs: -------------------------------------------------------------------------------- 1 | using B3DDecompUtils; 2 | using Blitz3DDecomp.LowLevel; 3 | 4 | namespace Blitz3DDecomp.DecompilerSteps.Step3; 5 | 6 | static class BbCustomTypeFieldAccessRewrite 7 | { 8 | private static bool TryRewrite(Variable? ownerVariable, Variable? outputVar, Instruction instruction, uint fieldIndex) 9 | { 10 | if (ownerVariable is null) { return false; } 11 | if (!ownerVariable.DeclType.IsCustomType) { return false; } 12 | if (ownerVariable.DeclType.IsArrayType) { return false; } 13 | if (outputVar is null) { return false; } 14 | 15 | var customType = CustomType.GetTypeMatchingDeclType(ownerVariable.DeclType); 16 | 17 | instruction.SrcArg1 = $"{ownerVariable.Name}\\{customType.Fields[(int)fieldIndex].Name}"; 18 | instruction.Name = "mov"; 19 | instruction.DestArg = outputVar.Name; 20 | instruction.SrcArg2 = ""; 21 | 22 | return true; 23 | } 24 | 25 | private static bool HandleMavless(AssemblySection section, Instruction instruction) 26 | { 27 | if (instruction.Name != "call") { return false; } 28 | if (instruction.DestArg != "@_builtIn__bbFieldPtrAdd") { return false; } 29 | 30 | if (instruction.CallParameterAssignmentIndices is not { Length: 2 } assignmentIndices) { return false; } 31 | if (instruction.ReturnOutputVar is not { } outputVar) { return false; } 32 | 33 | var ownerVariable = section.Owner.InstructionArgumentToVariable(section.Owner.Instructions[assignmentIndices[0]].SrcArg1); 34 | uint fieldIndex = section.Owner.Instructions[assignmentIndices[1]].SrcArg1.HexToUint32() >> 2; 35 | 36 | return TryRewrite(ownerVariable, outputVar, instruction, fieldIndex); 37 | } 38 | 39 | private static bool HandleVanilla(AssemblySection section, Instruction instruction) 40 | { 41 | if (instruction.Name != "add") { return false; } 42 | 43 | var ownerVariable = section.Owner.InstructionArgumentToVariable(instruction.SrcArg1); 44 | var outputVar = section.Owner.InstructionArgumentToVariable(instruction.DestArg); 45 | 46 | if (!instruction.SrcArg2.TryHexToUint32(out var fieldIndex)) { return false; } 47 | fieldIndex >>= 2; 48 | 49 | return TryRewrite(ownerVariable, outputVar, instruction, fieldIndex); 50 | } 51 | 52 | private static bool ProcessSection(AssemblySection section) 53 | { 54 | bool somethingChanged = false; 55 | foreach (var instruction in section.Instructions) 56 | { 57 | somethingChanged |= HandleMavless(section, instruction); 58 | somethingChanged |= HandleVanilla(section, instruction); 59 | } 60 | return somethingChanged; 61 | } 62 | 63 | public static bool Process(Function function) 64 | { 65 | bool somethingChanged = false; 66 | foreach (var section in function.AssemblySections) 67 | { 68 | somethingChanged |= ProcessSection(section); 69 | } 70 | return somethingChanged; 71 | } 72 | } -------------------------------------------------------------------------------- /Blitz3DDecomp/DecompilerSteps/Step5/CleanupElse.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using Blitz3DDecomp.HighLevel; 3 | using Blitz3DDecomp.HighLevel.ComparisonResults; 4 | 5 | namespace Blitz3DDecomp.DecompilerSteps.Step5; 6 | 7 | static class CleanupElse 8 | { 9 | public static void Process(Function function) 10 | { 11 | var sectionsByName = function.HighLevelSectionsByName; 12 | for (int i = 0; i < function.HighLevelStatements.Count - 1; i++) 13 | { 14 | var statement = function.HighLevelStatements[i]; 15 | var nextStatement = function.HighLevelStatements[i + 1]; 16 | if (statement is not UnconditionalJumpStatement { PointsToUserSection: false } jumpStatement 17 | || nextStatement is not EndIfStatement) { continue; } 18 | 19 | var jumpStatementSection = sectionsByName[jumpStatement.SectionName]; 20 | if (jumpStatementSection.StartIndex < i) { continue; } 21 | 22 | int indent = 0; 23 | for (int j = i + 1; j < jumpStatementSection.StartIndex; j++) 24 | { 25 | indent -= function.HighLevelStatements[j].IndentationToSubtract; 26 | if (indent < -1) { break; } 27 | indent += function.HighLevelStatements[j].IndentationToAdd; 28 | } 29 | if (indent < -1) { continue; } 30 | 31 | var endIfSection = jumpStatementSection; 32 | while (endIfSection is { IsEmpty: true, NextSection: { } nextSection }) 33 | { 34 | endIfSection = nextSection; 35 | } 36 | 37 | if (indent > -1) 38 | { 39 | bool isValidElse = true; 40 | for (int j = endIfSection.StartIndex; j < function.HighLevelStatements.Count; j++) 41 | { 42 | if (function.HighLevelStatements[j] is not EndIfStatement) 43 | { 44 | isValidElse = false; 45 | break; 46 | } 47 | indent -= function.HighLevelStatements[j].IndentationToSubtract; 48 | if (indent <= -1) { break; } 49 | indent += function.HighLevelStatements[j].IndentationToAdd; 50 | } 51 | if (!isValidElse || indent != -1) { continue; } 52 | } 53 | endIfSection.Statements.Insert(0, new EndIfStatement()); 54 | if (i > 0 && function.HighLevelStatements[i - 1] is IfStatement { Condition: BooleanExpression ifCondition }) 55 | { 56 | function.HighLevelStatements[i - 1] = new IfStatement(ifCondition.Negated); 57 | function.FindSectionForStatementIndex(i + 1, out var nextStatementSection, out var indexInNextSection); 58 | nextStatementSection.Statements.RemoveAt(indexInNextSection); 59 | } 60 | else 61 | { 62 | function.HighLevelStatements[i + 1] = new ElseStatement(); 63 | } 64 | 65 | function.FindSectionForStatementIndex(i, out var section, out int indexInSection); 66 | section.Statements.RemoveAt(indexInSection); 67 | } 68 | } 69 | } -------------------------------------------------------------------------------- /Blitz3DDecomp/DecompilerSteps/Step5/CleanupBooleanExpressions.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | using Blitz3DDecomp.HighLevel; 3 | using Blitz3DDecomp.HighLevel.ComparisonResults; 4 | 5 | namespace Blitz3DDecomp.DecompilerSteps.Step5; 6 | 7 | static class CleanupBooleanExpressions 8 | { 9 | private static Expression MapExpression(Expression expression) 10 | { 11 | static bool isSubtract( 12 | Expression compareExpr, 13 | [NotNullWhen(returnValue: true)] out SubtractExpression? subtractExpression) 14 | { 15 | if (compareExpr is SubtractExpression subtractExpr) 16 | { 17 | subtractExpression = subtractExpr; 18 | return true; 19 | } 20 | 21 | subtractExpression = null; 22 | return false; 23 | } 24 | 25 | SubtractExpression? innerSubtractExpression; 26 | switch (expression) 27 | { 28 | case OneIfZeroExpression compareExpr when isSubtract(compareExpr.OriginalExpression, out innerSubtractExpression): 29 | return new OneIfExpressionsEqualExpression( 30 | innerSubtractExpression.Lhs, 31 | innerSubtractExpression.Rhs); 32 | case OneIfNotZeroExpression compareExpr when isSubtract(compareExpr.OriginalExpression, out innerSubtractExpression): 33 | return new OneIfExpressionsNotEqualExpression( 34 | innerSubtractExpression.Lhs, 35 | innerSubtractExpression.Rhs); 36 | case OneIfLessThanZeroExpression compareExpr when isSubtract(compareExpr.OriginalExpression, out innerSubtractExpression): 37 | return new OneIfExpressionIsLessThanOtherExpression( 38 | innerSubtractExpression.Lhs, 39 | innerSubtractExpression.Rhs); 40 | case OneIfLessThanOrEqualToZeroExpression compareExpr when isSubtract(compareExpr.OriginalExpression, out innerSubtractExpression): 41 | return new OneIfExpressionIsLessThanOrEqualToOtherExpression( 42 | innerSubtractExpression.Lhs, 43 | innerSubtractExpression.Rhs); 44 | case OneIfGreaterThanZeroExpression compareExpr when isSubtract(compareExpr.OriginalExpression, out innerSubtractExpression): 45 | return new OneIfExpressionIsGreaterThanOtherExpression( 46 | innerSubtractExpression.Lhs, 47 | innerSubtractExpression.Rhs); 48 | case OneIfGreaterThanOrEqualToZeroExpression compareExpr when isSubtract(compareExpr.OriginalExpression, out innerSubtractExpression): 49 | return new OneIfExpressionIsGreaterThanOrEqualToOtherExpression( 50 | innerSubtractExpression.Lhs, 51 | innerSubtractExpression.Rhs); 52 | case OneIfZeroExpression { OriginalExpression: BooleanExpression innerBooleanExpression }: 53 | return innerBooleanExpression.Negated; 54 | case OneIfNotZeroExpression { OriginalExpression: BooleanExpression innerBooleanExpression }: 55 | return innerBooleanExpression; 56 | } 57 | 58 | return expression; 59 | } 60 | 61 | public static void Process(Function function) 62 | { 63 | for (int i = 0; i < function.HighLevelStatements.Count; i++) 64 | { 65 | function.HighLevelStatements[i] = function.HighLevelStatements[i] 66 | .Map(stmt => stmt, MapExpression); 67 | } 68 | } 69 | } -------------------------------------------------------------------------------- /Blitz3DDecomp/DecompilerSteps/Step4/GuessFloatsFromConstants.cs: -------------------------------------------------------------------------------- 1 | using B3DDecompUtils; 2 | using Blitz3DDecomp.LowLevel; 3 | 4 | namespace Blitz3DDecomp.DecompilerSteps.Step4; 5 | 6 | static class GuessFloatsFromConstants 7 | { 8 | private static void GuessForVariable(Function function, Variable variable, uint constant, string reason) 9 | { 10 | if (variable.DeclType != DeclType.Unknown) { return; } 11 | if (constant == 0) 12 | { 13 | // Int 0 and float 0 have the same representation 14 | // so we can't disambiguate here 15 | return; 16 | } 17 | 18 | // If the exponent of the float is all zeroes or mostly ones, 19 | // or if the mantissa is a small non-zero number, 20 | // this is probably an int because writing that kind of float in source code is hard 21 | var potentialExponent = (constant & 0x7f80_0000) >> 23; 22 | var potentialMantissa = constant & 0x007f_ffff; 23 | var guessedType = DeclType.Float; 24 | if (potentialExponent is < 96 or > 190) { return; } 25 | if (potentialMantissa is (> 0) and (< 512)) { return; } 26 | variable.DeclType = DeclType.Float; 27 | variable.Trace = variable.Trace.Append($"{function}: {variable.Name} is probably {variable.DeclType} because {reason}"); 28 | } 29 | 30 | private static void ProcessSection(AssemblySection section) 31 | { 32 | foreach (var instruction in section.Instructions) 33 | { 34 | uint constant; 35 | switch (instruction.Name) 36 | { 37 | case "mov" or "xchg" or "lea": 38 | var variable = section.Owner.InstructionArgumentToVariable(instruction.DestArg) 39 | ?? section.Owner.InstructionArgumentToVariable(instruction.SrcArg1) 40 | ?? section.Owner.InstructionArgumentToVariable(instruction.SrcArg2); 41 | if (variable?.DeclType != DeclType.Unknown) { continue; } 42 | 43 | if (!instruction.DestArg.TryHexToUint32(out constant) 44 | && !instruction.SrcArg1.TryHexToUint32(out constant) 45 | && !instruction.SrcArg2.TryHexToUint32(out constant)) 46 | { 47 | continue; 48 | } 49 | 50 | GuessForVariable(section.Owner, variable, constant, instruction.ToString()); 51 | break; 52 | case "call": 53 | var callee = Function.GetFunctionByName(instruction.DestArg); 54 | if (instruction.CallParameterAssignmentIndices is not { } assignmentIndices) { continue; } 55 | 56 | for (int i = 0; i < assignmentIndices.Length; i++) 57 | { 58 | var parameter = callee.Parameters[i]; 59 | var assignmentInstruction = section.Owner.Instructions[assignmentIndices[i]]; 60 | 61 | if (!assignmentInstruction.SrcArg1.TryHexToUint32(out constant)) 62 | { 63 | continue; 64 | } 65 | 66 | GuessForVariable(callee, parameter, constant, $"({assignmentInstruction}) from {section.Name}"); 67 | } 68 | break; 69 | } 70 | } 71 | } 72 | 73 | public static void Process(Function function) 74 | { 75 | foreach (var section in function.AssemblySections) 76 | { 77 | ProcessSection(section); 78 | } 79 | } 80 | } -------------------------------------------------------------------------------- /Blitz3DDecomp/DecompilerSteps/Step5/CleanupLogicalBooleanExpressions.cs: -------------------------------------------------------------------------------- 1 | using Blitz3DDecomp.HighLevel; 2 | 3 | namespace Blitz3DDecomp.DecompilerSteps.Step5; 4 | 5 | static class CleanupLogicalBooleanExpressions 6 | { 7 | public static void Process(Function function) 8 | { 9 | bool containsTemp(Expression expression) 10 | => expression is VariableExpression { Variable: Function.DecompGeneratedTempVariable } 11 | || expression.InnerExpressions.Any(containsTemp); 12 | 13 | Function.DecompGeneratedTempVariable extractTemp(Expression expression) 14 | => expression is VariableExpression { Variable: Function.DecompGeneratedTempVariable variable } 15 | ? variable 16 | : extractTemp(expression.InnerExpressions.Where(containsTemp).First()); 17 | 18 | for (int i = 0; i < function.HighLevelStatements.Count; i++) 19 | { 20 | if (function.HighLevelStatements[i] is not JumpIfExpressionStatement { Condition: var condition } jumpStatement 21 | || !containsTemp(condition)) 22 | { 23 | continue; 24 | } 25 | 26 | var varToLookFor = extractTemp(condition); 27 | 28 | function.FindSectionForStatementIndex(i, out var section, out var indexOfJumpStatementInSection); 29 | 30 | var jumpSectionIndex = function.HighLevelSections.FindIndex(s => s.Name == jumpStatement.SectionName); 31 | if (jumpSectionIndex <= function.HighLevelSections.IndexOf(section)) { continue; } 32 | 33 | var statementsThatConsumeValue = new List(); 34 | for (int j = indexOfJumpStatementInSection + 1; j < section.Statements.Count; j++) 35 | { 36 | if (section.Statements[j] is not AssignmentStatement { Destination: var dest, Source: var source } assignmentStatement) { continue; } 37 | 38 | switch (source) 39 | { 40 | case AndExpression { Lhs: VariableExpression { Variable: Function.DecompGeneratedTempVariable lhsVariableInAnd } } 41 | when lhsVariableInAnd == varToLookFor 42 | && condition is OneIfZeroExpression: 43 | case OrExpression { Lhs: VariableExpression { Variable: Function.DecompGeneratedTempVariable lhsVariableInOr } } 44 | when lhsVariableInOr == varToLookFor 45 | && condition is OneIfNotZeroExpression: 46 | statementsThatConsumeValue.Add(assignmentStatement); 47 | break; 48 | } 49 | } 50 | if (statementsThatConsumeValue.Count == 1) 51 | { 52 | var consumerIndex = section.Statements.IndexOf(statementsThatConsumeValue[0]); 53 | section.Statements[consumerIndex] = new AssignmentStatement( 54 | statementsThatConsumeValue[0].Destination, 55 | statementsThatConsumeValue[0].Source switch 56 | { 57 | AndExpression { Lhs: var lhsInAnd, Rhs: var rhsInAnd } => new LogicalAndExpression(Lhs: lhsInAnd, Rhs: rhsInAnd), 58 | OrExpression { Lhs: var lhsInOr, Rhs: var rhsInOr } => new LogicalOrExpression(Lhs: lhsInOr, Rhs: rhsInOr), 59 | _ => throw new NotImplementedException() 60 | }); 61 | section.Statements.RemoveAt(indexOfJumpStatementInSection); 62 | 63 | function.RemoveUnreferencedHighLevelSections(); 64 | 65 | i--; 66 | } 67 | } 68 | } 69 | } -------------------------------------------------------------------------------- /Blitz3DDecomp/DecompilerSteps/Step0/IngestCodeFiles.cs: -------------------------------------------------------------------------------- 1 | using System.Text.RegularExpressions; 2 | using B3DDecompUtils; 3 | using Blitz3DDecomp.LowLevel; 4 | 5 | namespace Blitz3DDecomp; 6 | 7 | public static class IngestCodeFiles 8 | { 9 | public readonly record struct TempSection(string Name, int StartIndex); 10 | 11 | public static void FromDir(string inputDir) 12 | { 13 | var symbolDescRegex = new Regex("@([0-9A-F]+): (.+)"); 14 | var instructionTrimRegex = new Regex(" @[0-9A-F]+:( [0-9A-F][0-9A-F])+ +(.+)"); 15 | var instructionNameParseRegex = new Regex("(.+?) (.+)"); 16 | var instructionOneSrcArgsParseRegex = new Regex("(.+),(.+)"); 17 | var instructionTwoSrcArgsParseRegex = new Regex("(.+),(.+),(.+)"); 18 | 19 | inputDir = inputDir.AppendToPath("Code"); 20 | foreach (var filePath in Directory.GetFiles(inputDir)) 21 | { 22 | var fileName = Path.GetFileNameWithoutExtension(filePath); 23 | 24 | string functionName = fileName is "__MAIN" 25 | ? "EntryPoint" 26 | : fileName; 27 | if (functionName.StartsWith("_f")) { functionName = functionName[2..]; } 28 | 29 | var lines = File.ReadAllLines(filePath); 30 | var sections = new List(); 31 | var instructions = new List(); 32 | 33 | foreach (var line in lines) 34 | { 35 | if (line.Length == 0) { continue; } 36 | if (line[0] == '@') 37 | { 38 | sections.Add(new TempSection(line[11..].Trim(), instructions.Count)); 39 | } 40 | else 41 | { 42 | int i = 14; 43 | for (; i < line.Length; i+=3) 44 | { 45 | if (line[i + 0] != ' ' 46 | || !line[i + 1].IsHexDigit() 47 | || !line[i + 2].IsHexDigit()) 48 | { 49 | break; 50 | } 51 | } 52 | 53 | for (; i < line.Length; i++) 54 | { 55 | if (line[i] != ' ') { break; } 56 | } 57 | 58 | int instructionStart = i; 59 | for (; i < line.Length; i++) 60 | { 61 | if (line[i] == ' ') { break; } 62 | } 63 | string instructionName = line[instructionStart..i]; 64 | 65 | i++; 66 | int destArgStart = i; 67 | for (; i < line.Length; i++) 68 | { 69 | if (line[i] == ',') { break; } 70 | } 71 | string destArg = destArgStart < line.Length ? line[destArgStart..Math.Min(i, line.Length)].Trim() : ""; 72 | 73 | i++; 74 | int srcArg1Start = i; 75 | for (; i < line.Length; i++) 76 | { 77 | if (line[i] == ',') { break; } 78 | } 79 | string srcArg1 = srcArg1Start < line.Length ? line[srcArg1Start..Math.Min(i, line.Length)].Trim() : ""; 80 | 81 | i++; 82 | string srcArg2 = i < line.Length ? line[i..].Trim() : ""; 83 | 84 | if (instructionName == "nop") { continue; } 85 | instructions.Add(new Instruction(instructionName, destArg, srcArg1, srcArg2)); 86 | } 87 | } 88 | 89 | var newFunction = new Function(functionName, instructions, sections.ToArray()); 90 | } 91 | 92 | 93 | } 94 | } -------------------------------------------------------------------------------- /Blitz3DDecomp/DecompilerSteps/Step2/HandleFloatInstructions.cs: -------------------------------------------------------------------------------- 1 | using B3DDecompUtils; 2 | using Blitz3DDecomp.LowLevel; 3 | 4 | namespace Blitz3DDecomp.DecompilerSteps.Step2; 5 | 6 | static class HandleFloatInstructions 7 | { 8 | private static void ProcessSection(AssemblySection section) 9 | { 10 | for (var i = 1; i < section.Instructions.Length - 1; i++) 11 | { 12 | void handlePotentialFloatReturn() 13 | { 14 | if (section.Owner.ReturnType == DeclType.Unknown && i < section.Instructions.Length - 2) 15 | { 16 | var potentialLeave = section.Instructions[i + 2]; 17 | if (potentialLeave.Name is "jmp" 18 | && potentialLeave.DestArg.EndsWith("_leave_f"+section.Owner.Name, StringComparison.OrdinalIgnoreCase)) 19 | { 20 | section.Owner.ReturnType = DeclType.Float; 21 | } 22 | } 23 | } 24 | 25 | var prevInstruction = section.Instructions[i - 1]; 26 | var instruction = section.Instructions[i]; 27 | var nextInstruction = section.Instructions[i + 1]; 28 | 29 | if (prevInstruction.Name is "push" && nextInstruction.Name is "pop") 30 | { 31 | var prevVariable = section.Owner.InstructionArgumentToVariable(prevInstruction.DestArg); 32 | var nextVariable = section.Owner.InstructionArgumentToVariable(nextInstruction.DestArg); 33 | 34 | var oldPrevType = prevVariable?.DeclType; 35 | var oldNextType = nextVariable?.DeclType; 36 | 37 | switch (instruction.Name) 38 | { 39 | case "fld": 40 | if (prevVariable?.DeclType == DeclType.Unknown) { prevVariable.DeclType = DeclType.Float; } 41 | if (nextVariable?.DeclType == DeclType.Unknown) 42 | { 43 | nextVariable.DeclType = DeclType.Float; 44 | handlePotentialFloatReturn(); 45 | } 46 | break; 47 | case "fild": 48 | if (prevVariable?.DeclType == DeclType.Unknown) { prevVariable.DeclType = DeclType.Int; } 49 | if (nextVariable?.DeclType == DeclType.Unknown) 50 | { 51 | nextVariable.DeclType = DeclType.Float; 52 | handlePotentialFloatReturn(); 53 | } 54 | break; 55 | case "fstp": 56 | if (nextVariable?.DeclType == DeclType.Unknown) { nextVariable.DeclType = DeclType.Float; } 57 | break; 58 | case "fistp": 59 | if (nextVariable?.DeclType == DeclType.Unknown) { nextVariable.DeclType = DeclType.Int; } 60 | break; 61 | } 62 | 63 | if (oldPrevType == DeclType.Unknown && prevVariable != null && prevVariable.DeclType != DeclType.Unknown) 64 | { 65 | prevVariable.Trace = prevVariable.Trace.Append($"{section.Owner}: {prevVariable.Name} is {prevVariable.DeclType} because {instruction}"); 66 | } 67 | if (oldNextType == DeclType.Unknown && nextVariable != null && nextVariable.DeclType != DeclType.Unknown) 68 | { 69 | nextVariable.Trace = nextVariable.Trace.Append($"{section.Owner}: {nextVariable?.Name} is {nextVariable?.DeclType} because {instruction}"); 70 | } 71 | } 72 | } 73 | } 74 | 75 | public static void Process(Function function) 76 | { 77 | foreach (var section in function.AssemblySections) 78 | { 79 | ProcessSection(section); 80 | } 81 | } 82 | } -------------------------------------------------------------------------------- /Blitz3DDecomp/Basics/DimArray.cs: -------------------------------------------------------------------------------- 1 | namespace Blitz3DDecomp; 2 | 3 | sealed class DimArray 4 | { 5 | public sealed class AccessVariable : Variable 6 | { 7 | public readonly DimArray DimArray; 8 | public readonly string Index; 9 | 10 | public AccessVariable(DimArray dimArray, string arrayIndex) : base($"{dimArray.Name}[{arrayIndex}]") 11 | { 12 | this.DimArray = dimArray; 13 | this.Index = arrayIndex; 14 | } 15 | 16 | public override DeclType DeclType 17 | { 18 | get => DimArray.ElementDeclType; 19 | set => DimArray.ElementDeclType = value; 20 | } 21 | 22 | public override string ToInstructionArg() 23 | => Name; 24 | } 25 | 26 | public AccessVariable GetAccessVariable(string arrayIndex) => new AccessVariable(this, arrayIndex); 27 | 28 | public static ICollection AllDimArrays => lookupDictionary.Values; 29 | private static Dictionary lookupDictionary = new(); 30 | 31 | public readonly string Name; 32 | public readonly int NumDimensions; 33 | public DeclType ElementDeclType; 34 | 35 | private DimArray(string name, int numDimensions, DeclType elementDeclType) 36 | { 37 | Name = name; 38 | NumDimensions = numDimensions; 39 | ElementDeclType = elementDeclType; 40 | 41 | lookupDictionary.Add(name, this); 42 | } 43 | 44 | private static (string ArrayName, int NumDimensions, DeclType ElementDeclType) ParseSymbolName(string symbolName) 45 | { 46 | var defaultRetVal = ("", 0, DeclType.Unknown); 47 | 48 | symbolName = symbolName.StripDeref(); 49 | if (symbolName.Length >= 3 && symbolName[0] == '@') { symbolName = symbolName[1..]; } 50 | 51 | if (symbolName.Length >= 3 && symbolName[0] == '_' && symbolName[1] == 'a') 52 | { 53 | symbolName = symbolName[2..]; 54 | } 55 | else 56 | { 57 | return defaultRetVal; 58 | } 59 | 60 | if (!symbolName.EndsWith("dim", StringComparison.Ordinal)) { return defaultRetVal; } 61 | symbolName = symbolName[..^3]; 62 | 63 | int numDimensions = 0; 64 | for (int i = symbolName.Length - 1; i >= 0; i--) 65 | { 66 | if (symbolName[i] != '_') { continue; } 67 | 68 | if (!int.TryParse(symbolName[(i + 1)..], out numDimensions)) 69 | { 70 | return defaultRetVal; 71 | } 72 | symbolName = symbolName[..i]; 73 | 74 | break; 75 | } 76 | if (numDimensions <= 0) { return defaultRetVal; } 77 | 78 | string arrayName = ""; 79 | string typeName = ""; 80 | for (int i = symbolName.Length - 1; i >= 0; i--) 81 | { 82 | if (symbolName[i] != '_') { continue; } 83 | 84 | arrayName = symbolName[..i]; 85 | typeName = symbolName[(i + 1)..]; 86 | break; 87 | } 88 | 89 | var elementDeclType = typeName switch 90 | { 91 | "int" => DeclType.Int, 92 | "float" => DeclType.Float, 93 | "string" => DeclType.String, 94 | _ => DeclType.Unknown 95 | }; 96 | 97 | return (arrayName, numDimensions, elementDeclType); 98 | } 99 | 100 | public static DimArray? TryCreateFromSymbolName(string symbolName) 101 | { 102 | var (arrayName, numDimensions, elementDeclType) = ParseSymbolName(symbolName); 103 | 104 | if (string.IsNullOrEmpty(arrayName) 105 | || numDimensions <= 0) 106 | { 107 | return null; 108 | } 109 | 110 | return lookupDictionary.TryGetValue(arrayName, out var retVal) 111 | ? null 112 | : new DimArray(name: arrayName, numDimensions: numDimensions, elementDeclType: elementDeclType); 113 | } 114 | 115 | public static DimArray? TryFindByName(string symbolName) 116 | { 117 | if (lookupDictionary.TryGetValue(symbolName, out var retVal)) 118 | { 119 | return retVal; 120 | } 121 | 122 | var (arrayName, _, _) = ParseSymbolName(symbolName); 123 | 124 | if (string.IsNullOrEmpty(arrayName)) 125 | { 126 | return null; 127 | } 128 | 129 | return lookupDictionary.TryGetValue(arrayName, out retVal) 130 | ? retVal 131 | : null; 132 | } 133 | } -------------------------------------------------------------------------------- /Blitz3DDecomp/DecompilerSteps/Step0/CountLocals.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using System.Globalization; 3 | using System.Text.RegularExpressions; 4 | 5 | namespace Blitz3DDecomp; 6 | 7 | static class CountLocals 8 | { 9 | public static void Process(Function function) 10 | { 11 | if (!function.AssemblySections.Any()) { return; } 12 | 13 | var coreSection = 14 | function.CoreSymbolName == "__MAIN" 15 | ? function.AssemblySectionsByName.First(kvp => kvp.Key.EndsWith("_begin__MAIN")).Value 16 | : function.AssemblySectionsByName[function.CoreSymbolName]; 17 | int startIndex = function.CoreSymbolName == "__MAIN" ? 0 : 5; 18 | 19 | var initializedRegisters = new HashSet(); 20 | var ebpOffsetLocalRegex = new Regex("\\[ebp-0x([0-9a-f]+)\\]"); 21 | var ebpOffsets = new HashSet(); 22 | for (var i = startIndex; i < coreSection.Instructions.Length; i++) 23 | { 24 | bool isCallAllowedInPreamble(string instructionArg) 25 | { 26 | return instructionArg.EndsWith("_builtIn__bbVecAlloc", StringComparison.OrdinalIgnoreCase) 27 | || instructionArg.EndsWith("_builtIn__bbStrRetain", StringComparison.OrdinalIgnoreCase); 28 | } 29 | 30 | var instruction = coreSection.Instructions[i]; 31 | if (instruction.IsJumpOrCall) 32 | { 33 | if (instruction.Name == "call") 34 | { 35 | initializedRegisters.Add("eax"); 36 | } 37 | 38 | if (!isCallAllowedInPreamble(instruction.DestArg)) 39 | { 40 | break; 41 | } 42 | } 43 | 44 | if (instruction.Name == "mov" && instruction.DestArg.IsRegister() && instruction.SrcArg1 == "0x0") 45 | { 46 | initializedRegisters.Add(instruction.DestArg); 47 | continue; 48 | } 49 | 50 | if (instruction.Name == "mov" 51 | && ebpOffsetLocalRegex.Match(instruction.DestArg) is { Success: true } ebpOffsetMatch) 52 | { 53 | if (instruction.SrcArg1.IsRegister() && !initializedRegisters.Contains(instruction.SrcArg1)) { break; } 54 | 55 | if (!ebpOffsets.Add(int.Parse(ebpOffsetMatch.Groups[1].Value, NumberStyles.HexNumber))) { break; } 56 | 57 | coreSection.PreambleEndIndex = i; 58 | 59 | continue; 60 | } 61 | 62 | if (instruction.DestArg.ContainsRegister() && instruction.DestArg.Contains("ebp")) { break; } 63 | 64 | if (instruction.SrcArg1.ContainsRegister() && instruction.SrcArg1.Contains("ebp")) 65 | { 66 | bool isParamForLocalInitializer = false; 67 | if (instruction.SrcArg1.Contains("[ebp+")) 68 | { 69 | for (int j = i + 1; j < coreSection.Instructions.Length; j++) 70 | { 71 | var instruction2 = coreSection.Instructions[j]; 72 | if (instruction2.Name != "call") { continue; } 73 | 74 | isParamForLocalInitializer = isCallAllowedInPreamble(instruction2.DestArg); 75 | break; 76 | } 77 | } 78 | if (!isParamForLocalInitializer) { break; } 79 | } 80 | 81 | if (instruction.DestArg.Contains("@_v") || instruction.SrcArg1.Contains("@_v")){ break; } 82 | 83 | if (instruction.DestArg.Contains("@_f") || instruction.SrcArg1.Contains("@_f")){ break; } 84 | } 85 | 86 | for (int i = 4; i <= ebpOffsets.Count * 4; i += 4) 87 | { 88 | if (!ebpOffsets.Contains(i)) 89 | { 90 | Debugger.Break(); 91 | } 92 | } 93 | function.LocalVariables.AddRange(Enumerable.Range(0, ebpOffsets.Count) 94 | .Select(i => new Function.LocalVariable($"local{i}", i) { DeclType = DeclType.Unknown })); 95 | 96 | var lastLocalIndex = function.AssemblySections 97 | .SelectMany(s => s.Instructions.ToArray()) 98 | .SelectMany(i => new[] { i.DestArg, i.SrcArg1, i.SrcArg2 }) 99 | .Select(a => a.StripDeref()) 100 | .Where(a => a.StartsWith("ebp-0x", StringComparison.Ordinal)) 101 | .Distinct() 102 | .Select(a => int.Parse(a[6..], NumberStyles.HexNumber) >> 2) 103 | .Append(0) 104 | .Max(); 105 | function.CompilerGeneratedTempVars.AddRange(Enumerable.Range(ebpOffsets.Count, lastLocalIndex - ebpOffsets.Count) 106 | .Select(i => new Function.LocalVariable($"temp{i}", i) { DeclType = DeclType.Unknown })); 107 | } 108 | } -------------------------------------------------------------------------------- /Blitz3DDecomp/DecompilerSteps/Step1/CollectCalls.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using System.Globalization; 3 | using System.Text.RegularExpressions; 4 | using Blitz3DDecomp.LowLevel; 5 | 6 | namespace Blitz3DDecomp; 7 | 8 | static class CollectCalls 9 | { 10 | private static readonly Dictionary guesses = new Dictionary(); 11 | 12 | static void CrawlUp(IReadOnlyList instructions, int startIndex, out int espDiff, out int finalI, int dep) 13 | { 14 | finalI = 0; 15 | var startInstruction = instructions[startIndex]; 16 | 17 | espDiff = 0; 18 | if (startInstruction.DestArg.ContainsRegister()) 19 | { 20 | //Debugger.Break(); 21 | return; 22 | } 23 | 24 | var functionName = startInstruction.DestArg[1..]; 25 | var function = Function.TryGetFunctionByName(functionName); 26 | if (function is { Parameters.Count: 0 }) 27 | { 28 | startInstruction.CallParameterAssignmentIndices = Array.Empty(); 29 | finalI = startIndex; 30 | return; 31 | } 32 | var foundArgs = new Dictionary(); 33 | for (int i = startIndex - 1; i >= 0; i--) 34 | { 35 | var instruction = instructions[i]; 36 | if (instruction is { Name: "sub", DestArg: "esp", SrcArg1: var newEspOffsetStr }) 37 | { 38 | var newEspOffset = int.Parse(newEspOffsetStr[2..], NumberStyles.HexNumber); 39 | espDiff -= newEspOffset; 40 | 41 | if (espDiff < 0 && function is null) 42 | { 43 | finalI = i; 44 | break; 45 | } 46 | } 47 | else if (instruction.Name == "call") 48 | { 49 | CrawlUp(instructions, i, out int newEspDiff, out i, dep + 1); 50 | function = Function.TryGetFunctionByName(functionName); 51 | espDiff += newEspDiff; 52 | } 53 | else if (instruction.Name == "mov" && instruction.DestArg.Contains("[esp")) 54 | { 55 | var relativeRegex = new Regex("\\[esp\\+0x([0-9a-f]+)\\]"); 56 | var thisOffset = 0; 57 | if (relativeRegex.Match(instruction.DestArg) is { Success: true } relativeMatch) 58 | { 59 | thisOffset = int.Parse(relativeMatch.Groups[1].Value, NumberStyles.HexNumber); 60 | } 61 | 62 | if (foundArgs.ContainsKey(-espDiff + thisOffset)) 63 | { 64 | Debugger.Break(); 65 | } 66 | foundArgs[-espDiff + thisOffset] = i; 67 | 68 | if ((function != null && -espDiff + thisOffset > function.Parameters.Count * 4) || -espDiff + thisOffset < 0) 69 | { 70 | Debugger.Break(); 71 | } 72 | } 73 | 74 | if (function != null && foundArgs.Count >= function.Parameters.Count) 75 | { 76 | finalI = i; 77 | break; 78 | } 79 | } 80 | 81 | if (function != null) 82 | { 83 | if (foundArgs.Count != function.Parameters.Count) { Debugger.Break(); } 84 | espDiff += function.Parameters.Count * 4; 85 | } 86 | else 87 | { 88 | foreach (var kvp in foundArgs.OrderBy(k => k.Key)) 89 | { 90 | if (foundArgs.ContainsKey(kvp.Key + 4)) continue; 91 | if (foundArgs.Any(kvp2 => kvp2.Key > kvp.Key)) 92 | { 93 | Debugger.Break(); 94 | } 95 | break; 96 | } 97 | 98 | int foundArgCount = 99 | foundArgs.ContainsKey(0) 100 | ? foundArgs.Count 101 | : 0; // The generated assembly tends to not touch [esp] prior to a call to a function with no arguments 102 | 103 | espDiff += foundArgCount * 4; 104 | if (guesses.TryGetValue(functionName, out var prevGuess) && prevGuess != foundArgCount) 105 | { 106 | Debugger.Break(); 107 | } 108 | guesses.TryAdd(functionName, foundArgCount); 109 | if (foundArgCount == 0) 110 | { 111 | finalI = startIndex; 112 | espDiff = 0; 113 | } 114 | 115 | _ = new Function(functionName, foundArgCount); 116 | if (foundArgCount == 0) { foundArgs.Clear(); } 117 | } 118 | 119 | startInstruction.CallParameterAssignmentIndices = 120 | foundArgs.OrderBy(kvp => kvp.Key).Select(kvp => kvp.Value).ToArray(); 121 | } 122 | 123 | public static void Process(Function function) 124 | { 125 | var instructions = function.Instructions; 126 | for (int i = instructions.Length - 1; i >= 0; i--) 127 | { 128 | var instruction = instructions[i]; 129 | if (instruction.Name != "call") { continue; } 130 | CrawlUp(instructions, i, out var espDiff, out i, 0); 131 | } 132 | } 133 | } -------------------------------------------------------------------------------- /Blitz3DDecomp/DecompilerSteps/Step5/FixDimIndexer.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using B3DDecompUtils; 3 | using Blitz3DDecomp.HighLevel; 4 | using Blitz3DDecomp.Utils; 5 | 6 | namespace Blitz3DDecomp.DecompilerSteps.Step5; 7 | 8 | static class FixDimIndexer 9 | { 10 | public static void Process(Function function) 11 | { 12 | Expression unshift(Expression index) 13 | { 14 | if (index is ShiftRightUnsignedExpression 15 | { 16 | Rhs: ConstantExpression { Value: "0x2" }, 17 | Lhs: ShiftLeftExpression 18 | { 19 | Rhs: ConstantExpression { Value: "0x2" }, 20 | Lhs: var unshifted 21 | } 22 | }) 23 | { 24 | return unshifted; 25 | } 26 | return index; 27 | } 28 | 29 | DimAccessExpression rewriteDimAccess(DimAccessExpression dimAccessExpression) 30 | { 31 | var dim = dimAccessExpression.Owner; 32 | var dimSymbolName = $"@_a{dim.Name}_"; 33 | dimSymbolName += dim.ElementDeclType == DeclType.Int 34 | ? "int" 35 | : dim.ElementDeclType == DeclType.Float 36 | ? "float" 37 | : dim.ElementDeclType == DeclType.String 38 | ? "string" 39 | : "obj"; 40 | dimSymbolName += $"_{dim.NumDimensions}dim"; 41 | var dimSymbolExpression = new ConstantExpression(dimSymbolName); 42 | 43 | var indices = dimAccessExpression 44 | .Indices 45 | .Select(unshift) 46 | .ToArray(); 47 | if (dimAccessExpression.Indices.Length > 1 48 | || dimAccessExpression.Owner.NumDimensions <= 1) 49 | { 50 | return new DimAccessExpression(dim, indices.Select(CleanupIndexer.Process).ToArray()); 51 | } 52 | 53 | var indexExpression = indices[0]; 54 | var newIndices = new Expression?[dimAccessExpression.Owner.NumDimensions]; 55 | for (int i = 0; i < newIndices.Length; i++) 56 | { 57 | bool isDimOffsetExpression(Expression expression, out uint offset) 58 | { 59 | offset = uint.MaxValue; 60 | return expression is AddExpression { Lhs: var lhs, Rhs: ConstantExpression { Value: var rhsValue } } 61 | && lhs == dimSymbolExpression 62 | && rhsValue.TryHexToUint32(out offset); 63 | } 64 | 65 | Expression replaceOffsetExpressionWithOne(Expression expression) 66 | { 67 | if (isDimOffsetExpression(expression, out _)) 68 | { 69 | return new ConstantExpression("0x1"); 70 | } 71 | return expression; 72 | } 73 | 74 | bool foundNonZeroIndex = false; 75 | if (indexExpression is AddExpression { Lhs: var lhs, Rhs: var rhs }) 76 | { 77 | foreach (var innerExpression in lhs.InnerExpressions) 78 | { 79 | if (isDimOffsetExpression(innerExpression, out var offset)) 80 | { 81 | indexExpression = rhs; 82 | uint unwrappedIndexNum = ((offset - 0xc) >> 2) + 1; 83 | newIndices[unwrappedIndexNum] = lhs.Map(replaceOffsetExpressionWithOne); 84 | foundNonZeroIndex = true; 85 | break; 86 | } 87 | } 88 | foreach (var innerExpression in rhs.InnerExpressions) 89 | { 90 | if (isDimOffsetExpression(innerExpression, out var offset)) 91 | { 92 | indexExpression = lhs; 93 | uint unwrappedIndexNum = ((offset - 0xc) >> 2) + 1; 94 | newIndices[unwrappedIndexNum] = rhs.Map(replaceOffsetExpressionWithOne); 95 | foundNonZeroIndex = true; 96 | break; 97 | } 98 | } 99 | } 100 | if (!foundNonZeroIndex) 101 | { 102 | newIndices[0] = indexExpression; 103 | break; 104 | } 105 | } 106 | 107 | if (newIndices.Contains(null)) { throw new Exception($"At least one index not set"); } 108 | return new DimAccessExpression(dim, newIndices.OfType().Select(CleanupIndexer.Process).ToArray()); 109 | } 110 | 111 | for (int i = 0; i < function.HighLevelStatements.Count; i++) 112 | { 113 | var statement = function.HighLevelStatements[i]; 114 | function.HighLevelStatements[i] = statement.Map( 115 | stmt => stmt, 116 | expr => expr switch 117 | { 118 | DimAccessExpression dimAccessExpression 119 | => rewriteDimAccess(dimAccessExpression), 120 | _ 121 | => expr 122 | }); 123 | } 124 | } 125 | } -------------------------------------------------------------------------------- /Blitz3DDecomp/DecompilerSteps/Step5/CleanupForOnInt.cs: -------------------------------------------------------------------------------- 1 | using Blitz3DDecomp.HighLevel; 2 | 3 | namespace Blitz3DDecomp.DecompilerSteps.Step5; 4 | 5 | static class CleanupForOnInt 6 | { 7 | public static void Process(Function function) 8 | { 9 | var sectionsByName = function.HighLevelSectionsByName; 10 | for (int i = 1; i < function.HighLevelStatements.Count; i++) 11 | { 12 | if (function.HighLevelStatements[i] is not WhileStatement whileStatement) { continue; } 13 | if (function.HighLevelStatements[i - 1] is not AssignmentStatement { Destination: var possibleFirstAssignee, Source: var startValue }) { continue; } 14 | AccessExpression iterator; 15 | Expression endValue; 16 | bool stepMustBePositive; 17 | switch (whileStatement.Condition) 18 | { 19 | case OneIfExpressionIsGreaterThanOrEqualToOtherExpression condition: 20 | { 21 | if (condition.Lhs is AccessExpression possibleIterator) 22 | { 23 | iterator = possibleIterator; 24 | endValue = condition.Rhs; 25 | stepMustBePositive = false; 26 | } 27 | else if (condition.Rhs is AccessExpression possibleIterator2) 28 | { 29 | iterator = possibleIterator2; 30 | endValue = condition.Lhs; 31 | stepMustBePositive = true; 32 | } 33 | else 34 | { 35 | continue; 36 | } 37 | break; 38 | } 39 | case OneIfExpressionIsLessThanOrEqualToOtherExpression condition: 40 | { 41 | if (condition.Lhs is AccessExpression possibleIterator) 42 | { 43 | iterator = possibleIterator; 44 | endValue = condition.Rhs; 45 | stepMustBePositive = true; 46 | } 47 | else if (condition.Rhs is AccessExpression possibleIterator2) 48 | { 49 | iterator = possibleIterator2; 50 | endValue = condition.Lhs; 51 | stepMustBePositive = false; 52 | } 53 | else 54 | { 55 | continue; 56 | } 57 | break; 58 | } 59 | default: 60 | continue; 61 | } 62 | if (iterator != possibleFirstAssignee) { continue; } 63 | 64 | int indent = 1; 65 | int j = i + 1; 66 | for (; j < function.HighLevelStatements.Count; j++) 67 | { 68 | indent -= function.HighLevelStatements[j].IndentationToSubtract; 69 | if (indent <= 0) { break; } 70 | indent += function.HighLevelStatements[j].IndentationToAdd; 71 | } 72 | if (j >= function.HighLevelStatements.Count || indent < 0) { continue; } 73 | if (function.HighLevelStatements[j] is not WendStatement) { continue; } 74 | 75 | if (function.HighLevelStatements[j - 1] is not AssignmentStatement { Destination: VariableExpression destinationExpression, Source: var source }) { continue; } 76 | if (destinationExpression != iterator) { continue; } 77 | 78 | Expression step; 79 | switch (source) 80 | { 81 | case AddExpression addExpression: 82 | if (!stepMustBePositive) { continue; } 83 | if (addExpression.Lhs == destinationExpression 84 | && addExpression.Rhs is ConstantExpression possibleStep) 85 | { 86 | step = possibleStep; 87 | } 88 | else if (addExpression.Rhs == destinationExpression 89 | && addExpression.Lhs is ConstantExpression possibleStep2) 90 | { 91 | step = possibleStep2; 92 | } 93 | else 94 | { 95 | continue; 96 | } 97 | break; 98 | case SubtractExpression subtractExpression: 99 | if (stepMustBePositive) { continue; } 100 | if (subtractExpression.Lhs == destinationExpression 101 | && subtractExpression.Rhs is ConstantExpression possibleStep3) 102 | { 103 | step = new SignFlipExpression(possibleStep3); 104 | } 105 | else 106 | { 107 | continue; 108 | } 109 | break; 110 | default: 111 | continue; 112 | } 113 | 114 | function.HighLevelStatements[j] = new NextStatement(); 115 | function.FindSectionForStatementIndex(j - 1, out var section2, out var indexInSection2); 116 | section2.Statements.RemoveAt(indexInSection2); 117 | function.HighLevelStatements[i] = new ForOnIntStatement(iterator, startValue, endValue, step); 118 | function.FindSectionForStatementIndex(i - 1, out var section1, out var indexInSection1); 119 | section1.Statements.RemoveAt(indexInSection1); 120 | } 121 | } 122 | } -------------------------------------------------------------------------------- /Blitz3DDecomp/DecompilerSteps/Step5/ConvertFunctionCallsToFinalRepresentation.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | using Blitz3DDecomp.HighLevel; 3 | 4 | namespace Blitz3DDecomp.DecompilerSteps.Step5; 5 | 6 | static class ConvertFunctionCallsToFinalRepresentation 7 | { 8 | private static Statement MapStatement(Statement statement) 9 | { 10 | if (statement is AssignmentStatement { Destination: var destination, Source: CallExpression innerCallExpression } 11 | && innerCallExpression.Callee.Name is "_builtIn__bbReadInt" or "_builtIn__bbReadFloat" or "_builtIn__bbReadStr") 12 | { 13 | return new DataReadStatement(destination); 14 | } 15 | 16 | return statement; 17 | } 18 | 19 | private static Expression MapExpression(Expression expression) 20 | { 21 | static bool isCompareFunction( 22 | Expression innerExpression, 23 | [NotNullWhen(returnValue: true)]out CallExpression? callExpression) 24 | { 25 | if (innerExpression is not SubtractExpression 26 | { 27 | Lhs: CallExpression call, 28 | Rhs: ConstantExpression { Value: "0x0" } 29 | }) 30 | { 31 | callExpression = null; 32 | return false; 33 | } 34 | 35 | if (call.Callee.Name is "_builtIn__bbStrCompare" or "_builtIn__bbObjCompare") 36 | { 37 | callExpression = call; 38 | return true; 39 | } 40 | 41 | callExpression = null; 42 | return false; 43 | } 44 | 45 | CallExpression? innerCallExpression; 46 | switch (expression) 47 | { 48 | case OneIfZeroExpression compareExpr when isCompareFunction(compareExpr.OriginalExpression, out innerCallExpression): 49 | return new OneIfExpressionsEqualExpression( 50 | innerCallExpression.Arguments[0], 51 | innerCallExpression.Arguments[1]); 52 | case OneIfNotZeroExpression compareExpr when isCompareFunction(compareExpr.OriginalExpression, out innerCallExpression): 53 | return new OneIfExpressionsNotEqualExpression( 54 | innerCallExpression.Arguments[0], 55 | innerCallExpression.Arguments[1]); 56 | case OneIfLessThanZeroExpression compareExpr when isCompareFunction(compareExpr.OriginalExpression, out innerCallExpression): 57 | return new OneIfExpressionIsLessThanOtherExpression( 58 | innerCallExpression.Arguments[0], 59 | innerCallExpression.Arguments[1]); 60 | case OneIfLessThanOrEqualToZeroExpression compareExpr when isCompareFunction(compareExpr.OriginalExpression, out innerCallExpression): 61 | return new OneIfExpressionIsLessThanOrEqualToOtherExpression( 62 | innerCallExpression.Arguments[0], 63 | innerCallExpression.Arguments[1]); 64 | case OneIfGreaterThanZeroExpression compareExpr when isCompareFunction(compareExpr.OriginalExpression, out innerCallExpression): 65 | return new OneIfExpressionIsGreaterThanOtherExpression( 66 | innerCallExpression.Arguments[0], 67 | innerCallExpression.Arguments[1]); 68 | case OneIfGreaterThanOrEqualToZeroExpression compareExpr when isCompareFunction(compareExpr.OriginalExpression, out innerCallExpression): 69 | return new OneIfExpressionIsGreaterThanOrEqualToOtherExpression( 70 | innerCallExpression.Arguments[0], 71 | innerCallExpression.Arguments[1]); 72 | case CallExpression callExpression: 73 | switch (callExpression.Callee.Name) 74 | { 75 | case "_builtIn__bbMod" or "_builtIn__bbFMod": 76 | return new ModuloExpression(callExpression.Arguments[0], callExpression.Arguments[1]); 77 | case "_builtIn__bbAbs" or "_builtIn__bbFAbs": 78 | return new AbsExpression(callExpression.Arguments[0]); 79 | case "_builtIn__bbSgn" or "_builtIn__bbFSgn": 80 | return new SignExpression(callExpression.Arguments[0]); 81 | case "_builtIn__bbFPow": 82 | return new ExponentiationExpression(callExpression.Arguments[0], callExpression.Arguments[1]); 83 | case "_builtIn__bbStrConcat": 84 | return new AddExpression(callExpression.Arguments[0], callExpression.Arguments[1]); 85 | case "_builtIn__bbStrToInt": 86 | return new ConvertToIntExpression(callExpression.Arguments[0]); 87 | case "_builtIn__bbStrToFloat": 88 | return new ConvertToFloatExpression(callExpression.Arguments[0]); 89 | case "_builtIn__bbStrFromInt" or "_builtIn__bbStrFromFloat": 90 | return new ConvertToStringExpression(callExpression.Arguments[0]); 91 | } 92 | break; 93 | } 94 | 95 | return expression; 96 | } 97 | 98 | private static void ProcessStatement(Function function, int i) 99 | { 100 | var statement = function.HighLevelStatements[i]; 101 | function.HighLevelStatements[i] = statement.Map( 102 | MapStatement, 103 | MapExpression); 104 | } 105 | 106 | public static void Process(Function function) 107 | { 108 | for (var i = 0; i < function.HighLevelStatements.Count; i++) 109 | { 110 | ProcessStatement(function, i); 111 | } 112 | } 113 | } -------------------------------------------------------------------------------- /Blitz3DDecomp/DecompilerSteps/Step5/RemoveSingleUseTemps.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using System.Linq.Expressions; 3 | using B3DDecompUtils; 4 | using Blitz3DDecomp.HighLevel; 5 | using Blitz3DDecomp.Utils; 6 | using Expression = Blitz3DDecomp.HighLevel.Expression; 7 | 8 | namespace Blitz3DDecomp.DecompilerSteps.Step5; 9 | 10 | static class RemoveSingleUseTemps 11 | { 12 | private static void ProcessStatement(Function function, int statementIndex) 13 | { 14 | var statementToRemove = function.HighLevelStatements[statementIndex]; 15 | if (statementToRemove is not AssignmentStatement 16 | { 17 | Destination: VariableExpression { Variable: Function.DecompGeneratedTempVariable tempVariable }, 18 | Source: var srcExpression 19 | }) 20 | { 21 | return; 22 | } 23 | 24 | bool statementReferencesVariable(Statement statement) 25 | { 26 | var innerExpressions = statement.InnerExpressions.ToArray(); 27 | while (innerExpressions.Any()) 28 | { 29 | if (innerExpressions.Any(e => 30 | e is VariableExpression { Variable: var innerVar } 31 | && innerVar == tempVariable)) 32 | { 33 | return true; 34 | } 35 | innerExpressions = innerExpressions.SelectMany(e => e.InnerExpressions).ToArray(); 36 | } 37 | return false; 38 | } 39 | 40 | int secondUseIndex = -1; 41 | bool moreThanOneUse = false; 42 | int jumpIndex = -1; 43 | 44 | for (int i = statementIndex + 1; i < Math.Min(statementIndex + 60, function.HighLevelStatements.Count); i++) 45 | { 46 | var statement = function.HighLevelStatements[i]; 47 | if (jumpIndex < 0 && statement is JumpStatement) 48 | { 49 | jumpIndex = i; 50 | } 51 | 52 | if (!statementReferencesVariable(statement)) 53 | { 54 | continue; 55 | } 56 | 57 | if (secondUseIndex < 0) 58 | { 59 | secondUseIndex = i; 60 | } 61 | else 62 | { 63 | moreThanOneUse = true; 64 | break; 65 | } 66 | } 67 | 68 | if (secondUseIndex > jumpIndex && jumpIndex >= 0) { return; } 69 | 70 | function.FindSectionForStatementIndex(statementIndex, out var section, out var indexInSection); 71 | if (secondUseIndex < 0) 72 | { 73 | bool statementHasSideEffects = false; 74 | var innerExpressions = statementToRemove.InnerExpressions.ToArray(); 75 | while (innerExpressions.Any()) 76 | { 77 | if (innerExpressions.Any(e => e is CallExpression or ConstructorExpression)) 78 | { 79 | statementHasSideEffects = true; 80 | break; 81 | } 82 | innerExpressions = innerExpressions.SelectMany(e => e.InnerExpressions).ToArray(); 83 | } 84 | 85 | if (!statementHasSideEffects) 86 | { 87 | Logger.WriteLine($"{function}: removing {statementToRemove.StringRepresentation}"); 88 | section.Statements.RemoveAt(indexInSection); 89 | } 90 | else 91 | { 92 | var freestandingStatement = new FreeStandingExpressionStatement(srcExpression); 93 | if (srcExpression is not (CallExpression or ConstructorExpression)) { Debugger.Break(); } 94 | Logger.WriteLine($"{function}: rewriting {statementToRemove.StringRepresentation} as {freestandingStatement.StringRepresentation}"); 95 | function.HighLevelStatements[statementIndex] = freestandingStatement; 96 | } 97 | return; 98 | } 99 | 100 | function.FindSectionForStatementIndex(secondUseIndex, out var secondSection, out _); 101 | if (section != secondSection) { return; } 102 | 103 | Expression expressionMapper(Expression expression) 104 | => expression switch 105 | { 106 | VariableExpression { Variable: var variable } when variable == tempVariable => srcExpression, 107 | ArrayAccessExpression { Owner: var owner, Index: var index } => new ArrayAccessExpression(owner, CleanupIndexer.Process(index)), 108 | _ => expression 109 | }; 110 | 111 | var secondUseStatement = function.HighLevelStatements[secondUseIndex]; 112 | 113 | Statement rewrittenStatement; 114 | if (secondUseStatement is AssignmentStatement { Destination: VariableExpression { Variable: var secondVariable }, Source: var srcExpression2 } 115 | && secondVariable == tempVariable) 116 | { 117 | rewrittenStatement = new AssignmentStatement(new VariableExpression(tempVariable), srcExpression2.Map(expressionMapper)); 118 | } 119 | else if (moreThanOneUse) 120 | { 121 | return; 122 | } 123 | else 124 | { 125 | rewrittenStatement = secondUseStatement.Map( 126 | statementMapper: statement => statement, 127 | expressionMapper: expressionMapper); 128 | } 129 | Logger.WriteLine($"{function}: rewriting {secondUseStatement.StringRepresentation} as {rewrittenStatement.StringRepresentation} and removing {statementToRemove.StringRepresentation}"); 130 | function.HighLevelStatements[secondUseIndex] = rewrittenStatement; 131 | section.Statements.RemoveAt(indexInSection); 132 | } 133 | 134 | public static void Process(Function function) 135 | { 136 | for (int i = function.HighLevelStatements.Count - 1; i >= 0; i--) 137 | { 138 | ProcessStatement(function, i); 139 | } 140 | } 141 | } -------------------------------------------------------------------------------- /Blitz3DDecomp/DecompilerSteps/Step3/VariableTypePropagation.cs: -------------------------------------------------------------------------------- 1 | using B3DDecompUtils; 2 | using Blitz3DDecomp.LowLevel; 3 | 4 | namespace Blitz3DDecomp.DecompilerSteps.Step3; 5 | 6 | static class VariableTypePropagation 7 | { 8 | public static bool Process(Function function) 9 | { 10 | bool somethingChanged = false; 11 | 12 | void exchangeTypes(Variable destVar, Variable srcVar) 13 | { 14 | if (destVar.DeclType == DeclType.Unknown && srcVar.CanBeSourceOfPropagation()) 15 | { 16 | somethingChanged = true; 17 | destVar.DeclType = srcVar.DeclType; 18 | destVar.Trace = srcVar.Trace.Append($"{function}: {destVar.Name} is {destVar.DeclType} because {srcVar}"); 19 | } 20 | if (srcVar.DeclType == DeclType.Unknown && destVar.CanBeSourceOfPropagation()) 21 | { 22 | somethingChanged = true; 23 | srcVar.DeclType = destVar.DeclType; 24 | srcVar.Trace = destVar.Trace.Append($"{function}: {srcVar.Name} is {srcVar.DeclType} because {destVar}"); 25 | } 26 | } 27 | 28 | void handleMovLea(Instruction instruction, int instructionIndex) 29 | { 30 | if (instruction.Name is not ("mov" or "lea")) { return; } 31 | 32 | var destVar = function.InstructionArgumentToVariable(instruction.DestArg); 33 | var srcVar = function.InstructionArgumentToVariable(instruction.SrcArg1); 34 | if (destVar is null || srcVar is null) { return; } 35 | 36 | if (srcVar.DeclType == DeclType.Int 37 | && instruction.Name == "mov" 38 | && instruction.SrcArg1.StripDeref() != instruction.SrcArg1) 39 | { 40 | // Might be passing a Bank to a lib function as a pointer, 41 | // check for some pointer offset magic 42 | for (int i = instructionIndex - 1; i >= Math.Max(0, instructionIndex - 200); i--) 43 | { 44 | var prevInstruction = function.Instructions[i]; 45 | if (prevInstruction.Name != "add") { continue; } 46 | if (prevInstruction.DestArg != instruction.SrcArg1.StripDeref()) { continue; } 47 | 48 | var bankOffsetConstant = CurrentCompiler.Value is Compiler.BlitzPlus 49 | ? "0x1c" 50 | : "0x4"; 51 | if (prevInstruction.SrcArg2 != bankOffsetConstant) { break; } 52 | 53 | var newSrcVar = function.InstructionArgumentToVariable(prevInstruction.SrcArg1); 54 | if (newSrcVar?.DeclType != DeclType.Int) { break; } 55 | 56 | instruction.SrcArg1 = prevInstruction.SrcArg1; 57 | destVar.DeclType = DeclType.Pointer; 58 | destVar.Trace = newSrcVar.Trace.Append($"{function}: {destVar.Name} is {DeclType.Pointer} because it's a bank passed to a lib function"); 59 | return; 60 | } 61 | } 62 | 63 | exchangeTypes(destVar, srcVar); 64 | } 65 | 66 | void handleXchg(Instruction instruction) 67 | { 68 | if (instruction.Name is not "xchg") { return; } 69 | 70 | var lhsVarPrev = function.InstructionArgumentToVariable(instruction.DestArg); 71 | var rhsVarPrev = function.InstructionArgumentToVariable(instruction.SrcArg1); 72 | var lhsVarPost = instruction.XchgLhsPost; 73 | var rhsVarPost = instruction.XchgRhsPost; 74 | 75 | if (lhsVarPrev is null || rhsVarPrev is null || lhsVarPost is null || rhsVarPost is null) { return; } 76 | 77 | exchangeTypes(rhsVarPost, lhsVarPrev); 78 | exchangeTypes(lhsVarPost, rhsVarPrev); 79 | } 80 | 81 | void handleCmp(Instruction instruction) 82 | { 83 | if (instruction.Name is not "cmp") { return; } 84 | 85 | var destVar = function.InstructionArgumentToVariable(instruction.DestArg); 86 | var srcVar = function.InstructionArgumentToVariable(instruction.SrcArg1); 87 | if (destVar is null && srcVar is null) { return; } 88 | 89 | uint parsedUint; 90 | if (destVar != null && srcVar != null) 91 | { 92 | exchangeTypes(destVar, srcVar); 93 | } 94 | else if (instruction.DestArg.TryHexToUint32(out parsedUint) && parsedUint != 0 && srcVar?.DeclType == DeclType.Unknown) 95 | { 96 | srcVar.DeclType = DeclType.Int; 97 | srcVar.Trace = srcVar.Trace.Append($"{function}: {srcVar.Name} is {DeclType.Int} because {instruction}"); 98 | somethingChanged = true; 99 | } 100 | else if (instruction.SrcArg1.TryHexToUint32(out parsedUint) && parsedUint != 0 && destVar?.DeclType == DeclType.Unknown) 101 | { 102 | destVar.DeclType = DeclType.Int; 103 | destVar.Trace = destVar.Trace.Append($"{function}: {destVar.Name} is {DeclType.Int} because {instruction}"); 104 | somethingChanged = true; 105 | } 106 | } 107 | 108 | for (var instructionIndex = 0; instructionIndex < function.Instructions.Length; instructionIndex++) 109 | { 110 | var instruction = function.Instructions[instructionIndex]; 111 | handleMovLea(instruction, instructionIndex); 112 | handleXchg(instruction); 113 | handleCmp(instruction); 114 | } 115 | 116 | for (var instructionIndex = function.Instructions.Length - 1; instructionIndex >= 0; instructionIndex--) 117 | { 118 | var instruction = function.Instructions[instructionIndex]; 119 | handleMovLea(instruction, instructionIndex); 120 | handleXchg(instruction); 121 | handleCmp(instruction); 122 | } 123 | 124 | return somethingChanged; 125 | } 126 | } --------------------------------------------------------------------------------