File size: 6,269 Bytes
1ae2e8e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
using Microsoft.CodeAnalysis;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;

#pragma warning disable CS8618
public sealed class RojoProject
{
    [JsonPropertyName("name")]
    public string Name { get; set; }

    [JsonPropertyName("tree")]
    public InstanceDescription Tree { get; set; }

    [JsonPropertyName("servePort")]
    public int ServePort { get; set; } = 34872;

    [JsonPropertyName("servePlaceIds")]
    public List<ulong> ServePlaceIds { get; set; } = [];

    [JsonPropertyName("placeId")]
    public string? PlaceId { get; set; }

    [JsonPropertyName("gameId")]
    public string? GameId { get; set; }

    [JsonPropertyName("serveAddress")]
    public string? ServeAddress { get; set; }

    [JsonPropertyName("globIgnorePaths")]
    public List<string> GlobIgnorePaths { get; set; } = [];

    [JsonPropertyName("emitLegacyScripts")]
    public bool EmitLegacyScripts { get; set; } = true;

    public bool IsValid()
    {
        return !string.IsNullOrEmpty(Name) && Tree != null;
    }
}

public sealed class InstanceDescription
{
    [JsonPropertyName("$className")]
    public string? ClassName { get; set; }
    [JsonPropertyName("$path")]
    public string? Path { get; set; }
    [JsonPropertyName("$properties")]
    public Dictionary<string, object>? Properties { get; set; }
    [JsonPropertyName("$ignoreUnknownInstances")]
    public bool IgnoreUnknownInstances { get; set; } = true;
    public Dictionary<string, InstanceDescription> Instances { get; set; } = [];

    [JsonExtensionData]
    public IDictionary<string, JsonElement> AdditionalData { get; set; } = new Dictionary<string, JsonElement>();

    public void OnDeserialized()
    {
        foreach (var kvp in AdditionalData)
        {
            var childInstance = kvp.Value.Deserialize<InstanceDescription>()!;
            Instances[kvp.Key] = childInstance;
            childInstance.OnDeserialized();
        }
    }
}
#pragma warning restore CS8618

namespace RobloxCS
{
    public static class RojoReader
    {
        private static readonly List<string> _services = ["ReplicatedStorage", "ReplicatedFirst", "ServerStorage", "ServerScriptService", "StarterPlayer", "StarterPlayerScripts"]; // things that will be converted into game:GetService("XXX")
        private static readonly Dictionary<string, string> _instanceNameMap = new Dictionary<string, string>
        {
            { "StarterPlayer", "game:GetService(\"Players\").LocalPlayer" },
            { "StarterPlayerScripts", "PlayerScripts" }
        };

        public static RojoProject Read(string configPath)
        {
            var jsonContent = "";
            RojoProject? project = default;

            try
            {
                jsonContent = File.ReadAllText(configPath);
            }
            catch (Exception e)
            {
                FailToRead(configPath, e.Message);
            }

            try
            {
                project = JsonSerializer.Deserialize<RojoProject>(jsonContent);
            }
            catch (Exception e)
            {
                FailToRead(configPath, e.ToString());
            }

            if (project == null || !project.IsValid())
            {
                FailToRead(configPath, "Invalid Rojo project! Make sure it has all required fields ('name' and 'tree').");
            }

            UpdateChildInstances(project!.Tree);
            return project!;
        }

        public static string? FindProjectPath(string directoryPath, string projectName)
        {
            return Directory.GetFiles(directoryPath).FirstOrDefault(file => Path.GetFileName(file) == $"{projectName}.project.json");
        }

        public static string? ResolveInstancePath(RojoProject project, string filePath)
        {
            var path = TraverseInstanceTree(project.Tree, Utility.FixPathSep(filePath));
            return path == null ? null : FormatInstancePath(Utility.FixPathSep(path));
        }

        private static string? TraverseInstanceTree(InstanceDescription instance, string filePath)
        {
            var instancePath = instance.Path != null ? Utility.FixPathSep(instance.Path) : null;
            if (instancePath != null && filePath.StartsWith(instancePath))
            {
                var remainingPath = filePath.Substring(instancePath.Length + 1); // +1 to omit '/'
                return Path.ChangeExtension(remainingPath, null);
            }

            foreach (var childInstance in instance.Instances)
            {
                var result = TraverseInstanceTree(childInstance.Value, filePath);
                var leftName = childInstance.Key;
                if (_instanceNameMap.TryGetValue(leftName, out var mappedName))
                {
                    leftName = mappedName;
                }

                if (result != null)
                {
                    return $"{leftName}/{result}";
                }
            }

            return null;
        }

        private static string FormatInstancePath(string path)
        {
            var segments = path.Split('/');
            var formattedPath = new StringBuilder();
            foreach (var segment in segments)
            {
                var isServiceIdentifier = _services.Contains(segment);
                if (segment == segments.First())
                {
                    formattedPath.Append(isServiceIdentifier ? $"game:GetService(\"{segment}\")" : segment);
                }
                else
                {
                    formattedPath.Append(formattedPath.Length > 0 ? "[\"" : "");
                    formattedPath.Append(segment);
                    formattedPath.Append("\"]");
                }
            }

            return formattedPath.ToString();
        }

        private static void UpdateChildInstances(InstanceDescription instance)
        {
            instance.OnDeserialized();
            foreach (var childInstance in instance.Instances.Values)
            {
                UpdateChildInstances(childInstance);
            }
        }

        private static void FailToRead(string configPath, string message)
        {
            Logger.Error($"Failed to read {configPath}!\n{message}");
        }
    }
}