188 lines
15 KiB
C#
188 lines
15 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Globalization;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Security.Cryptography;
|
|
using System.Text;
|
|
using System.Text.RegularExpressions;
|
|
using BlockOutReplica.Presentation;
|
|
using UnityEditor;
|
|
using UnityEngine;
|
|
|
|
namespace BlockOutReplica.Editor
|
|
{
|
|
public static class OriginalTypographyRecovery
|
|
{
|
|
private const string Destination = "Assets/Resources/OriginalTypography";
|
|
[MenuItem("Block Out/Recover Original Typography")]
|
|
public static void Recover()
|
|
{
|
|
string project = Path.GetFullPath(Path.Combine(Application.dataPath, "../.."));
|
|
string source = project + "/reverse/assetripper_export/unity_project/ExportedProject/Assets";
|
|
string binary = project + "/reverse/unpacked/base/assets/bin/Data/sharedassets0.assets.split180";
|
|
byte[] bytes = File.ReadAllBytes(binary);
|
|
byte[] needle = Encoding.UTF8.GetBytes("MuseoSansRounded-1000 SDF");
|
|
int nameOffset = -1;
|
|
for (int i = 4; i < bytes.Length - needle.Length; i++)
|
|
if (BitConverter.ToInt32(bytes, i - 4) == needle.Length && needle.Select((b, j) => bytes[i + j] == b).All(b => b))
|
|
{if (nameOffset != -1) throw new InvalidDataException("Ambiguous original Museo font blob"); nameOffset = i;}
|
|
if (nameOffset < 0) throw new InvalidDataException("Original Museo font blob not found");
|
|
var data = ScriptableObject.CreateInstance<OriginalSdfFontData>();
|
|
data.name = "MuseoSansRounded-1000";
|
|
data.SourceFile = "reverse/unpacked/base/assets/bin/Data/sharedassets0.assets.split180";
|
|
data.SourceNameOffset = nameOffset;
|
|
using (var sha = SHA256.Create()) data.SourceSha256 = string.Concat(sha.ComputeHash(bytes).Select(b => b.ToString("x2")));
|
|
var reader = new BinaryReader(new MemoryStream(bytes));
|
|
reader.BaseStream.Position = Align(nameOffset + needle.Length) + 20; // TMP_Asset hash, material PPtr, material hash.
|
|
if (ReadString(reader) != "1.1.0") throw new InvalidDataException("Unsupported source TMP font layout");
|
|
ReadString(reader); // Source font GUID. Static atlas build does not include the source TTF.
|
|
reader.BaseStream.Position += 12; // source font PPtr
|
|
if (reader.ReadInt32() != 0) throw new InvalidDataException("Expected original static font atlas");
|
|
reader.ReadInt32(); // FaceInfo face index
|
|
data.Family = ReadString(reader); data.Style = ReadString(reader);
|
|
data.PointSize = reader.ReadInt32(); data.FaceScale = reader.ReadSingle(); reader.ReadInt32(); // Units per EM
|
|
data.LineHeight = reader.ReadSingle(); data.Ascent = reader.ReadSingle(); data.CapHeight = reader.ReadSingle();
|
|
reader.ReadSingle(); reader.ReadSingle(); data.Descent = reader.ReadSingle();
|
|
reader.BaseStream.Position += 9 * 4; // superscript/subscript, underline/strikethrough and tab metrics
|
|
int glyphCount = reader.ReadInt32();
|
|
if (data.Family != "Museo Sans Rounded" || data.Style != "1000" || data.PointSize != 50 || glyphCount != 317)
|
|
throw new InvalidDataException("Source font identity/layout differs; do not guess glyphs");
|
|
var glyphs = new Dictionary<int, OriginalSdfFontData.Glyph>();
|
|
for (int i = 0; i < glyphCount; i++)
|
|
{
|
|
var glyph = new OriginalSdfFontData.Glyph {Index = reader.ReadInt32(), Width = reader.ReadSingle(), Height = reader.ReadSingle(),
|
|
BearingX = reader.ReadSingle(), BearingY = reader.ReadSingle(), Advance = reader.ReadSingle()};
|
|
glyph.Rect = new Rect(reader.ReadInt32(), reader.ReadInt32(), reader.ReadInt32(), reader.ReadInt32());
|
|
glyph.Scale = reader.ReadSingle();
|
|
if (reader.ReadInt32() != 0) throw new InvalidDataException("Unexpected source glyph atlas index");
|
|
reader.ReadInt32(); // Glyph class definition
|
|
glyphs.Add(glyph.Index, glyph);
|
|
}
|
|
int characterCount = reader.ReadInt32();
|
|
if (characterCount != 317) throw new InvalidDataException("Unexpected original character table length");
|
|
var characters = new List<OriginalSdfFontData.Glyph>();
|
|
for (int i = 0; i < characterCount; i++)
|
|
{
|
|
if (reader.ReadInt32() != 1) throw new InvalidDataException("Expected original TMP character element");
|
|
int unicode = reader.ReadInt32(), index = reader.ReadInt32(); float scale = reader.ReadSingle();
|
|
var original = glyphs[index];
|
|
characters.Add(new OriginalSdfFontData.Glyph {Unicode = unicode, Index = index, Rect = original.Rect, Width = original.Width,
|
|
Height = original.Height, BearingX = original.BearingX, BearingY = original.BearingY, Advance = original.Advance, Scale = original.Scale * scale});
|
|
}
|
|
if (reader.ReadInt32() != 1 || reader.ReadInt32() != 0) throw new InvalidDataException("Expected one local source atlas");
|
|
data.SourceAtlasPathId = checked((int)reader.ReadInt64());
|
|
if (data.SourceAtlasPathId != 248) throw new InvalidDataException("Source font atlas identity differs");
|
|
data.Glyphs = characters.ToArray();
|
|
data.Padding = Scalar(File.ReadAllText(source + "/Material/MuseoSansRounded-1000 SDF-Flat.mat"), "_GradientScale") - 1;
|
|
Directory.CreateDirectory(Destination);
|
|
string atlasPath = Destination + "/MuseoSansRounded-1000 SDF Atlas.png";
|
|
File.Copy(source + "/Texture2D/MuseoSansRounded-1000 SDF Atlas.png", atlasPath, true);
|
|
AssetDatabase.ImportAsset(atlasPath, ImportAssetOptions.ForceSynchronousImport);
|
|
var importer = (TextureImporter)AssetImporter.GetAtPath(atlasPath);
|
|
importer.textureType = TextureImporterType.Default; importer.textureCompression = TextureImporterCompression.Uncompressed;
|
|
importer.mipmapEnabled = false; importer.sRGBTexture = false; importer.alphaIsTransparency = false;
|
|
importer.maxTextureSize = 2048; importer.npotScale = TextureImporterNPOTScale.None;
|
|
importer.wrapMode = TextureWrapMode.Clamp; importer.filterMode = FilterMode.Bilinear; importer.SaveAndReimport();
|
|
data.Atlas = AssetDatabase.LoadAssetAtPath<Texture2D>(atlasPath);
|
|
var material = new Material(Shader.Find("BlockOut/OriginalTypographySdf")) {name = "Original Museo SDF Compatibility", mainTexture = data.Atlas};
|
|
data.RenderingMaterial = Save(material, Destination + "/MuseoSdf.mat");
|
|
data.Profiles = new[] {Profile(source, "Body", "Flat"), Profile(source, "Counter", "DarkGray"),
|
|
Profile(source, "Title", "PurpleTitle1"), Profile(source, "Button", "Atlas"),
|
|
Profile(source, "ButtonGreen", "Atlas"), Profile(source, "ButtonRed", "Atlas"),
|
|
Profile(source, "ButtonGreenOutline", "Multilayer-GreenDarkGreen"),
|
|
Profile(source, "ButtonRedOuter", "Multilayer-59-6-3"), Profile(source, "ButtonRedOutline", "Multilayer-85-17-7"),
|
|
Profile(source, "PopupLabel", "PurpleTitle1"),
|
|
Profile(source, "Timer", "LevelTimer"), Profile(source, "Navigation", "Flat"),
|
|
Profile(source, "TitleOuter", "PurpleTitle3"), Profile(source, "TitleExpanded", "DilateFull"),
|
|
// Pause's expanded Text (1) uses DilateFull, but its TMP vertex tint is
|
|
// absent from the export. These genuine source palette colors are retained
|
|
// separately; applying them to that layer is an explicit reconstruction.
|
|
Profile(source, "TitleBrightPalette", "MultilayerMidLightPurple"),
|
|
Profile(source, "TitleHighlightPalette", "MultilayerOuterLightPurple"),
|
|
Profile(source, "JourneyTitle", "JourneyTitle1"), Profile(source, "JourneyTitleWhiteStroke", "JourneyTitle2"),
|
|
Profile(source, "JourneyTitleOuter", "JourneyTitle3")};
|
|
data = Save(data, Destination + "/MuseoSansRounded-1000.asset");
|
|
foreach (string fontName in new[] {"Roboto-Bold.ttf", "LiberationSans.ttf"})
|
|
{
|
|
string path = Destination + "/" + fontName;
|
|
if (!File.Exists(path)) File.Copy(source + "/Font/" + fontName, path);
|
|
AssetDatabase.ImportAsset(path, ImportAssetOptions.ForceSynchronousImport);
|
|
}
|
|
AssetDatabase.SaveAssets();
|
|
Debug.Log($"BLOCKOUT_ORIGINAL_TYPOGRAPHY_RECOVERED: family={data.Family}, style={data.Style}, pointSize={data.PointSize}, glyphs={glyphCount}, characters={characterCount}, atlasPathId={data.SourceAtlasPathId}, blobOffset={nameOffset}, sourceSha256={data.SourceSha256}, trueSdfGlyphs=True, sourceTtfAbsent=True, kerningNotRecovered=True, shaderCompatibilityAdapter=True");
|
|
}
|
|
|
|
[MenuItem("Block Out/Validate Original Typography")]
|
|
public static void Validate()
|
|
{
|
|
var data = Resources.Load<OriginalSdfFontData>("OriginalTypography/MuseoSansRounded-1000");
|
|
if (data == null || data.Glyphs.Length != 317 || !data.Supports("PLAY Journey Level 10 PAUSE 1000 02:59"))
|
|
throw new InvalidDataException("Actual original Latin glyph table is missing");
|
|
if (data.Atlas.width != 1024 || data.Atlas.height != 1024 || data.Glyphs.Any(g => g.Rect.xMin < 0 || g.Rect.yMin < 0 || g.Rect.xMax > 1024 || g.Rect.yMax > 1024))
|
|
throw new InvalidDataException("Original glyph rectangles do not match atlas");
|
|
if (data.Supports("中") || data.Supports("∞")) throw new InvalidDataException("Do not pretend source font supports absent glyphs");
|
|
if (data.RenderingMaterial == null || !data.RenderingMaterial.shader.isSupported) throw new InvalidDataException("Typography compatibility shader unavailable");
|
|
foreach (string role in new[] {"Title", "TitleOuter", "TitleExpanded", "JourneyTitle", "JourneyTitleWhiteStroke", "JourneyTitleOuter"})
|
|
{
|
|
var profile = data.GetProfile(role);
|
|
string source = Path.GetFullPath(Path.Combine(Application.dataPath,
|
|
"../../reverse/assetripper_export/unity_project/ExportedProject/Assets/Material/" + profile.SourceMaterial + ".mat"));
|
|
if (Mathf.Abs(profile.ScaleRatioA - Scalar(File.ReadAllText(source), "_ScaleRatioA")) > .00001f)
|
|
throw new InvalidDataException("Original title ScaleRatioA missing; rerun typography recovery: " + role);
|
|
}
|
|
foreach (var profile in data.Profiles)
|
|
{
|
|
string source = Path.GetFullPath(Path.Combine(Application.dataPath,
|
|
"../../reverse/assetripper_export/unity_project/ExportedProject/Assets/Material/" + profile.SourceMaterial + ".mat"));
|
|
string yaml = File.ReadAllText(source);
|
|
if (profile.OutlineEnabled != Regex.IsMatch(yaml, @"(?m)^ - OUTLINE_ON$") ||
|
|
profile.UnderlayEnabled != Regex.IsMatch(yaml, @"(?m)^ - UNDERLAY_ON$") ||
|
|
Mathf.Abs(profile.ScaleRatioC - Scalar(yaml, "_ScaleRatioC")) > .00001f)
|
|
throw new InvalidDataException("Original typography keyword/underlay ratio missing: " + profile.Name);
|
|
}
|
|
foreach (string role in new[] {"ButtonGreen", "ButtonRed", "ButtonGreenOutline", "ButtonRedOuter", "ButtonRedOutline", "PopupLabel", "TitleBrightPalette", "TitleHighlightPalette"})
|
|
if (!data.Profiles.Any(p => p.Name == role)) throw new InvalidDataException("Original popup profile missing: " + role);
|
|
if (data.GetProfile("ButtonGreenOutline").Face.g <= data.GetProfile("ButtonGreenOutline").Face.r ||
|
|
data.GetProfile("ButtonRedOutline").Face.r <= data.GetProfile("ButtonRedOutline").Face.g)
|
|
throw new InvalidDataException("Original button colored layers are not green/red");
|
|
Color bright = data.GetProfile("TitleBrightPalette").Face;
|
|
if (bright.r <= data.GetProfile("Title").Outline.r || bright.b <= data.GetProfile("Title").Outline.b)
|
|
throw new InvalidDataException("Title expanded layer must use distinct original bright-purple palette, not the inner outline color");
|
|
Debug.Log("BLOCKOUT_ORIGINAL_TYPOGRAPHY_VALIDATION: passed=True, characters=317, latinLabels=True, missingUnicodeExplicit=True, sourceFontNotFreight=True, titleSourceScaleRatios=True, sourceOutlineUnderlayKeywords=True, pauseOriginalButtonLayers=True, expandedTitleTint=OriginalMultilayerMidLightPurplePaletteReconstruction, pauseVertexTintNotRecovered=True, gpuContourVisualNotValidated=True");
|
|
}
|
|
|
|
private static OriginalSdfFontData.Profile Profile(string source, string role, string suffix)
|
|
{
|
|
string name = suffix == "Atlas" ? "MuseoSansRounded-1000 Atlas Material" : "MuseoSansRounded-1000 SDF-" + suffix;
|
|
string yaml = File.ReadAllText(source + "/Material/" + name + ".mat");
|
|
return new OriginalSdfFontData.Profile {Name = role, SourceMaterial = name,
|
|
Face = ColorField(yaml, "_FaceColor"), Outline = ColorField(yaml, "_OutlineColor"), Underlay = ColorField(yaml, "_UnderlayColor"),
|
|
FaceDilate = Scalar(yaml, "_FaceDilate"), OutlineWidth = Scalar(yaml, "_OutlineWidth"), UnderlayDilate = Scalar(yaml, "_UnderlayDilate"),
|
|
UnderlayOffsetX = Scalar(yaml, "_UnderlayOffsetX"), UnderlayOffsetY = Scalar(yaml, "_UnderlayOffsetY"),
|
|
ScaleRatioA = Scalar(yaml, "_ScaleRatioA"), ScaleRatioC = Scalar(yaml, "_ScaleRatioC"),
|
|
OutlineEnabled = Regex.IsMatch(yaml, @"(?m)^ - OUTLINE_ON$"),
|
|
UnderlayEnabled = Regex.IsMatch(yaml, @"(?m)^ - UNDERLAY_ON$")};
|
|
}
|
|
private static float Scalar(string yaml, string name) => float.Parse(Regex.Match(yaml, @"(?m)^\s*" + name + @": (\S+)").Groups[1].Value, CultureInfo.InvariantCulture);
|
|
private static Color ColorField(string yaml, string name)
|
|
{
|
|
Match m = Regex.Match(yaml, name + @": \{r: ([^,]+), g: ([^,]+), b: ([^,]+), a: ([^}]+)\}");
|
|
return new Color(float.Parse(m.Groups[1].Value, CultureInfo.InvariantCulture), float.Parse(m.Groups[2].Value, CultureInfo.InvariantCulture),
|
|
float.Parse(m.Groups[3].Value, CultureInfo.InvariantCulture), float.Parse(m.Groups[4].Value, CultureInfo.InvariantCulture));
|
|
}
|
|
private static int Align(int position) => (position + 3) & ~3;
|
|
private static string ReadString(BinaryReader reader)
|
|
{
|
|
int count = reader.ReadInt32(); if (count < 0 || count > 512) throw new InvalidDataException("Invalid aligned source string");
|
|
string value = Encoding.UTF8.GetString(reader.ReadBytes(count)); reader.BaseStream.Position = Align((int)reader.BaseStream.Position); return value;
|
|
}
|
|
private static T Save<T>(T asset, string path) where T : UnityEngine.Object
|
|
{
|
|
T old = AssetDatabase.LoadAssetAtPath<T>(path);
|
|
if (old == null) {AssetDatabase.CreateAsset(asset, path); return asset;}
|
|
EditorUtility.CopySerialized(asset, old); UnityEngine.Object.DestroyImmediate(asset); return old;
|
|
}
|
|
}
|
|
}
|