Correct UniqueId/FontFace implementations.

This commit is contained in:
Max 2022-10-12 21:19:43 -05:00
parent 583d69713d
commit 619b89d2a9
17 changed files with 3045 additions and 2687 deletions

View File

@ -1,7 +1,7 @@
using System; using System;
using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Collections.Generic;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text; using System.Text;
@ -17,43 +17,46 @@ namespace RobloxFiles.BinaryFormat
File = file; File = file;
} }
// Reads 'count * sizeof(T)' interleaved bytes and converts // Reads 'count * sizeof(T)' interleaved bytes
// them into an array of T[count] where each value in the public T[] ReadInterleaved<T>(int count, Func<byte[], int, T> transform) where T : struct
// array has been decoded by the provided 'decode' function.
public T[] ReadInterleaved<T>(int count, Func<byte[], int, T> decode) where T : struct
{ {
int bufferSize = Marshal.SizeOf<T>(); int sizeof_T = Marshal.SizeOf<T>();
byte[] interleaved = ReadBytes(count * bufferSize); int blobSize = count * sizeof_T;
T[] values = new T[count]; var blob = ReadBytes(blobSize);
var work = new byte[sizeof_T];
var values = new T[count];
for (int i = 0; i < count; i++) for (int offset = 0; offset < count; offset++)
{ {
long buffer = 0; for (int i = 0; i < sizeof_T; i++)
for (int column = 0; column < bufferSize; column++)
{ {
long block = interleaved[(column * count) + i]; int index = (i * count) + offset;
int shift = (bufferSize - column - 1) * 8; work[sizeof_T - i - 1] = blob[index];
buffer |= (block << shift);
} }
byte[] sequence = BitConverter.GetBytes(buffer); values[offset] = transform(work, 0);
values[i] = decode(sequence, 0);
} }
return values; return values;
} }
// Decodes an int from an interleaved buffer. // Rotates the sign bit of an int32 buffer.
private int DecodeInt(byte[] buffer, int startIndex) public int RotateInt32(byte[] buffer, int startIndex)
{ {
int value = BitConverter.ToInt32(buffer, startIndex); int value = BitConverter.ToInt32(buffer, startIndex);
return (value >> 1) ^ (-(value & 1)); return (int)((uint)value >> 1) ^ (-(value & 1));
} }
// Decodes a float from an interleaved buffer. // Rotates the sign bit of an int64 buffer.
private float DecodeFloat(byte[] buffer, int startIndex) public long RotateInt64(byte[] buffer, int startIndex)
{
long value = BitConverter.ToInt64(buffer, startIndex);
return (long)((ulong)value >> 1) ^ (-(value & 1));
}
// Rotates the sign bit of a float buffer.
public float RotateFloat(byte[] buffer, int startIndex)
{ {
uint u = BitConverter.ToUInt32(buffer, startIndex); uint u = BitConverter.ToUInt32(buffer, startIndex);
uint i = (u >> 1) | (u << 31); uint i = (u >> 1) | (u << 31);
@ -62,28 +65,10 @@ namespace RobloxFiles.BinaryFormat
return BitConverter.ToSingle(b, 0); return BitConverter.ToSingle(b, 0);
} }
// Reads an interleaved buffer of integers. // Reads and accumulates an interleaved int32 buffer.
public int[] ReadInts(int count)
{
return ReadInterleaved(count, DecodeInt);
}
// Reads an interleaved buffer of floats.
public float[] ReadFloats(int count)
{
return ReadInterleaved(count, DecodeFloat);
}
// Reads an interleaved buffer of unsigned integers.
public uint[] ReadUInts(int count)
{
return ReadInterleaved(count, BitConverter.ToUInt32);
}
// Reads and accumulates an interleaved buffer of integers.
public List<int> ReadInstanceIds(int count) public List<int> ReadInstanceIds(int count)
{ {
int[] values = ReadInts(count); int[] values = ReadInterleaved(count, RotateInt32);
for (int i = 1; i < count; ++i) for (int i = 1; i < count; ++i)
values[i] += values[i - 1]; values[i] += values[i - 1];

View File

@ -103,15 +103,21 @@ namespace RobloxFiles.BinaryFormat
Marshal.FreeHGlobal(converter); Marshal.FreeHGlobal(converter);
} }
// Encodes an int for an interleaved buffer. // Rotates the sign bit of the provided int.
private static int EncodeInt(int value) public int RotateInt(int value)
{ {
return (value << 1) ^ (value >> 31); return (value << 1) ^ (value >> 31);
} }
// Encodes a float for an interleaved buffer. // Rotates the sign bit of the provided long.
private static float EncodeFloat(float value) public long RotateLong(long value)
{
return (value << 1) ^ (value >> 63);
}
// Rotates the sign bit of the provided float.
public float RotateFloat(float value)
{ {
byte[] buffer = BitConverter.GetBytes(value); byte[] buffer = BitConverter.GetBytes(value);
uint bits = BitConverter.ToUInt32(buffer, 0); uint bits = BitConverter.ToUInt32(buffer, 0);
@ -125,13 +131,19 @@ namespace RobloxFiles.BinaryFormat
// Writes an interleaved list of integers. // Writes an interleaved list of integers.
public void WriteInts(List<int> values) public void WriteInts(List<int> values)
{ {
WriteInterleaved(values, EncodeInt); WriteInterleaved(values, RotateInt);
}
// Writes an interleaved list of longs
public void WriteLongs(List<long> values)
{
WriteInterleaved(values, RotateLong);
} }
// Writes an interleaved list of floats // Writes an interleaved list of floats
public void WriteFloats(List<float> values) public void WriteFloats(List<float> values)
{ {
WriteInterleaved(values, EncodeFloat); WriteInterleaved(values, RotateFloat);
} }
// Accumulatively writes an interleaved array of integers. // Accumulatively writes an interleaved array of integers.

View File

@ -78,8 +78,8 @@ namespace RobloxFiles.BinaryFormat.Chunks
} }
// Setup some short-hand functions for actions used during the read procedure. // Setup some short-hand functions for actions used during the read procedure.
var readInts = new Func<int[]>(() => reader.ReadInts(instCount)); var readInts = new Func<int[]>(() => reader.ReadInterleaved(instCount, reader.RotateInt32));
var readFloats = new Func<float[]>(() => reader.ReadFloats(instCount)); var readFloats = new Func<float[]>(() => reader.ReadInterleaved(instCount, reader.RotateFloat));
var readProperties = new Action<Func<int, object>>(read => var readProperties = new Action<Func<int, object>>(read =>
{ {
@ -434,7 +434,7 @@ namespace RobloxFiles.BinaryFormat.Chunks
} }
case PropertyType.Enum: case PropertyType.Enum:
{ {
uint[] enums = reader.ReadUInts(instCount); uint[] enums = reader.ReadInterleaved(instCount, BitConverter.ToUInt32);
readProperties(i => readProperties(i =>
{ {
@ -619,22 +619,17 @@ namespace RobloxFiles.BinaryFormat.Chunks
} }
case PropertyType.Int64: case PropertyType.Int64:
{ {
long[] longs = reader.ReadInterleaved(instCount, (buffer, start) => var values = reader.ReadInterleaved(instCount, reader.RotateInt64);
{ readProperties(i => values[i]);
long result = BitConverter.ToInt64(buffer, start);
return (long)((ulong)result >> 1) ^ (-(result & 1));
});
readProperties(i => longs[i]);
break; break;
} }
case PropertyType.SharedString: case PropertyType.SharedString:
{ {
uint[] SharedKeys = reader.ReadUInts(instCount); var keys = reader.ReadInterleaved(instCount, BitConverter.ToUInt32);
readProperties(i => readProperties(i =>
{ {
uint key = SharedKeys[i]; uint key = keys[i];
return File.SharedStrings[key]; return File.SharedStrings[key];
}); });
@ -654,14 +649,16 @@ namespace RobloxFiles.BinaryFormat.Chunks
} }
case PropertyType.UniqueId: case PropertyType.UniqueId:
{ {
readProperties(i => var uniqueIds = reader.ReadInterleaved(instCount, (buffer, offset) =>
{ {
var index = reader.ReadUInt32(); var random = reader.RotateInt64(buffer, 0);
var time = reader.ReadUInt32(); var time = BitConverter.ToUInt32(buffer, 8);
var random = reader.ReadUInt64(); var index = BitConverter.ToUInt32(buffer, 12);
return new UniqueId(index, time, random);
return new UniqueId(random, time, index);
}); });
readProperties(i => uniqueIds[i]);
break; break;
} }
case PropertyType.FontFace: case PropertyType.FontFace:
@ -670,13 +667,11 @@ namespace RobloxFiles.BinaryFormat.Chunks
{ {
string family = reader.ReadString(); string family = reader.ReadString();
if (family.EndsWith(".otf") || family.EndsWith(".ttf"))
return new FontFace(family);
var weight = (FontWeight)reader.ReadUInt16(); var weight = (FontWeight)reader.ReadUInt16();
var style = (FontStyle)reader.ReadByte(); var style = (FontStyle)reader.ReadByte();
var cachedFaceId = reader.ReadString();
return new FontFace(family, weight, style); return new FontFace(family, weight, style, cachedFaceId);
}); });
break; break;
@ -1233,12 +1228,7 @@ namespace RobloxFiles.BinaryFormat.Chunks
longs.Add(value); longs.Add(value);
}); });
writer.WriteInterleaved(longs, value => writer.WriteLongs(longs);
{
// Move the sign bit to the front.
return (value << 1) ^ (value >> 63);
});
break; break;
} }
case PropertyType.SharedString: case PropertyType.SharedString:
@ -1293,14 +1283,16 @@ namespace RobloxFiles.BinaryFormat.Chunks
} }
case PropertyType.UniqueId: case PropertyType.UniqueId:
{ {
var uniqueIds = new List<UniqueId>();
props.ForEach(prop => props.ForEach(prop =>
{ {
var uniqueId = prop.CastValue<UniqueId>(); var uniqueId = prop.CastValue<UniqueId>();
writer.Write(uniqueId.Index); var rotated = writer.RotateLong(uniqueId.Random);
writer.Write(uniqueId.Time); uniqueIds.Add(new UniqueId(rotated, uniqueId.Time, uniqueId.Index));
writer.Write(uniqueId.Random);
}); });
writer.WriteInterleaved(uniqueIds);
break; break;
} }
case PropertyType.FontFace: case PropertyType.FontFace:
@ -1310,16 +1302,16 @@ namespace RobloxFiles.BinaryFormat.Chunks
var font = prop.CastValue<FontFace>(); var font = prop.CastValue<FontFace>();
string family = font.Family; string family = font.Family;
writer.WriteString(font.Family); writer.WriteString(family);
if (family.EndsWith(".otf") || family.EndsWith(".ttf"))
return;
var weight = (ushort)font.Weight; var weight = (ushort)font.Weight;
writer.Write(weight); writer.Write(weight);
var style = (byte)font.Style; var style = (byte)font.Style;
writer.Write(style); writer.Write(style);
var cachedFaceId = font.CachedFaceId;
writer.WriteString(cachedFaceId);
}); });
break; break;

View File

@ -91,20 +91,14 @@ namespace RobloxFiles.DataTypes
public CFrame(Vector3 eye, Vector3 look) public CFrame(Vector3 eye, Vector3 look)
{ {
Vector3 zAxis = (eye - look).Unit, Vector3 zAxis = (eye - look).Unit,
xAxis = Vector3.Up.Cross(zAxis), xAxis = Vector3.yAxis.Cross(zAxis),
yAxis = zAxis.Cross(xAxis); yAxis = zAxis.Cross(xAxis);
if (xAxis.Magnitude == 0) if (xAxis.Magnitude == 0)
{ {
xAxis = Vector3.z; xAxis = Vector3.zAxis;
yAxis = Vector3.x; yAxis = Vector3.xAxis;
zAxis = Vector3.y; zAxis = Vector3.yAxis;
if (zAxis.Y < 0)
{
xAxis = -xAxis;
zAxis = -zAxis;
}
} }
m11 = xAxis.X; m12 = yAxis.X; m13 = zAxis.X; m14 = eye.X; m11 = xAxis.X; m12 = yAxis.X; m13 = zAxis.X; m14 = eye.X;
@ -299,18 +293,18 @@ namespace RobloxFiles.DataTypes
public static CFrame FromAxisAngle(Vector3 axis, float theta) public static CFrame FromAxisAngle(Vector3 axis, float theta)
{ {
Vector3 r = VectorAxisAngle(axis, Vector3.x, theta), Vector3 r = VectorAxisAngle(axis, Vector3.xAxis, theta),
u = VectorAxisAngle(axis, Vector3.y, theta), u = VectorAxisAngle(axis, Vector3.yAxis, theta),
b = VectorAxisAngle(axis, Vector3.z, theta); b = VectorAxisAngle(axis, Vector3.zAxis, theta);
return new CFrame(0, 0, 0, r.X, u.X, b.X, r.Y, u.Y, b.Y, r.Z, u.Z, b.Z); return new CFrame(0, 0, 0, r.X, u.X, b.X, r.Y, u.Y, b.Y, r.Z, u.Z, b.Z);
} }
public static CFrame FromEulerAnglesXYZ(float x, float y, float z) public static CFrame FromEulerAnglesXYZ(float x, float y, float z)
{ {
CFrame cfx = FromAxisAngle(Vector3.x, x), CFrame cfx = FromAxisAngle(Vector3.xAxis, x),
cfy = FromAxisAngle(Vector3.y, y), cfy = FromAxisAngle(Vector3.yAxis, y),
cfz = FromAxisAngle(Vector3.z, z); cfz = FromAxisAngle(Vector3.zAxis, z);
return cfx * cfy * cfz; return cfx * cfy * cfz;
} }
@ -410,9 +404,9 @@ namespace RobloxFiles.DataTypes
{ {
var tests = new float[3] var tests = new float[3]
{ {
XVector.Dot(Vector3.x), XVector.Dot(Vector3.xAxis),
YVector.Dot(Vector3.y), YVector.Dot(Vector3.yAxis),
ZVector.Dot(Vector3.z) ZVector.Dot(Vector3.zAxis)
}; };
foreach (var test in tests) foreach (var test in tests)

View File

@ -2,8 +2,9 @@
namespace RobloxFiles.DataTypes namespace RobloxFiles.DataTypes
{ {
// Implementation of Roblox's Font datatype. // Implementation of Roblox's FontFace datatype.
// Renamed to FontFace to avoid disambiguation with System.Font and the Font enum. // In Luau this type is named Font, but we avoid that name
// to avoid ambiguity with System.Font and Roblox's Font enum.
public class FontFace public class FontFace
{ {
@ -11,8 +12,16 @@ namespace RobloxFiles.DataTypes
public readonly FontWeight Weight = FontWeight.Regular; public readonly FontWeight Weight = FontWeight.Regular;
public readonly FontStyle Style = FontStyle.Normal; public readonly FontStyle Style = FontStyle.Normal;
public FontFace(Content family, FontWeight weight = FontWeight.Regular, FontStyle style = FontStyle.Normal) // Roblox caches the asset of the font's face to make it
// load faster. At runtime both the Family and the CachedFaceId
// are loaded in parallel. If the CachedFaceId doesn't match with
// the family file's face asset, then the correct one will be loaded late.
// Setting this is not required, it's just a throughput optimization.
public Content CachedFaceId { get; set; } = "";
public FontFace(Content family, FontWeight weight = FontWeight.Regular, FontStyle style = FontStyle.Normal, string cachedFaceId = "")
{ {
CachedFaceId = cachedFaceId;
Family = family; Family = family;
Weight = weight; Weight = weight;
Style = style; Style = style;

View File

@ -1,16 +1,27 @@
namespace RobloxFiles.DataTypes using System;
namespace RobloxFiles.DataTypes
{ {
public struct UniqueId public struct UniqueId
{ {
public readonly uint Time; public readonly uint Time;
public readonly uint Index; public readonly uint Index;
public readonly ulong Random; public readonly long Random;
public UniqueId(uint time, uint index, ulong random) public UniqueId(long random, uint time, uint index)
{ {
Time = time; Time = time;
Index = index; Index = index;
Random = random; Random = random;
} }
public override string ToString()
{
string random = Random.ToString("x2").PadLeft(16, '0');
string index = Index.ToString("x2").PadLeft(8, '0');
string time = Time.ToString("x2").PadLeft(8, '0');
return $"{random}{time}{index}";
}
} }
} }

View File

@ -98,13 +98,13 @@ namespace RobloxFiles.DataTypes
public static readonly Vector3 zero = new Vector3(0, 0, 0); public static readonly Vector3 zero = new Vector3(0, 0, 0);
public static readonly Vector3 one = new Vector3(1, 1, 1); public static readonly Vector3 one = new Vector3(1, 1, 1);
public static readonly Vector3 x = new Vector3(1, 0, 0); public static readonly Vector3 xAxis = new Vector3(1, 0, 0);
public static readonly Vector3 y = new Vector3(0, 1, 0); public static readonly Vector3 yAxis = new Vector3(0, 1, 0);
public static readonly Vector3 z = new Vector3(0, 0, 1); public static readonly Vector3 zAxis = new Vector3(0, 0, 1);
public static Vector3 Right => x; public static Vector3 Right => xAxis;
public static Vector3 Up => y; public static Vector3 Up => yAxis;
public static Vector3 Back => z; public static Vector3 Back => zAxis;
public float Dot(Vector3 other) public float Dot(Vector3 other)
{ {

View File

@ -5,10 +5,10 @@ export type FormatFunc = (any) -> string
export type Format = { [string]: FormatFunc } export type Format = { [string]: FormatFunc }
export type IEnum = { GetEnumItems: (IEnum) -> {EnumItem} } export type IEnum = { GetEnumItems: (IEnum) -> {EnumItem} }
local function flags<T>(flags: T, enum: IEnum): string local function flags(flagType: any, enum: Enum): string
local value = 0 local value = 0
for i, item in enum:GetEnumItems() do for i, item: EnumItem in enum:GetEnumItems() do
if (flags :: any)[item.Name] then if (flags :: any)[item.Name] then
value += (2 ^ item.Value) value += (2 ^ item.Value)
end end

View File

@ -8,72 +8,68 @@ local classes = {}
local outStream = "" local outStream = ""
local stackLevel = 0 local stackLevel = 0
local singletons = local singletons = {
{ Speaker = Instance.new("Sound"), -- close enough
Speaker = Instance.new("Sound"); -- close enough Terrain = workspace:WaitForChild("Terrain", 1000),
Terrain = workspace:WaitForChild("Terrain", 1000); ParabolaAdornment = Instance.new("BoxHandleAdornment"), -- close enough
ParabolaAdornment = Instance.new("BoxHandleAdornment"); -- close enough StarterPlayerScripts = StarterPlayer:WaitForChild("StarterPlayerScripts"),
StarterPlayerScripts = StarterPlayer:WaitForChild("StarterPlayerScripts"); StarterCharacterScripts = StarterPlayer:WaitForChild("StarterCharacterScripts"),
StarterCharacterScripts = StarterPlayer:WaitForChild("StarterCharacterScripts"); ChatWindowConfiguration = TextChatService:WaitForChild("ChatWindowConfiguration", 10),
ChatWindowConfiguration = TextChatService:WaitForChild("ChatWindowConfiguration", 10); ChatInputBarConfiguration = TextChatService:WaitForChild("ChatInputBarConfiguration", 10),
ChatInputBarConfiguration = TextChatService:WaitForChild("ChatInputBarConfiguration", 10);
} }
local exceptionClasses = local exceptionClasses = {
{ PackageLink = true,
PackageLink = true; ScriptDebugger = true,
ScriptDebugger = true; ChatWindowConfiguration = true,
ChatWindowConfiguration = true; ChatInputBarConfiguration = true,
ChatInputBarConfiguration = true;
} }
local numberTypes = local numberTypes = {
{ int = true,
int = true; long = true,
long = true; int64 = true,
int64 = true; float = true,
float = true; double = true,
double = true;
} }
local stringTypes = local stringTypes = {
{ string = true,
string = true; Content = true,
Content = true; BinaryString = true,
BinaryString = true; ProtectedString = true,
ProtectedString = true;
} }
local isCoreScript = pcall(function () local isCoreScript = pcall(function()
local restricted = game:GetService("RobloxPluginGuiService") local restricted = game:GetService("RobloxPluginGuiService")
return tostring(restricted) return tostring(restricted)
end) end)
local function write(formatString, ...) local function write(formatString, ...)
local tabs = string.rep(' ', stackLevel * 4) local tabs = string.rep(" ", stackLevel * 4)
local fmt = formatString or "" local fmt = formatString or ""
local value = tabs .. fmt:format(...) local value = tabs .. fmt:format(...)
outStream = outStream .. value outStream = outStream .. value
end end
local function writeLine(formatString, ...) local function writeLine(formatString, ...)
if not formatString then if not formatString then
outStream = outStream .. '\n' outStream = outStream .. "\n"
return return
end end
write(formatString .. '\n', ...) write(formatString .. "\n", ...)
end end
local function openStack() local function openStack()
writeLine('{') writeLine("{")
stackLevel += 1 stackLevel += 1
end end
local function closeStack() local function closeStack()
stackLevel -= 1 stackLevel -= 1
writeLine('}') writeLine("}")
end end
local function clearStream() local function clearStream()
@ -84,7 +80,7 @@ end
local function exportStream(label) local function exportStream(label)
local results = outStream:gsub("\n\n\n", "\n\n") local results = outStream:gsub("\n\n\n", "\n\n")
local export local export
if plugin then if plugin then
export = Instance.new("Script") export = Instance.new("Script")
export.Archivable = false export.Archivable = false
@ -92,9 +88,9 @@ local function exportStream(label)
export.Name = label export.Name = label
export.Parent = workspace export.Parent = workspace
Selection:Add{export} Selection:Add({ export })
end end
if isCoreScript then if isCoreScript then
StudioService:CopyToClipboard(results) StudioService:CopyToClipboard(results)
elseif not plugin then elseif not plugin then
@ -108,101 +104,99 @@ end
local function getTags(object) local function getTags(object)
local tags = {} local tags = {}
if object.Tags ~= nil then if object.Tags ~= nil then
for _,tag in pairs(object.Tags) do for _, tag in pairs(object.Tags) do
tags[tag] = true tags[tag] = true
end end
end end
if object.Name == "Terrain" then if object.Name == "Terrain" then
tags.NotCreatable = nil tags.NotCreatable = nil
end end
return tags return tags
end end
local function upcastInheritance(class, root) local function upcastInheritance(class, root)
local superClass = classes[class.Superclass] local superClass = classes[class.Superclass]
if not superClass then if not superClass then
return return
end end
if not root then if not root then
root = class root = class
end end
if not superClass.Inherited then if not superClass.Inherited then
superClass.Inherited = root superClass.Inherited = root
end end
upcastInheritance(superClass, root) upcastInheritance(superClass, root)
end end
local function canCreateClass(class) local function canCreateClass(class)
local tags = getTags(class) local tags = getTags(class)
local canCreate = true local canCreate = true
if tags.NotCreatable then if tags.NotCreatable then
canCreate = false canCreate = false
end end
if tags.Service then if tags.Service then
canCreate = true canCreate = true
end end
if tags.Settings then if tags.Settings then
canCreate = false canCreate = false
end end
if singletons[class.Name] then if singletons[class.Name] then
canCreate = true canCreate = true
end end
return canCreate return canCreate
end end
local function collectProperties(class) local function collectProperties(class)
local propMap = {} local propMap = {}
for _,member in ipairs(class.Members) do for _, member in ipairs(class.Members) do
if member.MemberType == "Property" then if member.MemberType == "Property" then
local propName = member.Name local propName = member.Name
propMap[propName] = member propMap[propName] = member
end end
end end
return propMap return propMap
end end
local function createProperty(propName, propType) local function createProperty(propName, propType)
local category = "DataType"; local category = "DataType"
local name = propType local name = propType
if propType:find(':') then if propType:find(":") then
local data = string.split(propType, ':') local data = string.split(propType, ":")
category = data[1] category = data[1]
name = data[2] name = data[2]
end end
return return
{ {
Name = propName; Name = propName,
Serialization = Serialization = {
{ CanSave = true,
CanSave = true; CanLoad = true,
CanLoad = true; },
};
ValueType = {
ValueType = Category = category,
{ Name = name,
Category = category; },
Name = name;
}; Security = "None",
Security = "None";
} }
end end
@ -214,28 +208,27 @@ local formatting: Format = require(script.Formatting)
type FormatFunc = formatting.FormatFunc type FormatFunc = formatting.FormatFunc
type Format = formatting.Format type Format = formatting.Format
local formatLinks = local formatLinks = {
{ ["int"] = "Int",
["int"] = "Int"; ["long"] = "Int",
["long"] = "Int";
["float"] = "Float",
["float"] = "Float"; ["byte[]"] = "Bytes",
["byte[]"] = "Bytes"; ["double"] = "Double",
["double"] = "Double"; ["boolean"] = "Bool",
["boolean"] = "Bool";
["string"] = "String",
["string"] = "String"; ["Content"] = "String",
["Content"] = "String";
["Color3uint8"] = "Color3",
["Color3uint8"] = "Color3"; ["ProtectedString"] = "String",
["ProtectedString"] = "String";
} }
local function getFormatFunction(valueType: string): FormatFunc local function getFormatFunction(valueType: string): FormatFunc
if not formatting[valueType] then if not formatting[valueType] then
valueType = formatLinks[valueType] valueType = formatLinks[valueType]
end end
return formatting[valueType] or formatting.Null return formatting[valueType] or formatting.Null
end end
@ -250,7 +243,7 @@ function patchIndex:__index(key)
if not rawget(self, key) then if not rawget(self, key) then
rawset(self, key, {}) rawset(self, key, {})
end end
return self[key] return self[key]
end end
@ -273,71 +266,70 @@ if plugin then
button = toolbar:CreateButton( button = toolbar:CreateButton(
"Dump API", "Dump API",
"Generates a C# dump of Roblox's Class/Enum API.", "Generates a C# dump of Roblox's Class/Enum API.",
"rbxasset://textures/Icon_Stream_Off@2x.png" "rbxasset://textures/Icon_Stream_Off@2x.png"
) )
button.ClickableWhenViewportHidden = true button.ClickableWhenViewportHidden = true
end end
local function getAsync(url) local function getAsync(url)
local enabled local enabled
if isCoreScript then if isCoreScript then
enabled = HttpService:GetHttpEnabled() enabled = HttpService:GetHttpEnabled()
HttpService:SetHttpEnabled(true) HttpService:SetHttpEnabled(true)
end end
local result = HttpService:GetAsync(url) local result = HttpService:GetAsync(url)
if isCoreScript then if isCoreScript then
HttpService:SetHttpEnabled(enabled) HttpService:SetHttpEnabled(enabled)
end end
return result return result
end end
local function generateClasses() local function generateClasses()
local env = getfenv() local env = getfenv()
local version = getAsync(baseUrl .. "version.txt") local version = getAsync(baseUrl .. "version.txt")
local apiDump = getAsync(baseUrl .. "API-Dump.json") local apiDump = getAsync(baseUrl .. "API-Dump.json")
apiDump = HttpService:JSONDecode(apiDump) apiDump = HttpService:JSONDecode(apiDump)
local classNames = {} local classNames = {}
classes = {} classes = {}
local enumMap = local enumMap = {
{ Axis = true,
Axis = true; FontSize = true,
FontSize = true; FontStyle = true,
FontStyle = true; FontWeight = true,
FontWeight = true;
} }
for _,class in ipairs(apiDump.Classes) do for _, class in ipairs(apiDump.Classes) do
local className = class.Name local className = class.Name
local superClass = classes[class.Superclass] local superClass = classes[class.Superclass]
if singletons[className] then if singletons[className] then
class.Singleton = true class.Singleton = true
class.Object = singletons[className] class.Object = singletons[className]
end end
if superClass and canCreateClass(class) then if superClass and canCreateClass(class) then
local classTags = getTags(class) local classTags = getTags(class)
if classTags.Service then if classTags.Service then
pcall(function () pcall(function()
if not className:find("Network") then if not className:find("Network") then
class.Object = game:GetService(className) class.Object = game:GetService(className)
end end
end) end)
elseif not classTags.NotCreatable then elseif not classTags.NotCreatable then
pcall(function () pcall(function()
local dumpFolder = game:FindFirstChild("DumpFolder") local dumpFolder = game:FindFirstChild("DumpFolder")
class.Object = Instance.new(className) class.Object = Instance.new(className)
if dumpFolder then if dumpFolder then
local old = dumpFolder:FindFirstChildOfClass(className) local old = dumpFolder:FindFirstChildOfClass(className)
@ -350,50 +342,50 @@ local function generateClasses()
end end
end) end)
end end
upcastInheritance(class) upcastInheritance(class)
end end
classes[className] = class classes[className] = class
table.insert(classNames, className) table.insert(classNames, className)
end end
outStream = "" outStream = ""
writeLine("// Auto-generated list of creatable Roblox classes.") writeLine("// Auto-generated list of creatable Roblox classes.")
writeLine("// Updated as of %s", version) writeLine("// Updated as of %s", version)
writeLine() writeLine()
writeLine("using System;") writeLine("using System;")
writeLine() writeLine()
writeLine("using RobloxFiles.DataTypes;") writeLine("using RobloxFiles.DataTypes;")
writeLine("using RobloxFiles.Enums;") writeLine("using RobloxFiles.Enums;")
writeLine("using RobloxFiles.Utility;") writeLine("using RobloxFiles.Utility;")
writeLine() writeLine()
writeLine("#pragma warning disable IDE1006 // Naming Styles") writeLine("#pragma warning disable IDE1006 // Naming Styles")
writeLine() writeLine()
writeLine("namespace RobloxFiles") writeLine("namespace RobloxFiles")
openStack() openStack()
for i, className in ipairs(classNames) do for i, className in ipairs(classNames) do
local class = classes[className] local class = classes[className]
local classTags = getTags(class) local classTags = getTags(class)
local registerClass = canCreateClass(class) local registerClass = canCreateClass(class)
local object = class.Object local object = class.Object
if class.Inherited then if class.Inherited then
registerClass = true registerClass = true
end end
if class.Name == "Instance" or class.Name == "Studio" then if class.Name == "Instance" or class.Name == "Studio" then
registerClass = false registerClass = false
end end
local noSecurityCheck = pcall(function () local noSecurityCheck = pcall(function()
if not classTags.Service then if not classTags.Service then
return tostring(object) return tostring(object)
end end
@ -402,7 +394,7 @@ local function generateClasses()
if not noSecurityCheck then if not noSecurityCheck then
object = nil object = nil
end end
if not object then if not object then
if class.Inherited then if class.Inherited then
object = class.Inherited.Object object = class.Inherited.Object
@ -412,40 +404,40 @@ local function generateClasses()
registerClass = false registerClass = false
end end
end end
if exceptionClasses[class.Name] then if exceptionClasses[class.Name] then
registerClass = true registerClass = true
end end
if registerClass then if registerClass then
local objectType local objectType
if classTags.NotCreatable and class.Inherited and not class.Singleton then if classTags.NotCreatable and class.Inherited and not class.Singleton then
objectType = "abstract class" objectType = "abstract class"
else else
objectType = "class" objectType = "class"
end end
writeLine("public %s %s : %s", objectType, className, class.Superclass) writeLine("public %s %s : %s", objectType, className, class.Superclass)
openStack() openStack()
local classPatches = getPatches(className) local classPatches = getPatches(className)
local redirectProps = classPatches.Redirect local redirectProps = classPatches.Redirect
local propMap = collectProperties(class) local propMap = collectProperties(class)
local propNames = {} local propNames = {}
for _,propName in pairs(classPatches.Remove) do for _, propName in pairs(classPatches.Remove) do
propMap[propName] = nil propMap[propName] = nil
end end
for propName in pairs(propMap) do for propName in pairs(propMap) do
table.insert(propNames, propName) table.insert(propNames, propName)
end end
for propName, propType in pairs(classPatches.Add) do for propName, propType in pairs(classPatches.Add) do
local prop = propMap[propName] local prop = propMap[propName]
if prop then if prop then
local serial = prop.Serialization local serial = prop.Serialization
serial.CanSave = true serial.CanSave = true
@ -455,13 +447,13 @@ local function generateClasses()
table.insert(propNames, propName) table.insert(propNames, propName)
end end
end end
local firstLine = true local firstLine = true
class.PropertyMap = propMap class.PropertyMap = propMap
local ancestor = class local ancestor = class
local diffProps = {} local diffProps = {}
while object do while object do
ancestor = classes[ancestor.Superclass] ancestor = classes[ancestor.Superclass]
@ -471,27 +463,25 @@ local function generateClasses()
local inheritProps = ancestor.PropertyMap local inheritProps = ancestor.PropertyMap
local inherited = ancestor.Inherited local inherited = ancestor.Inherited
local baseObject = if inherited local baseObject = if inherited then inherited.Object else nil
then inherited.Object
else nil
if inheritProps and baseObject then if inheritProps and baseObject then
for name, prop in pairs(inheritProps) do for name, prop in pairs(inheritProps) do
local tags = getTags(prop) local tags = getTags(prop)
if tags.ReadOnly then if tags.ReadOnly then
continue continue
end end
local gotPropValue, propValue = pcall(function () local gotPropValue, propValue = pcall(function()
return object[name] return object[name]
end) end)
local gotBaseValue, baseValue = pcall(function () local gotBaseValue, baseValue = pcall(function()
return baseObject[name] return baseObject[name]
end) end)
if gotBaseValue and gotPropValue then if gotBaseValue and gotPropValue then
if propValue ~= baseValue then if propValue ~= baseValue then
diffProps[name] = propValue diffProps[name] = propValue
@ -500,7 +490,7 @@ local function generateClasses()
end end
end end
end end
if classTags.Service or next(diffProps) then if classTags.Service or next(diffProps) then
local headerFormat = "public %s()" local headerFormat = "public %s()"
@ -510,10 +500,10 @@ local function generateClasses()
writeLine(headerFormat, className) writeLine(headerFormat, className)
openStack() openStack()
if classTags.Service then if classTags.Service then
writeLine("IsService = true;") writeLine("IsService = true;")
if next(diffProps) then if next(diffProps) then
writeLine() writeLine()
end end
@ -532,7 +522,7 @@ local function generateClasses()
local value = diffProps[name] local value = diffProps[name]
local valueType = typeof(value) local valueType = typeof(value)
local formatFunc = getFormatFunction(valueType) local formatFunc = getFormatFunction(valueType)
if formatFunc ~= formatting.Null then if formatFunc ~= formatting.Null then
local result = formatFunc(value) local result = formatFunc(value)
@ -544,37 +534,37 @@ local function generateClasses()
end end
end end
end end
closeStack() closeStack()
end end
table.sort(propNames) table.sort(propNames)
for j, propName in ipairs(propNames) do for j, propName in ipairs(propNames) do
local prop = propMap[propName] local prop = propMap[propName]
local propTags = getTags(prop) local propTags = getTags(prop)
local serial = prop.Serialization local serial = prop.Serialization
local typeData = prop.ValueType local typeData = prop.ValueType
local category = typeData.Category local category = typeData.Category
local valueType = typeData.Name local valueType = typeData.Name
local redirect = redirectProps[propName] local redirect = redirectProps[propName]
local couldSave = (serial.CanSave or propTags.Deprecated or redirect) local couldSave = (serial.CanSave or propTags.Deprecated or redirect)
if serial.CanLoad and couldSave then if serial.CanLoad and couldSave then
if firstLine and (classTags.Service or next(diffProps)) then if firstLine and (classTags.Service or next(diffProps)) then
writeLine() writeLine()
end end
local name = propName local name = propName
local default = "" local default = ""
if propName == className then if propName == className then
name = name .. '_' name = name .. "_"
end end
if valueType == "int64" then if valueType == "int64" then
valueType = "long" valueType = "long"
elseif valueType == "BinaryString" then elseif valueType == "BinaryString" then
@ -582,40 +572,40 @@ local function generateClasses()
elseif valueType == "Font" and category ~= "Enum" then elseif valueType == "Font" and category ~= "Enum" then
valueType = "FontFace" valueType = "FontFace"
end end
local first = name:sub(1, 1) local first = name:sub(1, 1)
if first == first:lower() then if first == first:lower() then
local pascal = first:upper() .. name:sub(2) local pascal = first:upper() .. name:sub(2)
if propMap[pascal] ~= nil and propTags.Deprecated then if propMap[pascal] ~= nil and propTags.Deprecated then
redirect = pascal redirect = pascal
end end
end end
if redirect then if redirect then
local get, set, flag local get, set, flag
if typeof(redirect) == "string" then if typeof(redirect) == "string" then
get = redirect get = redirect
set = redirect .. " = value" set = redirect .. " = value"
if redirect == "value" then if redirect == "value" then
set = "this." .. set set = "this." .. set
end end
else else
get = redirect.Get get = redirect.Get
set = redirect.Set set = redirect.Set
flag = redirect.Flag flag = redirect.Flag
end end
if not firstLine and set then if not firstLine and set then
writeLine() writeLine()
end end
if propTags.Deprecated then if propTags.Deprecated then
writeLine("[Obsolete]") writeLine("[Obsolete]")
end end
if set then if set then
if flag then if flag then
writeLine("public %s %s %s", flag, valueType, name) writeLine("public %s %s %s", flag, valueType, name)
@ -626,9 +616,9 @@ local function generateClasses()
openStack() openStack()
writeLine("get => %s;", get) writeLine("get => %s;", get)
if set:find('\n') then if set:find("\n") then
writeLine() writeLine()
writeLine("set") writeLine("set")
openStack() openStack()
@ -645,23 +635,23 @@ local function generateClasses()
else else
writeLine("public %s %s => %s;", valueType, name, get) writeLine("public %s %s => %s;", valueType, name, get)
end end
if j ~= #propNames and set then if j ~= #propNames and set then
writeLine() writeLine()
end end
else else
local value = classPatches.Defaults[propName] local value = classPatches.Defaults[propName]
local gotValue = (value ~= nil) local gotValue = (value ~= nil)
if not gotValue then if not gotValue then
gotValue, value = pcall(function () gotValue, value = pcall(function()
return object[propName] return object[propName]
end) end)
end end
if not gotValue and category ~= "Class" then if not gotValue and category ~= "Class" then
-- Fallback to implicit defaults -- Fallback to implicit defaults
if numberTypes[valueType] then if numberTypes[valueType] then
value = 0 value = 0
gotValue = true gotValue = true
@ -673,9 +663,9 @@ local function generateClasses()
gotValue = true gotValue = true
elseif category == "DataType" then elseif category == "DataType" then
local DataType = env[valueType] local DataType = env[valueType]
if DataType and typeof(DataType) == "table" and not rawget(env, valueType) then if DataType and typeof(DataType) == "table" and not rawget(env, valueType) then
pcall(function () pcall(function()
value = DataType.new() value = DataType.new()
gotValue = true gotValue = true
end) end)
@ -685,7 +675,7 @@ local function generateClasses()
local lowestId = math.huge local lowestId = math.huge
local lowest local lowest
for _,item in pairs(enum:GetEnumItems()) do for _, item in pairs(enum:GetEnumItems()) do
local itemValue = item.Value local itemValue = item.Value
if itemValue < lowestId then if itemValue < lowestId then
@ -706,21 +696,27 @@ local function generateClasses()
if gotValue then if gotValue then
warn(src, "Fell back to implicit value for property:", id) warn(src, "Fell back to implicit value for property:", id)
else else
warn(src, "!! Could not figure out default value for property:", id, "value error was:", value) warn(
src,
"!! Could not figure out default value for property:",
id,
"value error was:",
value
)
end end
end end
if gotValue then if gotValue then
local formatKey = if category == "Enum" then "Enum" else valueType local formatKey = if category == "Enum" then "Enum" else valueType
local formatFunc = getFormatFunction(formatKey) local formatFunc = getFormatFunction(formatKey)
if formatFunc == formatting.Null then if formatFunc == formatting.Null then
local literal = typeof(value) local literal = typeof(value)
formatFunc = getFormatFunction(literal) formatFunc = getFormatFunction(literal)
end end
local result local result
if formatFunc then if formatFunc then
if typeof(formatFunc) == "string" then if typeof(formatFunc) == "string" then
result = formatFunc result = formatFunc
@ -732,7 +728,7 @@ local function generateClasses()
if result == "" then if result == "" then
result = nil result = nil
end end
if result ~= nil then if result ~= nil then
default = " = " .. result default = " = " .. result
end end
@ -747,34 +743,34 @@ local function generateClasses()
-- .____. -- .____.
propTags.Deprecated = false propTags.Deprecated = false
end end
if propTags.Deprecated then if propTags.Deprecated then
if not firstLine then if not firstLine then
writeLine() writeLine()
end end
writeLine("[Obsolete]") writeLine("[Obsolete]")
end end
writeLine("public %s %s%s;", valueType, name, default) writeLine("public %s %s%s;", valueType, name, default)
if propTags.Deprecated and j ~= #propNames then if propTags.Deprecated and j ~= #propNames then
writeLine() writeLine()
end end
end end
firstLine = false firstLine = false
end end
end end
closeStack() closeStack()
if (i ~= #classNames) then if i ~= #classNames then
writeLine() writeLine()
end end
end end
end end
closeStack() closeStack()
exportStream("Classes") exportStream("Classes")
@ -784,64 +780,64 @@ end
local function generateEnums(whiteList) local function generateEnums(whiteList)
local version = getfenv().version():gsub("%. ", ".") local version = getfenv().version():gsub("%. ", ".")
clearStream() clearStream()
writeLine("// Auto-generated list of Roblox enums.") writeLine("// Auto-generated list of Roblox enums.")
writeLine("// Updated as of %s", version) writeLine("// Updated as of %s", version)
writeLine() writeLine()
writeLine("namespace RobloxFiles.Enums") writeLine("namespace RobloxFiles.Enums")
openStack() openStack()
local enums = Enum:GetEnums() local enums = Enum:GetEnums()
for i, enum in ipairs(enums) do for i, enum in ipairs(enums) do
local enumName = tostring(enum) local enumName = tostring(enum)
if whiteList and not whiteList[enumName] then if whiteList and not whiteList[enumName] then
continue continue
end end
writeLine("public enum %s", enumName) writeLine("public enum %s", enumName)
openStack() openStack()
local enumItems = enum:GetEnumItems() local enumItems = enum:GetEnumItems()
local lastValue = -1 local lastValue = -1
local mapped = {} local mapped = {}
table.sort(enumItems, function (a, b) table.sort(enumItems, function(a, b)
return a.Value < b.Value return a.Value < b.Value
end) end)
for j, enumItem in ipairs(enumItems) do for j, enumItem in ipairs(enumItems) do
local text = "" local text = ""
local comma = ',' local comma = ","
local name = enumItem.Name local name = enumItem.Name
local value = enumItem.Value local value = enumItem.Value
if not mapped[value] then if not mapped[value] then
if (value - lastValue) ~= 1 then if (value - lastValue) ~= 1 then
text = " = " .. value; text = " = " .. value
end end
if j == #enumItems then if j == #enumItems then
comma = "" comma = ""
end end
lastValue = value lastValue = value
mapped[value] = true mapped[value] = true
writeLine("%s%s%s", name, text, comma) writeLine("%s%s%s", name, text, comma)
end end
end end
closeStack() closeStack()
if i ~= #enums then if i ~= #enums then
writeLine() writeLine()
end end
end end
closeStack() closeStack()
exportStream("Enums") exportStream("Enums")
end end
@ -859,4 +855,4 @@ end
if game.Name:sub(1, 9) == "Null.rbxl" then if game.Name:sub(1, 9) == "Null.rbxl" then
generateAll() generateAll()
end end

1
Plugins/sourcemap.json Normal file
View File

@ -0,0 +1 @@
{"name":"GenerateApiDump","className":"Script","filePaths":["GenerateApiDump\\init.server.lua","default.project.json"],"children":[{"name":"Formatting","className":"ModuleScript","filePaths":["GenerateApiDump\\Formatting.lua"]},{"name":"PropertyPatches","className":"ModuleScript","filePaths":["GenerateApiDump\\PropertyPatches.lua"]}]}

Binary file not shown.

View File

@ -66,9 +66,9 @@ namespace RobloxFiles.Tokens
var style = (FontWeight)attribute.ReadByte(); var style = (FontWeight)attribute.ReadByte();
var family = attribute.ReadString(); var family = attribute.ReadString();
_ = attribute.ReadInt(); // Reserved var cachedFaceId = attribute.ReadString();
return new FontFace(family, style, weight); return new FontFace(family, style, weight, cachedFaceId);
} }
public void WriteAttribute(RbxAttribute attribute, FontFace value) public void WriteAttribute(RbxAttribute attribute, FontFace value)

View File

@ -1,4 +1,5 @@
using System; using System;
using System.Linq;
using System.Xml; using System.Xml;
using RobloxFiles.DataTypes; using RobloxFiles.DataTypes;
@ -14,13 +15,21 @@ namespace RobloxFiles.Tokens
if (Guid.TryParse(hex, out var guid)) if (Guid.TryParse(hex, out var guid))
{ {
var bytes = guid.ToByteArray(); var bytes = new byte[16];
var random = BitConverter.ToUInt64(bytes, 0);
var time = BitConverter.ToUInt32(bytes, 8); for (int i = 0; i < 16; i++)
var index = BitConverter.ToUInt32(bytes, 12); {
var hexChar = hex.Substring(i * 2, 2);
bytes[15 - i] = Convert.ToByte(hexChar, 16);
}
var rand = BitConverter.ToInt64(bytes, 8);
var time = BitConverter.ToUInt32(bytes, 4);
var index = BitConverter.ToUInt32(bytes, 0);
var uniqueId = new UniqueId(rand, time, index);
prop.Value = uniqueId;
prop.Value = new UniqueId(time, index, random);
return true; return true;
} }
@ -30,8 +39,8 @@ namespace RobloxFiles.Tokens
public void WriteProperty(Property prop, XmlDocument doc, XmlNode node) public void WriteProperty(Property prop, XmlDocument doc, XmlNode node)
{ {
var uniqueId = prop.CastValue<UniqueId>(); var uniqueId = prop.CastValue<UniqueId>();
var random = BitConverter.GetBytes(uniqueId.Random);
var random = BitConverter.GetBytes(uniqueId.Random);
var time = BitConverter.GetBytes(uniqueId.Time); var time = BitConverter.GetBytes(uniqueId.Time);
var index = BitConverter.GetBytes(uniqueId.Index); var index = BitConverter.GetBytes(uniqueId.Index);

View File

@ -59,7 +59,7 @@ namespace RobloxFiles
internal static BindingFlags BindingFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.FlattenHierarchy | BindingFlags.IgnoreCase; internal static BindingFlags BindingFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.FlattenHierarchy | BindingFlags.IgnoreCase;
// TODO: Map typeof(ProtectedString) to PropertyType.ProtectedString // TODO: Map typeof(ProtectedString) to PropertyType.ProtectedString
// if binary files are ever publically allowed to read it. // if binary files are ever publicly allowed to read it.
public static readonly IReadOnlyDictionary<Type, PropertyType> Types = new Dictionary<Type, PropertyType>() public static readonly IReadOnlyDictionary<Type, PropertyType> Types = new Dictionary<Type, PropertyType>()
{ {

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,8 @@
using System.Collections.Generic; using System.Linq;
using System.Linq; using System.Collections.Generic;
using RobloxFiles.Enums; using RobloxFiles.Enums;
using RobloxFiles.DataTypes;
namespace RobloxFiles.Utility namespace RobloxFiles.Utility
{ {
@ -26,6 +27,56 @@ namespace RobloxFiles.Utility
{ 96, FontSize.Size96 }, { 96, FontSize.Size96 },
}; };
public static readonly IReadOnlyDictionary<Font, FontFace> FontFaces = new Dictionary<Font, FontFace>()
{
{ Font.Legacy, new FontFace("rbxasset://fonts/families/LegacyArial.json") },
{ Font.Arial, new FontFace("rbxasset://fonts/families/Arial.json") },
{ Font.ArialBold, new FontFace("rbxasset://fonts/families/Arial.json", FontWeight.Bold) },
{ Font.SourceSans, new FontFace("rbxasset://fonts/families/SourceSansPro.json") },
{ Font.SourceSansBold, new FontFace("rbxasset://fonts/families/SourceSansPro.json", FontWeight.Bold) },
{ Font.SourceSansSemibold, new FontFace("rbxasset://fonts/families/SourceSansPro.json", FontWeight.SemiBold) },
{ Font.SourceSansLight, new FontFace("rbxasset://fonts/families/SourceSansPro.json", FontWeight.Light) },
{ Font.SourceSansItalic, new FontFace("rbxasset://fonts/families/SourceSansPro.json", FontWeight.Regular, FontStyle.Italic) },
{ Font.Bodoni, new FontFace("rbxasset://fonts/families/AccanthisADFStd.json") },
{ Font.Garamond, new FontFace("rbxasset://fonts/families/Guru.json") },
{ Font.Cartoon, new FontFace("rbxasset://fonts/families/ComicNeueAngular.json") },
{ Font.Code, new FontFace("rbxasset://fonts/families/Inconsolata.json") },
{ Font.Highway, new FontFace("rbxasset://fonts/families/HighwayGothic.json") },
{ Font.SciFi, new FontFace("rbxasset://fonts/families/Zekton.json") },
{ Font.Arcade, new FontFace("rbxasset://fonts/families/PressStart2P.json") },
{ Font.Fantasy, new FontFace("rbxasset://fonts/families/Balthazar.json") },
{ Font.Antique, new FontFace("rbxasset://fonts/families/RomanAntique.json") },
{ Font.Gotham, new FontFace("rbxasset://fonts/families/GothamSSm.json") },
{ Font.GothamMedium, new FontFace("rbxasset://fonts/families/GothamSSm.json", FontWeight.Medium) },
{ Font.GothamBold, new FontFace("rbxasset://fonts/families/GothamSSm.json", FontWeight.Bold) },
{ Font.GothamBlack, new FontFace("rbxasset://fonts/families/GothamSSm.json", FontWeight.Heavy) },
{ Font.AmaticSC, new FontFace("rbxasset://fonts/families/AmaticSC.json") },
{ Font.Bangers, new FontFace("rbxasset://fonts/families/Bangers.json") },
{ Font.Creepster, new FontFace("rbxasset://fonts/families/Creepster.json") },
{ Font.DenkOne, new FontFace("rbxasset://fonts/families/DenkOne.json") },
{ Font.Fondamento, new FontFace("rbxasset://fonts/families/Fondamento.json") },
{ Font.FredokaOne, new FontFace("rbxasset://fonts/families/FredokaOne.json") },
{ Font.GrenzeGotisch, new FontFace("rbxasset://fonts/families/GrenzeGotisch.json") },
{ Font.IndieFlower, new FontFace("rbxasset://fonts/families/IndieFlower.json") },
{ Font.JosefinSans, new FontFace("rbxasset://fonts/families/JosefinSans.json") },
{ Font.Jura, new FontFace("rbxasset://fonts/families/Jura.json") },
{ Font.Kalam, new FontFace("rbxasset://fonts/families/Kalam.json") },
{ Font.LuckiestGuy, new FontFace("rbxasset://fonts/families/LuckiestGuy.json") },
{ Font.Merriweather, new FontFace("rbxasset://fonts/families/Merriweather.json") },
{ Font.Michroma, new FontFace("rbxasset://fonts/families/Michroma.json") },
{ Font.Nunito, new FontFace("rbxasset://fonts/families/Nunito.json") },
{ Font.Oswald, new FontFace("rbxasset://fonts/families/Oswald.json") },
{ Font.PatrickHand, new FontFace("rbxasset://fonts/families/PatrickHand.json") },
{ Font.PermanentMarker, new FontFace("rbxasset://fonts/families/PermanentMarker.json") },
{ Font.Roboto, new FontFace("rbxasset://fonts/families/Roboto.json") },
{ Font.RobotoCondensed, new FontFace("rbxasset://fonts/families/RobotoCondensed.json") },
{ Font.RobotoMono, new FontFace("rbxasset://fonts/families/RobotoMono.json") },
{ Font.Sarpanch, new FontFace("rbxasset://fonts/families/Sarpanch.json") },
{ Font.SpecialElite, new FontFace("rbxasset://fonts/families/SpecialElite.json") },
{ Font.TitilliumWeb, new FontFace("rbxasset://fonts/families/TitilliumWeb.json") },
{ Font.Ubuntu, new FontFace("rbxasset://fonts/families/Ubuntu.json") },
};
public static FontSize GetFontSize(int fontSize) public static FontSize GetFontSize(int fontSize)
{ {
if (fontSize > 60) if (fontSize > 60)
@ -57,5 +108,19 @@ namespace RobloxFiles.Utility
return value; return value;
} }
public static Font GetFont(FontFace face)
{
var result = Font.Unknown;
var faceQuery = FontFaces
.Where(pair => face.Equals(pair.Value))
.Select(pair => pair.Key);
if (faceQuery.Any())
result = faceQuery.First();
return result;
}
} }
} }