Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 102 additions & 0 deletions .scripts/update_ilspy.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
#!/usr/bin/env bash

set -euo pipefail

if [[ -z "${ILSPY_DIR:-}" ]]; then
echo "Error: ILSPY_DIR is not set."
exit 1
fi

if [[ ! -d "$ILSPY_DIR" ]]; then
echo "Error: ILSPY_DIR does not exist: $ILSPY_DIR"
exit 1
fi

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
WORKSPACE_DIR="$(cd "$SCRIPT_DIR/../../.." && pwd)"
DEPS_DIR="$WORKSPACE_DIR/modules/gdsdecomp/godot-mono-decomp/dependencies"
CSPROJ_PATH="$WORKSPACE_DIR/modules/gdsdecomp/godot-mono-decomp/GodotMonoDecomp/GodotMonoDecomp.csproj"

mkdir -p "$DEPS_DIR"

echo "Packing ILSpy packages into: $DEPS_DIR"
(
cd "$ILSPY_DIR"
dotnet pack -c Release ICSharpCode.Decompiler -o "$DEPS_DIR"
dotnet pack -c Release ICSharpCode.ILSpyX -o "$DEPS_DIR"
)

latest_package_version() {
local package_name="$1"
local newest

newest="$(
ls -t "$DEPS_DIR/${package_name}."*.nupkg 2>/dev/null \
| while IFS= read -r path; do
[[ "$path" == *.snupkg ]] && continue
printf '%s\n' "$path"
done \
| head -n 1
)"

if [[ -z "$newest" ]]; then
echo "Error: Could not find packed package for $package_name in $DEPS_DIR"
exit 1
fi

local filename version
filename="${newest##*/}"
version="${filename#${package_name}.}"
version="${version%.nupkg}"
printf '%s\n' "$version"
}

prune_older_packages() {
local package_name="$1"
local keep_version="$2"

shopt -s nullglob
local files=(
"$DEPS_DIR/${package_name}."*.nupkg
"$DEPS_DIR/${package_name}."*.snupkg
)
shopt -u nullglob

for file in "${files[@]}"; do
case "$file" in
"$DEPS_DIR/${package_name}.${keep_version}.nupkg" | "$DEPS_DIR/${package_name}.${keep_version}.snupkg")
;;
*)
rm -f "$file"
;;
esac
done
}

decompiler_version="$(latest_package_version "ICSharpCode.Decompiler")"
ilspyx_version="$(latest_package_version "ICSharpCode.ILSpyX")"

prune_older_packages "ICSharpCode.Decompiler" "$decompiler_version"
prune_older_packages "ICSharpCode.ILSpyX" "$ilspyx_version"

if [[ ! -f "$CSPROJ_PATH" ]]; then
echo "Error: csproj not found: $CSPROJ_PATH"
exit 1
fi

# Remove existing Decompiler/ILSpyX references (active or commented), then insert updated package refs.
sed -i.bak '/PackageReference Include="ICSharpCode\.Decompiler"/d' "$CSPROJ_PATH"
sed -i.bak '/PackageReference Include="ICSharpCode\.ILSpyX"/d' "$CSPROJ_PATH"
sed -i.bak '/ProjectReference Include=".*ICSharpCode\.Decompiler\.csproj"/d' "$CSPROJ_PATH"
sed -i.bak '/ProjectReference Include=".*ICSharpCode\.ILSpyX\.csproj"/d' "$CSPROJ_PATH"

sed -i.bak "/<PackageReference Include=\"NuGet.Packaging\"/i\\
<PackageReference Include=\"ICSharpCode.Decompiler\" Version=\"$decompiler_version\" />\\
<PackageReference Include=\"ICSharpCode.ILSpyX\" Version=\"$ilspyx_version\" />
" "$CSPROJ_PATH"

rm -f "$CSPROJ_PATH.bak"

echo "Updated $CSPROJ_PATH"
echo " - ICSharpCode.Decompiler: $decompiler_version"
echo " - ICSharpCode.ILSpyX: $ilspyx_version"
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
using System.IO;
using ICSharpCode.Decompiler.CSharp;
using ICSharpCode.Decompiler.CSharp.OutputVisitor;
using ICSharpCode.Decompiler.CSharp.Syntax;

namespace GodotMonoDecomp;

internal sealed class CollectionExpressionArrayAnnotation
{
public static readonly CollectionExpressionArrayAnnotation Instance = new();

private CollectionExpressionArrayAnnotation()
{
}
}

internal sealed class CollectionExpressionSpreadElementAnnotation
{
public static readonly CollectionExpressionSpreadElementAnnotation Instance = new();

private CollectionExpressionSpreadElementAnnotation()
{
}
}

public class GodotCSharpOutputVisitor : CSharpOutputVisitor
{
public GodotCSharpOutputVisitor(TextWriter w, CSharpFormattingOptions formattingOptions)
: base(w, formattingOptions)
{
}

public override void VisitArrayInitializerExpression(ArrayInitializerExpression arrayInitializerExpression)
{
if (arrayInitializerExpression.Annotation<CollectionExpressionArrayAnnotation>() == null)
{
base.VisitArrayInitializerExpression(arrayInitializerExpression);
return;
}

StartNode(arrayInitializerExpression);
WriteToken(Roles.LBracket);

bool first = true;
foreach (var element in arrayInitializerExpression.Elements)
{
if (!first)
{
WriteToken(Roles.Comma);
Space();
}

if (element.Annotation<CollectionExpressionSpreadElementAnnotation>() != null)
{
WriteToken(BinaryOperatorExpression.RangeRole);
}

element.AcceptVisitor(this);
first = false;
}

WriteToken(Roles.RBracket);
EndNode(arrayInitializerExpression);
}
}
82 changes: 82 additions & 0 deletions godot-mono-decomp/GodotMonoDecomp/Common.cs
Original file line number Diff line number Diff line change
Expand Up @@ -277,4 +277,86 @@ public static Guid GenerateDeterministicGuidFromString(string input)
return new Guid(hashBytes);
}
}

private const string GeneratedCodeAttributeFullName = "System.CodeDom.Compiler.GeneratedCodeAttribute";
public static bool RemoveGeneratedCodeAttributes(ICSharpCode.Decompiler.CSharp.Syntax.AstNodeCollection<ICSharpCode.Decompiler.CSharp.Syntax.AttributeSection> attributeSections, string generatorName)
{
bool removedAny = false;
foreach (var section in attributeSections.ToArray())
{
foreach (var attribute in section.Attributes.ToArray())
{
if (IsGeneratedCodeAttributeForTool(attribute, generatorName))
{
attribute.Remove();
removedAny = true;
}
}

if (section.Attributes.Count == 0)
{
section.Remove();
}
}

return removedAny;
}

public static bool HasMatchingShortAttributeName(string typeName, string expectedShortName)
{
if (typeName.StartsWith("global::", StringComparison.Ordinal))
{
typeName = typeName.Substring("global::".Length);
}

int lastDot = typeName.LastIndexOf('.');
if (lastDot >= 0 && lastDot < typeName.Length - 1)
{
typeName = typeName.Substring(lastDot + 1);
}

if (typeName.EndsWith("Attribute", StringComparison.Ordinal))
{
typeName = typeName.Substring(0, typeName.Length - "Attribute".Length);
}

return string.Equals(typeName, expectedShortName, StringComparison.Ordinal);
}

public static bool IsAttribute(ICSharpCode.Decompiler.CSharp.Syntax.Attribute attribute, string expectedFullName, string expectedShortName)
{
if (attribute.Type.Annotation<ICSharpCode.Decompiler.Semantics.TypeResolveResult>() is { Type: { } typeResult })
{
if (string.Equals(typeResult.FullName, expectedFullName, StringComparison.Ordinal))
{
return true;
}
}

if (attribute.GetSymbol() is IMethod method && method.DeclaringType != null)
{
if (string.Equals(method.DeclaringType.FullName, expectedFullName, StringComparison.Ordinal))
{
return true;
}
}

return HasMatchingShortAttributeName(attribute.Type.ToString(), expectedShortName);
}


public static bool IsGeneratedCodeAttributeForTool(ICSharpCode.Decompiler.CSharp.Syntax.Attribute attribute, string generatorName)
{
if (!IsAttribute(attribute, GeneratedCodeAttributeFullName, "GeneratedCode"))
{
return false;
}

if (attribute.Arguments.FirstOrDefault() is ICSharpCode.Decompiler.CSharp.Syntax.PrimitiveExpression { Value: string toolName })
{
return string.Equals(toolName, generatorName, StringComparison.Ordinal);
}

return false;
}
}
4 changes: 2 additions & 2 deletions godot-mono-decomp/GodotMonoDecomp/DotNetDepInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,7 @@ public static string GetDepPath(string assemblyPath)
return null;
}

public async Task StartResolvePackageAndCheckHash(CancellationToken cancellationToken)
public async Task StartResolvePackageAndCheckHash(bool checkOnline, CancellationToken cancellationToken)
{
if (!Serviceable || Type != "package" || string.IsNullOrEmpty(Sha512) || !Sha512.StartsWith("sha512-"))
{
Expand All @@ -245,7 +245,7 @@ public async Task StartResolvePackageAndCheckHash(CancellationToken cancellation
string? hash;
try
{
hash = await NugetDetails.ResolvePackageAndGetContentHash(Name, Version, cancellationToken);
hash = await NugetDetails.ResolvePackageAndGetContentHash(Name, Version, checkOnline, cancellationToken);
}
catch (HttpRequestException e) when (e.StatusCode == HttpStatusCode.NotFound)
{
Expand Down
88 changes: 88 additions & 0 deletions godot-mono-decomp/GodotMonoDecomp/FixSwitchExpressionCasts.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
using ICSharpCode.Decompiler.CSharp;
using ICSharpCode.Decompiler.CSharp.Syntax;
using ICSharpCode.Decompiler.CSharp.Transforms;
using ICSharpCode.Decompiler.TypeSystem;

namespace GodotMonoDecomp;

/// <summary>
/// Intended to fix switch expressions that do not have enough context to determine the best type as a result of a parent member reference expression.
/// </summary>
/// <example>
/// e.g:
/// ```c#
/// public Animal GetAnimal(AnimalType animalType)
/// {
/// return (animalType switch
/// {
/// // at least one of these should have a cast to 'Animal' or the member reference expression below will cause the switch expression to fail to compile.
/// AnimalType.Dog => new Dog(),
/// AnimalType.Fish => new Fish(),
/// _ => throw new ArgumentException("Invalid animal type")
/// }).ToValidated();
/// }
/// ```
/// </example>
public class FixSwitchExpressionCasts : DepthFirstAstVisitor, IAstTransform
{
private TransformContext? context;

public void Run(AstNode rootNode, TransformContext context)
{
this.context = context;
rootNode.AcceptVisitor(this);
}

private static bool IsCastableSwitchSectionExpressionBody(Expression body)
{
return body is not PrimitiveExpression && body is not NullReferenceExpression && body is not ThrowExpression;
}

public override void VisitSwitchExpression(SwitchExpression switchExpr)
{
if (context?.TypeSystemAstBuilder is null || switchExpr.Parent is not MemberReferenceExpression)
{
base.VisitSwitchExpression(switchExpr);
return;
}
var resolved = switchExpr.GetResolveResult();

if (!resolved.IsError && switchExpr.SwitchSections.Count > 1 && resolved.Type is not null)
{
if (!switchExpr.SwitchSections.Any(s => s.Body is CastExpression))
{
var resolvedTypeDefinition = resolved.Type.GetDefinition();
HashSet<ITypeDefinition> allDefs = [];
var mismatchedSections = switchExpr.SwitchSections.Where(s => {
if (!IsCastableSwitchSectionExpressionBody(s.Body))
{
return false;
}
var rr = s.Body.GetResolveResult();
if (rr is not null && !rr.IsError)
{
var def = rr.Type.GetDefinition();
if (def is not null)
{
allDefs.Add(def);
if (!def.Equals(resolvedTypeDefinition))
{
return true;
}
}

}
return false;
}).ToArray();
if (mismatchedSections.Length > 0 && allDefs.Count > 1) {
var first = mismatchedSections.FirstOrDefault()!;
var body = first.Body;
first.Body = null;
first.Body = new CastExpression(context.TypeSystemAstBuilder.ConvertType(resolved.Type), body);
}
}
}

base.VisitSwitchExpression(switchExpr);
}
}
Loading
Loading