using _2DGAMELIB; using SlaveMatrix.Properties; using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; using System.Text; using Newtonsoft.Json; using Newtonsoft.Json.Linq; namespace SlaveMatrix.Extract; class Program { static readonly string ProjectRoot = Environment.CurrentDirectory; static string AssetsDir = Path.Combine(ProjectRoot, "SlaveMatrix", "Assets"); static string OutputDir; // Complete translation map for extraction output. // Separate from the runtime KeyMap in ObjExtensions — the runtime map only // contains entries that match the old game code's string lookups. static readonly Dictionary EnglishNameMap = new() { ["咳"] = "Cough", ["腰"] = "Waist", ["胴"] = "Torso", ["首"] = "Neck", ["頭"] = "Head", ["後髪0"] = "BackHair0", ["後髪1"] = "BackHair1", ["横髪"] = "SideHair", ["脚"] = "Leg", ["腕"] = "Arm", ["肩"] = "Shoulder", ["胸"] = "Chest", ["下腕"] = "LowerArm", ["上腕"] = "UpperArm", ["鳥翼上腕"] = "鳥翼UpperArm", ["獣翼上腕"] = "獣翼UpperArm", ["四足上腕"] = "四足UpperArm", ["鳥翼下腕"] = "鳥翼LowerArm", ["獣翼下腕"] = "獣翼LowerArm", ["四足下腕"] = "四足LowerArm", ["基髪"] = "BaseHair", ["胸毛"] = "ChestHair", ["前髪"] = "FrontHair", ["眉左"] = "EyebrowLeft", ["額目"] = "ForeheadEye", ["額瞼"] = "ForeheadEyelid", ["顔ハイライト左"] = "FaceHighlightLeft", ["顔面"] = "Face", ["魔性中瞼左"] = "DemonicMidEyelidLeft", ["魔性強瞼左"] = "DemonicStrongEyelidLeft", ["魔性弱瞼左"] = "DemonicWeakEyelidLeft", ["単眼目"] = "MonoEye", ["単眼眉"] = "MonoEyebrow", ["単眼瞼"] = "MonoEyelid", ["単足"] = "MonoLeg", ["獣性瞼左"] = "BestialEyelidLeft", ["目傷左"] = "EyeScarLeft", ["目尻影左"] = "EyeCornerShadowLeft", ["目左"] = "EyeLeft", ["目隠帯"] = "Blindfold", ["頬目左"] = "CheekEyeLeft", ["頬瞼左"] = "CheekEyelidLeft", ["頬肌左"] = "CheekSkinLeft", ["口"] = "Mouth", ["舌"] = "Tongue", ["涎左"] = "DroolLeft", ["涎口裂け左"] = "DroolMouthGashLeft", ["呼気"] = "Breath", ["吹出し"] = "SpeechBubble", ["鼻"] = "Nose", ["鼻水左"] = "NoseDripLeft", ["鼻肌"] = "NoseSkin", ["耳"] = "Ear", ["獣耳"] = "BeastEar", ["角"] = "Horn", ["胸郭"] = "Chest", ["胸郭肌"] = "ChestSkin", ["胸郭腹板"] = "ChestPlate", ["胸左"] = "LeftBreast", ["胴肌"] = "TorsoSkin", ["胴腹板"] = "TorsoPlate", ["腰肌"] = "WaistSkin", ["ボテ腹"] = "PregnantBelly", ["ボテ腹板"] = "PregnantBellyPlate", ["固定帯"] = "FixingBelt", ["紅潮"] = "Blush", ["肛門"] = "Anus", ["肛門精液垂れ"] = "AnusSemenDrip", ["膣基"] = "VaginaBase", ["膣内精液"] = "InternalSemen", ["性器"] = "Genitals", ["性器精液垂れ"] = "GenitalsSemenDrip", ["射精"] = "Ejaculation", ["放尿"] = "Urination", ["噴乳左"] = "LactationLeft", ["潮吹"] = "Squirting", ["尾"] = "Tail", ["背中"] = "Back", ["手"] = "Hand", ["足"] = "Foot", ["腿"] = "Thigh", ["前翅"] = "Forewing", ["後翅"] = "Hindwing", ["触手"] = "Tentacle", ["触覚"] = "Antenna", ["節足"] = "SegmentLeg", ["虫鎌"] = "InsectScythe", ["虫顎"] = "InsectJaw", ["植"] = "Plant", ["葉"] = "Leaf", ["羽根箒"] = "FeatherDuster", ["飛沫"] = "Splash", ["飛膜先"] = "MembraneTip", ["飛膜根"] = "MembraneBase", ["上着トップ"] = "JacketTop", ["上着ミドル"] = "JacketMid", ["上着ボトム前"] = "JacketBottomFront", ["上着ボトム後"] = "JacketBottomBack", ["下着トップ"] = "UnderwearTop", ["下着ボトム"] = "UnderwearBottom", ["下着乳首左"] = "UnderwearNippleLeft", ["下着陰核"] = "UnderwearClitoris", ["帽子"] = "Hat", ["パンスト"] = "Pantyhose", ["キャップ左"] = "CapLeft", ["キャップ中"] = "CapMid", ["玉口枷"] = "BallGag", ["留具前"] = "FastenerFront", ["留具後"] = "FastenerBack", ["拘束具上"] = "RestraintUpper", ["拘束具下"] = "RestraintLower", ["拘束鎖"] = "RestraintChain", ["押し付け"] = "Pressing", ["コモン"] = "CommonCursor", ["ハンド"] = "HandCursor", ["ペニス"] = "PenisCursor", ["アナル"] = "AnalCursor", ["マウス"] = "MouthCursor", ["ディル"] = "DildoCursor", ["デンマ"] = "VibratorCursor", ["ドリル"] = "DrillCursor", ["パール"] = "PearlCursor", ["ロータ"] = "RotoCursor", ["T字剃刀"] = "TRazorCursor", ["ぶっかけ"] = "Bukkake", ["キスマーク"] = "KissMark", ["汗"] = "Sweat", ["汚れ"] = "Stain", ["流血中"] = "BleedingMid", ["流血大"] = "BleedingLarge", ["流血小"] = "BleedingSmall", ["鞭痕"] = "WhipMark", ["横髪左"] = "SideHairLeft", ["涙左"] = "TearLeft", ["青筋左"] = "VeinLeft", ["頭部前"] = "Forehead", ["頭部後"] = "BackOfHead", ["鰭"] = "Fin", ["獣翼手"] = "BeastWingHand", ["鳥翼手"] = "BirdWingHand", ["エイリアン目左"] = "AlienEyeLeft", ["ピアス"] = "Piercing", ["反応"] = "Reaction", ["多足"] = "MultiLeg", ["大顎上"] = "MandibleUpper", ["大顎基"] = "MandibleBase", ["意思表示"] = "Expression", ["断面"] = "CrossSection", ["染み"] = "Spot", ["長物"] = "Phallus", ["調教鞭"] = "Whip", ["衝撃"] = "Impact", ["鎖"] = "Chain", ["四足ボテ腹"] = "QuadrupedPregnantBelly", ["四足固定帯"] = "QuadrupedFixingBelt", ["四足性器"] = "QuadrupedGenitals", ["四足性器精液垂れ"] = "QuadrupedGenitalsSemenDrip", ["四足手"] = "QuadrupedHand", ["四足放尿"] = "QuadrupedUrination", ["四足断面"] = "QuadrupedCrossSection", ["四足染み"] = "QuadrupedSpot", ["四足潮吹"] = "QuadrupedSquirting", ["四足留具前"] = "QuadrupedFastenerFront", ["四足留具後"] = "QuadrupedFastenerBack", ["四足肛門"] = "QuadrupedAnus", ["四足肛門精液垂れ"] = "QuadrupedAnusSemenDrip", ["四足胴"] = "QuadrupedTorso", ["四足胸郭"] = "QuadrupedChest", ["四足脇"] = "QuadrupedSide", ["四足脚"] = "QuadrupedLeg", ["四足腰"] = "QuadrupedWaist", ["四足腿"] = "QuadrupedThigh", ["四足膣内精液"] = "QuadrupedInternalSemen", ["四足膣基"] = "QuadrupedVaginaBase", ["四足足"] = "QuadrupedFoot", ["四足飛沫"] = "QuadrupedSplash", }; static void Main(string[] args) { Console.WriteLine("SlaveMatrix Asset Extractor"); Console.WriteLine("=================================\n"); OutputDir = args.Length > 0 && args[0] == "--output" && args.Length > 1 ? Path.GetFullPath(args[1]) : Path.GetFullPath(AssetsDir); var partsDir = Path.Combine(OutputDir, "Parts"); var jsonDir = Path.Combine(OutputDir, "..", "..", "extracted"); Directory.CreateDirectory(partsDir); Directory.CreateDirectory(jsonDir); var resources = new (string Name, byte[] Data)[] { ("胴体", Resources.胴体), ("肩左", Resources.肩左), ("腕左", Resources.腕左), ("脚左", Resources.脚左), ("尻尾", Resources.尻尾), ("半身", Resources.半身), ("肢左", Resources.肢左), ("肢中", Resources.肢中), ("性器", Resources.性器), ("性器付", Resources.性器付), ("スタンプ", Resources.スタンプ), ("カーソル", Resources.カーソル), ("その他", Resources.その他), }; var catalog = new JObject(); catalog["parts"] = new JArray(); var catalogParts = (JArray)catalog["parts"]; foreach (var (name, data) in resources) { Console.Write($"Loading {name}... "); Obj? obj; try { obj = data.ObjLoadRaw(); if (obj == null) { Console.WriteLine("FAILED: null"); continue; } Console.WriteLine($"OK ({obj.Difss.Count} keys)"); } catch (Exception ex) { Console.WriteLine($"FAILED: {ex.Message}"); continue; } Console.Write(" Applying MigrateKeys... "); // Capture original keys before migration for original_key tracking var origKeys = obj.Difss.Keys.Cast().ToList(); obj.MigrateKeys(); Console.WriteLine("OK"); var jsonObj = ExportObj(obj); var jsonPath = Path.Combine(jsonDir, $"{name}.json"); File.WriteAllText(jsonPath, JsonConvert.SerializeObject(jsonObj, Formatting.Indented)); var keys = obj.Difss.Keys.Cast().ToList(); foreach (var key in keys) { // Use extraction EnglishNameMap for directory naming var engKey = EnglishNameMap.TryGetValue(key, out var mapped) ? mapped : key; var difs = obj.Difss[key]; var partDir = Path.Combine(partsDir, SanitizeName(engKey)); Directory.CreateDirectory(partDir); var foundJoints = new List(); // original_key: the pre-migration Japanese key var originalKey = origKeys.Contains(key) ? key : origKeys.FirstOrDefault(ok => EnglishNameMap.GetValueOrDefault(ok, ok) == engKey) ?? key; var partEntry = new JObject { ["id"] = engKey, ["original_key"] = originalKey, ["resource"] = name, ["morph_x"] = difs.CountX, ["morph_y"] = difs.CountY, ["variants"] = new JArray(), ["fields"] = new JArray() }; var variants = (JArray)partEntry["variants"]; for (int x = 0; x < difs.CountX; x++) { var dif = difs[x]; for (int y = 0; y < dif.Count; y++) { var pars = dif[y]; var svgName = $"x{x}y{y}.svg"; var svgContent = ExportParsToSvg(pars, foundJoints); variants.Add(new JObject { ["x"] = x, ["y"] = y, ["file"] = svgName }); File.WriteAllText(Path.Combine(partDir, svgName), svgContent); } } foreach (string childKey in difs[0][0].Keys) { ((JArray)partEntry["fields"]).Add(new JObject { ["name"] = childKey }); } if (foundJoints.Count > 0) { var joints = new JArray(); foreach (var j in foundJoints.Distinct()) { var parts = j.Split(','); joints.Add(new JObject { ["position"] = new JArray { double.Parse(parts[0], CultureInfo.InvariantCulture), double.Parse(parts[1], CultureInfo.InvariantCulture) } }); } partEntry["joints"] = joints; } var yaml = ToYaml((JObject)partEntry); File.WriteAllText(Path.Combine(partDir, "part.yaml"), yaml); catalogParts.Add(new JObject { ["id"] = engKey, ["path"] = $"Parts/{SanitizeName(engKey)}/" }); } Console.WriteLine($" -> {keys.Count} parts exported"); } var catalogYaml = ToYaml(catalog); var catalogPath = Path.Combine(OutputDir, "Catalog.yaml"); File.WriteAllText(catalogPath, catalogYaml); Console.WriteLine($"\nCatalog: {catalogPath}"); Console.WriteLine($"Total: {((JArray)catalog["parts"]).Count} parts\n"); } static string ToYaml(JObject obj, int indent = 0) { var prefix = new string(' ', indent); var sb = new StringBuilder(); foreach (var prop in obj.Properties()) { if (prop.Value is JObject nested) { sb.AppendLine($"{prefix}{prop.Name}:"); sb.Append(ToYaml(nested, indent + 2)); } else if (prop.Value is JArray arr) { if (arr.Count == 0) { sb.AppendLine($"{prefix}{prop.Name}: []"); continue; } if (arr[0] is JObject) { sb.AppendLine($"{prefix}{prop.Name}:"); foreach (var item in arr) { var objItem = (JObject)item; sb.Append($"{prefix}- "); var first = true; foreach (var p in objItem.Properties()) { if (first) { sb.AppendLine($"{p.Name}: {FormatYamlValue(p.Value)}"); first = false; } else { sb.AppendLine($"{prefix} {p.Name}: {FormatYamlValue(p.Value)}"); } } } } else { sb.AppendLine($"{prefix}{prop.Name}:"); foreach (var item in arr) sb.AppendLine($"{prefix}- {FormatYamlValue(item)}"); } } else { sb.AppendLine($"{prefix}{prop.Name}: {FormatYamlValue(prop.Value)}"); } } return sb.ToString(); } static string FormatYamlValue(JToken token) { if (token.Type == JTokenType.String) { var s = token.Value(); if (s.Contains(':') || s.Contains('#') || s.Contains('\n')) return $"\"{s}\""; return s; } if (token.Type == JTokenType.Float) return ((double)token).ToString("G", CultureInfo.InvariantCulture); if (token.Type == JTokenType.Integer) return ((long)token).ToString(CultureInfo.InvariantCulture); return token.ToString(); } static string ExportParsToSvg(PartGroup PartGroup, List foundJoints) { var sb = new StringBuilder(); sb.AppendLine(""); sb.AppendLine(""); ExportParsToSvgInner(PartGroup, sb, foundJoints); sb.AppendLine(""); return sb.ToString(); } static void ExportParsToSvgInner(PartGroup PartGroup, StringBuilder sb, List foundJoints) { foreach (string key in PartGroup.Keys) { var val = PartGroup[key]; if (val is PartGroup childPars) { sb.AppendLine($""); ExportParsToSvgInner(childPars, sb, foundJoints); sb.AppendLine(""); } else if (val is ShapePart par) { ExportParToSvg(par, key, sb, foundJoints); } } } static void ExportParToSvg(ShapePart ShapePart, string name, StringBuilder sb, List foundJoints) { if (!ShapePart.Dra) return; var bx = ShapePart.BasePoint.X; var by = ShapePart.BasePoint.Y; var px = ShapePart.Position.X; var py = ShapePart.Position.Y; var angle = ShapePart.Angle; var sx = ShapePart.Size * ShapePart.SizeX; var sy = ShapePart.Size * ShapePart.SizeY; var hasTransform = System.Math.Abs(bx) > 0.001 || System.Math.Abs(by) > 0.001 || System.Math.Abs(px) > 0.001 || System.Math.Abs(py) > 0.001 || System.Math.Abs(angle) > 0.001 || System.Math.Abs(sx - 1) > 0.001 || System.Math.Abs(sy - 1) > 0.001; if (hasTransform) sb.Append($""); foreach (var outObj in ShapePart.OP) { var points = outObj.ps; if (points.Count < 2) continue; var d = BuildSvgPath(points, outObj.Tension, ShapePart.Closed); var fill = ShapePart.Closed ? "#cccccc" : "none"; var stroke = outObj.Outline ? "#000000" : "none"; var sw_val = outObj.Outline ? System.Math.Max(ShapePart.PenWidth, 0.001) : 0.0; sb.AppendLine($""); } if (hasTransform) sb.Append(""); foreach (var joi in ShapePart.JP) { foundJoints.Add($"{F(joi.Joint.X)},{F(joi.Joint.Y)}"); } } static string BuildSvgPath(List points, float tension, bool closed) { int n = points.Count; if (n < 2) return ""; var sb = new StringBuilder(); sb.Append($"M {F(points[0].X)} {F(points[0].Y)}"); if (n == 2) { sb.Append($" L {F(points[1].X)} {F(points[1].Y)}"); if (closed) sb.Append(" Z"); return sb.ToString(); } double tensionFactor = 1.0 - tension; if (tensionFactor < 0.0) tensionFactor = 0.0; int segmentCount = closed ? n : n - 1; for (int i = 0; i < segmentCount; i++) { int p0, p1, p2, p3; if (closed) { p0 = (i - 1 + n) % n; p1 = i; p2 = (i + 1) % n; p3 = (i + 2) % n; } else { p0 = System.Math.Max(i - 1, 0); p1 = i; p2 = i + 1; p3 = System.Math.Min(i + 2, n - 1); } var ci1x = tensionFactor * 0.5 * (points[p2].X - points[p0].X); var ci1y = tensionFactor * 0.5 * (points[p2].Y - points[p0].Y); var ci2x = tensionFactor * 0.5 * (points[p3].X - points[p1].X); var ci2y = tensionFactor * 0.5 * (points[p3].Y - points[p1].Y); var c1x = F(points[p1].X + ci1x / 3); var c1y = F(points[p1].Y + ci1y / 3); var c2x = F(points[p2].X + ci2x / 3); var c2y = F(points[p2].Y + ci2y / 3); var ex = F(points[p2].X); var ey = F(points[p2].Y); sb.Append($" C {c1x} {c1y} {c2x} {c2y} {ex} {ey}"); } if (closed) sb.Append(" Z"); return sb.ToString(); } static string F(double v) => v.ToString("G", CultureInfo.InvariantCulture); static string EscapeXml(string s) => s.Replace("&", "&").Replace("<", "<").Replace(">", ">").Replace("\"", """); static string SanitizeName(string name) { var invalid = Path.GetInvalidFileNameChars(); var result = name; foreach (var c in invalid) result = result.Replace(c, '_'); return result; } static JObject ExportObj(Obj obj) { var keys = obj.Difss.Keys.Cast().ToList(); var result = new JObject { ["Tag"] = obj.Tag ?? "", ["KeyCount"] = keys.Count, ["Keys"] = JArray.FromObject(keys), ["Difss"] = new JObject() }; foreach (var key in keys) { result["Difss"][key] = ExportDifs(obj.Difss[key]); } return result; } static JObject ExportDifs(Difs difs) { int xCount = difs.CountX; int yCount = difs.CountY; var result = new JObject { ["Tag"] = difs.Tag ?? "", ["ValueX"] = difs.ValueX, ["ValueY"] = difs.ValueY, ["CountX"] = xCount, ["CountY"] = yCount, ["Difs"] = new JArray() }; for (int x = 0; x < xCount; x++) { var dif = difs[x]; var difArr = new JArray(); for (int y = 0; y < dif.Count; y++) { difArr.Add(ExportPars(dif[y])); } ((JArray)result["Difs"]).Add(difArr); } return result; } static JObject ExportPars(PartGroup PartGroup) { var result = new JObject { ["Tag"] = PartGroup.Tag ?? "", ["Children"] = new JObject() }; foreach (string key in PartGroup.Keys) { var val = PartGroup[key]; if (val is PartGroup childPars) result["Children"][key] = ExportPars(childPars); else if (val is ShapePartT parT) result["Children"][key] = ExportParT(parT); else if (val is ShapePart par) result["Children"][key] = ExportPar(par); else result["Children"][key] = val?.ToString() ?? "null"; } return result; } static JObject ExportPar(ShapePart ShapePart) { var result = new JObject { ["Tag"] = ShapePart.Tag ?? "", ["Dra"] = ShapePart.Dra, ["PenWidth"] = ShapePart.PenWidth, ["Closed"] = ShapePart.Closed, ["BasePoint"] = ExportVec(ShapePart.BasePoint), ["Position"] = ExportVec(ShapePart.Position), ["Angle"] = ShapePart.Angle, ["Size"] = ShapePart.Size, ["SizeX"] = ShapePart.SizeX, ["SizeY"] = ShapePart.SizeY, ["Out"] = new JArray() }; foreach (var outObj in ShapePart.OP) { var outJ = new JObject { ["Tension"] = outObj.Tension, ["Outline"] = outObj.Outline, ["Points"] = new JArray() }; foreach (var pt in outObj.ps) ((JArray)outJ["Points"]).Add(ExportVec(pt)); ((JArray)result["Out"]).Add(outJ); } if (ShapePart.JP.Count > 0) { result["Joints"] = new JArray(); foreach (var joi in ShapePart.JP) ((JArray)result["Joints"]).Add(ExportVec(joi.Joint)); } return result; } static JObject ExportParT(ShapePartT ShapePartT) { var result = ExportPar(ShapePartT); result["Text"] = ShapePartT.Text ?? ""; result["FontSize"] = ShapePartT.FontSize; return result; } static JObject ExportVec(Vector2D v) => new JObject { ["X"] = v.X, ["Y"] = v.Y }; }