diff --git a/Ultima/AnimationEdit.cs b/Ultima/AnimationEdit.cs index db94d8fa..f19d319b 100644 --- a/Ultima/AnimationEdit.cs +++ b/Ultima/AnimationEdit.cs @@ -12,12 +12,17 @@ public sealed class AnimationEdit private static FileIndex _fileIndex3 = new FileIndex("Anim3.idx", "Anim3.mul", -1); private static FileIndex _fileIndex4 = new FileIndex("Anim4.idx", "Anim4.mul", -1); private static FileIndex _fileIndex5 = new FileIndex("Anim5.idx", "Anim5.mul", -1); + + // Custom universal 175-slot block anim + private static FileIndex _fileIndex6 = new FileIndex("anim_custom.idx", "anim_custom.mul", -1); private static AnimIdx[] _animCache; private static AnimIdx[] _animCache2; private static AnimIdx[] _animCache3; private static AnimIdx[] _animCache4; private static AnimIdx[] _animCache5; + // Ne cache for case 6: custom universal 175-slot block anim + private static AnimIdx[] _animCache6; static AnimationEdit() { @@ -50,6 +55,12 @@ private static void InitializeCache() { _animCache5 = new AnimIdx[_fileIndex5.IdxLength / 12]; } + + // Init cache for custom anim + if (_fileIndex6 != null && _fileIndex6.IdxLength > 0) + { + _animCache6 = new AnimIdx[_fileIndex6.IdxLength / 12]; + } } /// @@ -62,6 +73,8 @@ public static void Reload() _fileIndex3 = new FileIndex("Anim3.idx", "Anim3.mul", -1); _fileIndex4 = new FileIndex("Anim4.idx", "Anim4.mul", -1); _fileIndex5 = new FileIndex("Anim5.idx", "Anim5.mul", -1); + + _fileIndex6 = new FileIndex("anim_custom.idx", "anim_custom.mul", -1); InitializeCache(); } @@ -147,6 +160,12 @@ private static void GetFileIndex( index = 35000 + ((body - 400) * 175); } + break; + // NEW universal custom: anim_custom + case 6: + fileIndex = _fileIndex6; + index = body * 175; // universal block + break; } @@ -176,6 +195,8 @@ private static AnimIdx[] GetCache(int fileType) return _animCache4; case 5: return _animCache5; + case 6: + return _animCache6; default: return _animCache; } @@ -184,7 +205,7 @@ private static AnimIdx[] GetCache(int fileType) public static AnimIdx GetAnimation(int fileType, int body, int action, int dir) { AnimIdx[] cache = GetCache(fileType); - + GetFileIndex(body, fileType, action, dir, out FileIndex fileIndex, out int index); if (cache?[index] != null) @@ -193,7 +214,7 @@ public static AnimIdx GetAnimation(int fileType, int body, int action, int dir) } return cache[index] = new AnimIdx(index, fileIndex); - } + } public static bool IsActionDefined(int fileType, int body, int action) { @@ -215,13 +236,48 @@ public static bool IsActionDefined(int fileType, int body, int action) bool valid = fileIndex.Valid(index, out int length, out int _, out bool _); return valid && length >= 1; - } + } - public static void LoadFromVD(int fileType, int body, BinaryReader bin) + public static void SetBodyType(int fileType, int body, int bodyType) + { + AnimIdx[] cache = GetCache(fileType); + GetFileIndex(body, fileType, 0, 0, out FileIndex fileIndex, out int startIndex); + + int totalSlots = 35 * 5; // universal block + for (int i = 0; i < totalSlots; i++) + { + int idx = startIndex + i; + + // Materialize a minimal AnimIdx if missing so Save() will persist Extra + if (cache[idx] == null) + { + cache[idx] = AnimIdx.CreateEmpty(); + } + + cache[idx].BodyType = bodyType; + } + } + + public static void LoadFromVD(int fileType, int body, BinaryReader bin, int animType) { AnimIdx[] cache = GetCache(fileType); GetFileIndex(body, fileType, 0, 0, out FileIndex _, out int index); - int animLength = Animations.GetAnimLength(body, fileType) * 5; + + // Calculate the correct animLength based on the TYPE FROM THE *.VD FILE, not the destination slot. + int animLength; + switch (animType) + { + case 0: // High Detail Monster + animLength = 22 * 5; + break; + case 1: // Low Detail Animal + animLength = 13 * 5; + break; + default: // People/Equipment + animLength = 35 * 5; + break; + } + var entries = new Entry3D[animLength]; for (int i = 0; i < animLength; ++i) @@ -237,6 +293,10 @@ public static void LoadFromVD(int fileType, int body, BinaryReader bin) { bin.BaseStream.Seek(entry.Lookup, SeekOrigin.Begin); cache[index] = new AnimIdx(bin, entry.Extra); + if (fileType == 6) + { + cache[index].BodyType = animType; // Set the type! + } } ++index; } @@ -246,26 +306,34 @@ public static void ExportToVD(int fileType, int body, string file) { AnimIdx[] cache = GetCache(fileType); GetFileIndex(body, fileType, 0, 0, out FileIndex fileIndex, out int index); + using (var fs = new FileStream(file, FileMode.Create, FileAccess.Write, FileShare.Write)) using (var bin = new BinaryWriter(fs)) { bin.Write((short)6); + int animLength = Animations.GetAnimLength(body, fileType); int currType = animLength == 22 ? 0 : animLength == 13 ? 1 : 2; + + // Optional: prefer cached BodyType for the universal custom file (type 6) + // Ensures VD header reflects actual cached BodyType, even if GetAnimLength + // didn't infer it yet for a newly created empty slot that just got BodyType set. + AnimIdx animProbe = cache[index] ?? (cache[index] = new AnimIdx(index, fileIndex)); + if (fileType == 6 && animProbe != null) + currType = animProbe.BodyType; + bin.Write((short)currType); + long indexPos = bin.BaseStream.Position; long animPos = bin.BaseStream.Position + (12 * animLength * 5); + for (int i = index; i < index + (animLength * 5); i++) { AnimIdx anim; if (cache != null) - { anim = cache[i] != null ? cache[i] : cache[i] = new AnimIdx(i, fileIndex); - } else - { anim = cache[i] = new AnimIdx(i, fileIndex); - } if (anim == null) { @@ -316,6 +384,11 @@ public static void Save(int fileType, string path) cache = _animCache5; fileIndex = _fileIndex5; break; + case 6: + filename = "anim_custom"; + cache = _animCache6; + fileIndex = _fileIndex6; + break; } string idx = Path.Combine(path, filename + ".idx"); @@ -326,6 +399,7 @@ public static void Save(int fileType, string path) using (var binidx = new BinaryWriter(fsidx)) using (var binmul = new BinaryWriter(fsmul)) { + AnimIdx.fileType = fileType; for (int idxc = 0; idxc < cache.Length; ++idxc) { AnimIdx anim; @@ -355,6 +429,9 @@ public static void Save(int fileType, string path) public sealed class AnimIdx { + public int BodyType { get; set; } + public static int fileType { get; set; } + public readonly int PaletteCapacity = 0x100; private readonly int _idxExtra; @@ -373,6 +450,7 @@ public AnimIdx(int index, FileIndex fileIndex) } _idxExtra = extra; + this.BodyType = _idxExtra; // This is for anim_custom using (var bin = new BinaryReader(stream)) { @@ -432,6 +510,25 @@ public AnimIdx(BinaryReader bin, int extra) } } + public static AnimIdx CreateEmpty() + { + var ai = new AnimIdx(true); + ai.Palette = new ushort[ai.PaletteCapacity]; + // Fill palette with transparent (0x8000) + for (int i = 0; i < ai.PaletteCapacity; i++) + ai.Palette[i] = 0x8000; + + ai.Frames = new List(); // no frames + ai.BodyType = 2; // default P; caller will override + return ai; + } + + // Private ctor used only by CreateEmpty() + private AnimIdx(bool _) + { + // intentionally empty; fields are assigned by the factory + } + public unsafe Bitmap[] GetFrames() { if ((Frames == null) || (Frames.Count == 0)) @@ -617,7 +714,15 @@ public void Save(BinaryWriter bin, BinaryWriter idx) start = bin.BaseStream.Position - start; idx.Write((int)start); - idx.Write(_idxExtra); + if (fileType == 6) + { + // Write our stored BodyType property into the 'extra' field on disk. + idx.Write(this.BodyType); + } + else + { + idx.Write(_idxExtra); + } } public void ExportToVD(BinaryWriter bin, ref long indexpos, ref long animpos) diff --git a/Ultima/Animations.cs b/Ultima/Animations.cs index f7ff06a0..cb0b4ca1 100644 --- a/Ultima/Animations.cs +++ b/Ultima/Animations.cs @@ -14,6 +14,9 @@ public static class Animations private static FileIndex _fileIndex3 = new FileIndex("Anim3.idx", "Anim3.mul", 0x20000, -1); private static FileIndex _fileIndex4 = new FileIndex("Anim4.idx", "Anim4.mul", 0x20000, -1); private static FileIndex _fileIndex5 = new FileIndex("Anim5.idx", "Anim5.mul", 0x20000, -1); + + // Custom universal 175-slot block anim + private static FileIndex _fileIndex6 = new FileIndex("anim_custom.idx", "anim_custom.mul", 0x20000, -1); private static byte[] _streamBuffer; @@ -27,6 +30,9 @@ public static void Reload() _fileIndex3 = new FileIndex("Anim3.idx", "Anim3.mul", 0x20000, -1); _fileIndex4 = new FileIndex("Anim4.idx", "Anim4.mul", 0x20000, -1); _fileIndex5 = new FileIndex("Anim5.idx", "Anim5.mul", 0x20000, -1); + + // Custom universal 175-slot block anim + _fileIndex6 = new FileIndex("anim_custom.idx", "anim_custom.mul", 0x20000, -1); BodyConverter.Initialize(); BodyTable.Initialize(); @@ -313,6 +319,13 @@ public static int GetAnimCount(int fileType) return 400 + ((int)(_fileIndex4.IdxLength - (35000 * 12)) / (12 * 175)); case 5: return 400 + ((int)(_fileIndex5.IdxLength - (35000 * 12)) / (12 * 175)); + case 6: + if (_fileIndex6 != null && _fileIndex6.IdxLength > 0) + { + // Calculate total bodies based on the universal block size. + return (int)(_fileIndex6.IdxLength / (175 * 12)); + } + return 0; } } @@ -384,6 +397,34 @@ public static int GetAnimLength(int body, int fileType) length = 35; } + break; + case 6: + GetFileIndex(body, 0, 0, fileType, out FileIndex fileIndex, out int index); + + if (!fileIndex.Valid(index, out length, out int extra, out _)) + { + length = 0; + } + + if (length < 0) + { + length = 0; + } + + // The 'extra' field now dictates the animation type. + // 0=H, 1=L, 2=P. + if (extra == 0) + { + length = 22; // High Detail (Monster)} + } + else if (extra == 1) + { + length = 13; // Low Detail (Animal) + } + else + { + length = 35; // People/Equipment + } break; } return length; @@ -478,6 +519,12 @@ private static void GetFileIndex(int body, int action, int direction, int fileTy index = 35000 + ((body - 400) * 175); } + break; + // NEW universal custom: anim_custom + case 6: + fileIndex = _fileIndex6; + index = body * 175; // universal block + break; } diff --git a/Ultima/Files.cs b/Ultima/Files.cs index 1144682c..a285dc0a 100644 --- a/Ultima/Files.cs +++ b/Ultima/Files.cs @@ -46,6 +46,8 @@ public static void FireFileSaveEvent() "anim4.mul", "anim5.idx", "anim5.mul", + "anim_custom.idx", + "anim_custom.mul", "animdata.mul", "art.mul", "artidx.mul", diff --git a/UoFiddler.Controls/Forms/AnimationEditForm.Designer.cs b/UoFiddler.Controls/Forms/AnimationEditForm.Designer.cs index 6bc5074f..f6c465f3 100644 --- a/UoFiddler.Controls/Forms/AnimationEditForm.Designer.cs +++ b/UoFiddler.Controls/Forms/AnimationEditForm.Designer.cs @@ -44,6 +44,10 @@ private void InitializeComponent() fromvdToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); exportToolStripMenuItem1 = new System.Windows.Forms.ToolStripMenuItem(); tovdToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); + convertToolStripMenuItem1 = new System.Windows.Forms.ToolStripMenuItem(); + lLowAnimalsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); + hHighMonstersToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); + pEquipmentPeoplesToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); AnimationFileToolStrip = new System.Windows.Forms.ToolStrip(); SelectFileToolStripComboBox = new System.Windows.Forms.ToolStripComboBox(); tableLayoutPanel1 = new System.Windows.Forms.TableLayoutPanel(); @@ -222,21 +226,22 @@ private void InitializeComponent() // ContextMenuStripTreeView // ContextMenuStripTreeView.ImageScalingSize = new System.Drawing.Size(20, 20); - ContextMenuStripTreeView.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { addToolStripMenuItem, removeToolStripMenuItem, extractImagesToolStripMenuItem1, importToolStripMenuItem1, exportToolStripMenuItem1 }); + ContextMenuStripTreeView.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { addToolStripMenuItem, removeToolStripMenuItem, extractImagesToolStripMenuItem1, importToolStripMenuItem1, exportToolStripMenuItem1, convertToolStripMenuItem1 }); ContextMenuStripTreeView.Name = "contextMenuStrip2"; - ContextMenuStripTreeView.Size = new System.Drawing.Size(158, 114); + ContextMenuStripTreeView.Size = new System.Drawing.Size(181, 158); + ContextMenuStripTreeView.Opening += ContextMenuStripTreeView_Opening; // // addToolStripMenuItem // addToolStripMenuItem.Name = "addToolStripMenuItem"; - addToolStripMenuItem.Size = new System.Drawing.Size(157, 22); + addToolStripMenuItem.Size = new System.Drawing.Size(180, 22); addToolStripMenuItem.Text = "Replace"; addToolStripMenuItem.Visible = false; // // removeToolStripMenuItem // removeToolStripMenuItem.Name = "removeToolStripMenuItem"; - removeToolStripMenuItem.Size = new System.Drawing.Size(157, 22); + removeToolStripMenuItem.Size = new System.Drawing.Size(180, 22); removeToolStripMenuItem.Text = "Remove"; removeToolStripMenuItem.Click += OnClickRemoveAction; // @@ -244,7 +249,7 @@ private void InitializeComponent() // extractImagesToolStripMenuItem1.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { asBmpToolStripMenuItem, asTiffToolStripMenuItem, asJpgToolStripMenuItem, asPngToolStripMenuItem }); extractImagesToolStripMenuItem1.Name = "extractImagesToolStripMenuItem1"; - extractImagesToolStripMenuItem1.Size = new System.Drawing.Size(157, 22); + extractImagesToolStripMenuItem1.Size = new System.Drawing.Size(180, 22); extractImagesToolStripMenuItem1.Text = "Extract Images.."; // // asBmpToolStripMenuItem @@ -283,7 +288,7 @@ private void InitializeComponent() // importToolStripMenuItem1.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { fromvdToolStripMenuItem }); importToolStripMenuItem1.Name = "importToolStripMenuItem1"; - importToolStripMenuItem1.Size = new System.Drawing.Size(157, 22); + importToolStripMenuItem1.Size = new System.Drawing.Size(180, 22); importToolStripMenuItem1.Text = "Import.."; // // fromvdToolStripMenuItem @@ -297,16 +302,47 @@ private void InitializeComponent() // exportToolStripMenuItem1.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { tovdToolStripMenuItem }); exportToolStripMenuItem1.Name = "exportToolStripMenuItem1"; - exportToolStripMenuItem1.Size = new System.Drawing.Size(157, 22); + exportToolStripMenuItem1.Size = new System.Drawing.Size(180, 22); exportToolStripMenuItem1.Text = "Export.."; // // tovdToolStripMenuItem // tovdToolStripMenuItem.Name = "tovdToolStripMenuItem"; - tovdToolStripMenuItem.Size = new System.Drawing.Size(105, 22); + tovdToolStripMenuItem.Size = new System.Drawing.Size(106, 22); tovdToolStripMenuItem.Text = "To .vd"; tovdToolStripMenuItem.Click += OnClickExportToVD; // + // convertToolStripMenuItem1 + // + convertToolStripMenuItem1.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { lLowAnimalsToolStripMenuItem, hHighMonstersToolStripMenuItem, pEquipmentPeoplesToolStripMenuItem }); + convertToolStripMenuItem1.Name = "convertToolStripMenuItem1"; + convertToolStripMenuItem1.Size = new System.Drawing.Size(180, 22); + convertToolStripMenuItem1.Text = "Convert.."; + // + // lLowAnimalsToolStripMenuItem + // + lLowAnimalsToolStripMenuItem.Name = "lLowAnimalsToolStripMenuItem"; + lLowAnimalsToolStripMenuItem.Size = new System.Drawing.Size(200, 22); + lLowAnimalsToolStripMenuItem.Tag = "convertLow"; + lLowAnimalsToolStripMenuItem.Text = "L : Low (Animals)"; + lLowAnimalsToolStripMenuItem.Click += OnClickConvertType; + // + // hHighMonstersToolStripMenuItem + // + hHighMonstersToolStripMenuItem.Name = "hHighMonstersToolStripMenuItem"; + hHighMonstersToolStripMenuItem.Size = new System.Drawing.Size(200, 22); + hHighMonstersToolStripMenuItem.Tag = "convertHigh"; + hHighMonstersToolStripMenuItem.Text = "H : High (Monsters)"; + hHighMonstersToolStripMenuItem.Click += OnClickConvertType; + // + // pEquipmentPeoplesToolStripMenuItem + // + pEquipmentPeoplesToolStripMenuItem.Name = "pEquipmentPeoplesToolStripMenuItem"; + pEquipmentPeoplesToolStripMenuItem.Size = new System.Drawing.Size(200, 22); + pEquipmentPeoplesToolStripMenuItem.Tag = "convertEquipment"; + pEquipmentPeoplesToolStripMenuItem.Text = "P : Equipment (Peoples)"; + pEquipmentPeoplesToolStripMenuItem.Click += OnClickConvertType; + // // AnimationFileToolStrip // AnimationFileToolStrip.ImageScalingSize = new System.Drawing.Size(20, 20); @@ -320,7 +356,7 @@ private void InitializeComponent() // // SelectFileToolStripComboBox // - SelectFileToolStripComboBox.Items.AddRange(new object[] { "Choose anim file", "anim", "anim2", "anim3", "anim4", "anim5" }); + SelectFileToolStripComboBox.Items.AddRange(new object[] { "Choose anim file", "anim", "anim2", "anim3", "anim4", "anim5", "anim_custom" }); SelectFileToolStripComboBox.Name = "SelectFileToolStripComboBox"; SelectFileToolStripComboBox.Size = new System.Drawing.Size(140, 25); SelectFileToolStripComboBox.SelectedIndexChanged += OnAnimChanged; @@ -1561,5 +1597,9 @@ private void InitializeComponent() private System.Windows.Forms.GroupBox groupBox3; private System.Windows.Forms.TableLayoutPanel tableLayoutPanel1; private System.Windows.Forms.TableLayoutPanel tableLayoutPanel2; + private System.Windows.Forms.ToolStripMenuItem convertToolStripMenuItem1; + private System.Windows.Forms.ToolStripMenuItem lLowAnimalsToolStripMenuItem; + private System.Windows.Forms.ToolStripMenuItem hHighMonstersToolStripMenuItem; + private System.Windows.Forms.ToolStripMenuItem pEquipmentPeoplesToolStripMenuItem; } } \ No newline at end of file diff --git a/UoFiddler.Controls/Forms/AnimationEditForm.cs b/UoFiddler.Controls/Forms/AnimationEditForm.cs index 65e406d8..26bf5581 100644 --- a/UoFiddler.Controls/Forms/AnimationEditForm.cs +++ b/UoFiddler.Controls/Forms/AnimationEditForm.cs @@ -61,23 +61,7 @@ public AnimationEditForm() private readonly string[][] _animNames = { - new string[] - { - "Walk", - "Run", - "Idle", - "Eat", - "Alert", - "Attack1", - "Attack2", - "GetHit", - "Die1", - "Idle", - "Fidget", - "LieDown", - "Die2" - }, //animal - new string[] + new string[] // Index 0: High Detail (Monster) { "Walk", "Idle", @@ -101,8 +85,24 @@ public AnimationEditForm() "Fly", "TakeOff", "GetHitInAir" - }, //Monster - new string[] + }, + new string[] // Index 1: Low Detail (Animal) + { + "Walk", + "Run", + "Idle", + "Eat", + "Alert", + "Attack1", + "Attack2", + "GetHit", + "Die1", + "Idle", + "Fidget", + "LieDown", + "Die2" + }, + new string[] // Index 2: People/Equipment { "Walk_01", "WalkStaff_01", @@ -139,13 +139,13 @@ public AnimationEditForm() "Bow_Lesser_01", "Salute_Armed1h_01", "Ingest_Eat_01" - } //human + } }; private void OnLoad(object sender, EventArgs e) { Options.LoadedUltimaClass["AnimationEdit"] = true; - + AnimationListTreeView.BeginUpdate(); try { @@ -156,46 +156,118 @@ private void OnLoad(object sender, EventArgs e) TreeNode[] nodes = new TreeNode[count]; for (int i = 0; i < count; ++i) { - int animLength = Animations.GetAnimLength(i, _fileType); - string type = animLength == 22 ? "H" : animLength == 13 ? "L" : "P"; - TreeNode node = new TreeNode - { - Tag = i, - Text = $"{type}: {i} ({BodyConverter.GetTrueBody(_fileType, i)})" - }; + int animLength; + string type; + int namesIndex; - bool valid = false; - for (int j = 0; j < animLength; ++j) + if (_fileType == 6) { - TreeNode treeNode = new TreeNode + // Universal 175 mode: read BodyType from cache/idx extra if present, else default to People(2) + AnimIdx currentAnim = AnimationEdit.GetAnimation(_fileType, i, 0, 0); + int bodyType = (currentAnim != null && currentAnim.Frames != null) ? currentAnim.BodyType : 2; + + switch (bodyType) + { + case 0: // High (Monsters) + animLength = 22; + type = "H"; + namesIndex = 0; // _animNames should be "High (Monster)" set[0] + break; + case 1: // Low (Animals) + animLength = 13; + type = "L"; + namesIndex = 1; // _animNames should be "Low (Animal)" set[1] + break; + default: // People/Equipment + animLength = 35; + type = "P"; + namesIndex = 2; // _animNames should be "People/Equipment" set[2] + break; + } + + TreeNode node = new TreeNode { - Tag = j, - Text = string.Format("{0:D2} {1}", j, _animNames[animLength == 22 ? 1 : animLength == 13 ? 0 : 2][j]) + Tag = i, + Text = $"{type}: {i} ({BodyConverter.GetTrueBody(_fileType, i)})" }; - if (AnimationEdit.IsActionDefined(_fileType, i, j)) + bool valid = false; + for (int j = 0; j < animLength; ++j) { - valid = true; + TreeNode treeNode = new TreeNode + { + Tag = j, + Text = string.Format("{0:D2} {1}", j, _animNames[namesIndex][j]) + }; + + if (AnimationEdit.IsActionDefined(_fileType, i, j)) + { + valid = true; + } + else + { + treeNode.ForeColor = Color.Red; + } + + node.Nodes.Add(treeNode); } - else + + if (!valid) { - treeNode.ForeColor = Color.Red; + if (_showOnlyValid) + { + continue; + } + node.ForeColor = Color.Red; } - node.Nodes.Add(treeNode); + nodes[i] = node; } - - if (!valid) + else { - if (_showOnlyValid) + // ORIGINAL LOGIC for legacy file types (1..5) + animLength = Animations.GetAnimLength(i, _fileType); + type = animLength == 22 ? "H" : animLength == 13 ? "L" : "P"; + + TreeNode node = new TreeNode { - continue; + Tag = i, + Text = $"{type}: {i} ({BodyConverter.GetTrueBody(_fileType, i)})" + }; + + bool valid = false; + for (int j = 0; j < animLength; ++j) + { + namesIndex = animLength == 22 ? 0 : animLength == 13 ? 1 : 2; + TreeNode treeNode = new TreeNode + { + Tag = j, + Text = string.Format("{0:D2} {1}", j, _animNames[namesIndex][j]) + }; + + if (AnimationEdit.IsActionDefined(_fileType, i, j)) + { + valid = true; + } + else + { + treeNode.ForeColor = Color.Red; + } + + node.Nodes.Add(treeNode); } - node.ForeColor = Color.Red; - } + if (!valid) + { + if (_showOnlyValid) + { + continue; + } + node.ForeColor = Color.Red; + } - nodes[i] = node; + nodes[i] = node; + } } AnimationListTreeView.Nodes.AddRange(nodes.Where(n => n != null).ToArray()); @@ -217,6 +289,7 @@ private void OnLoad(object sender, EventArgs e) } _loaded = true; + } private void OnFilePathChangeEvent() @@ -244,6 +317,65 @@ private TreeNode GetNode(int tag) : AnimationListTreeView.Nodes[tag]; } + + private void RefreshNode(int bodyId, int newBodyType) + { + TreeNode node = GetNode(bodyId); + if (node == null) + { + return; // Safety check + } + + // 1. Determine the correct UI properties based on the new type. + string typePrefix; + int animLength; + switch (newBodyType) + { + case 0: // High + typePrefix = "H"; + animLength = 22; + break; + case 1: // Low + typePrefix = "L"; + animLength = 13; + break; + default: // People + typePrefix = "P"; + animLength = 35; + break; + } + + // 2. Update the main node's text (e.g., "P: 1" becomes "H: 1"). + node.Text = $"{typePrefix}: {bodyId} ({BodyConverter.GetTrueBody(_fileType, bodyId)})"; + + // 3. Clear the old child nodes (the 35 "People" actions). + node.Nodes.Clear(); + + // 4. Rebuild the child nodes with the correct new actions. + bool valid = false; + for (int j = 0; j < animLength; ++j) + { + TreeNode newActionNode = new TreeNode + { + Tag = j, + Text = string.Format("{0:D2} {1}", j, _animNames[newBodyType][j]) + }; + + if (AnimationEdit.IsActionDefined(_fileType, bodyId, j)) + { + valid = true; + } + else + { + newActionNode.ForeColor = Color.Red; + } + node.Nodes.Add(newActionNode); + } + + // 5. Update the main node's color to show if it has any valid frames. + node.ForeColor = valid ? Color.Black : Color.Red; + } + private unsafe void SetPaletteBox() { if (_fileType == 0) @@ -375,7 +507,7 @@ private void DrawFrameItem(object sender, DrawListViewItemEventArgs e) Bitmap bmp = currentBits[(int)e.Item.Tag]; var penColor = FramesListView.SelectedItems.Contains(e.Item) ? Color.Red : Color.Gray; e.Graphics.DrawRectangle(new Pen(penColor), e.Bounds.X, e.Bounds.Y, e.Bounds.Width, e.Bounds.Height); - e.Graphics.DrawImage(bmp, e.Bounds.X, e.Bounds.Y, bmp.Width, bmp.Height); + e.Graphics.DrawImage(bmp, e.Bounds.X, e.Bounds.Y, bmp.Width, bmp.Height); e.DrawText(TextFormatFlags.Bottom | TextFormatFlags.HorizontalCenter); } @@ -967,8 +1099,8 @@ private void AddImageAtCertainIndex(int frameCount, Bitmap[] bitBmp, Bitmap bmp, { bitBmp[index] = new Bitmap(bmp.Width, bmp.Height, PixelFormat.Format16bppArgb1555); bmp.SelectActiveFrame(dimension, index); - bitBmp[index] = ConvertBmpAnim(bitBmp[index], (int) numericUpDownRed.Value, - (int) numericUpDownGreen.Value, (int) numericUpDownBlue.Value); + bitBmp[index] = ConvertBmpAnim(bitBmp[index], (int)numericUpDownRed.Value, + (int)numericUpDownGreen.Value, (int)numericUpDownBlue.Value); edit.AddFrame(bitBmp[index], bitBmp[index].Width / 2); TreeNode node = GetNode(_currentBody); if (node != null) @@ -1102,6 +1234,8 @@ private void OnClickImportPalette(object sender, EventArgs e) } } + // The exact replacement for OnClickImportFromVD (AnimationEditForm_NEW.cs) + // Replace the entire method with: private void OnClickImportFromVD(object sender, EventArgs e) { if (_fileType == 0) @@ -1112,7 +1246,7 @@ private void OnClickImportFromVD(object sender, EventArgs e) using (OpenFileDialog dialog = new OpenFileDialog()) { dialog.Multiselect = false; - dialog.Title = "Choose palette file"; + dialog.Title = "Choose VD file to import"; dialog.CheckFileExists = true; dialog.Filter = "vd files (*.vd)|*.vd"; if (dialog.ShowDialog() != DialogResult.OK) @@ -1120,67 +1254,74 @@ private void OnClickImportFromVD(object sender, EventArgs e) return; } - int animLength = Animations.GetAnimLength(_currentBody, _fileType); - int currentType; - if (animLength == 22) - { - currentType = 0; - } - else - { - currentType = animLength == 13 ? 1 : 2; - } - using (FileStream fs = new FileStream(dialog.FileName, FileMode.Open, FileAccess.Read, FileShare.Read)) { using (BinaryReader bin = new BinaryReader(fs)) { - int fileType = bin.ReadInt16(); - int animType = bin.ReadInt16(); - if (fileType != 6) + int fileVersion = bin.ReadInt16(); + int animTypeFromFile = bin.ReadInt16(); // 0=H,1=L,2=P + if (fileVersion != 6) { - MessageBox.Show("Not an Anim File.", "Import", MessageBoxButtons.OK, + MessageBox.Show("Not a valid VD Anim File.", "Import Error", MessageBoxButtons.OK, MessageBoxIcon.Information, MessageBoxDefaultButton.Button1); return; } - if (animType != currentType) + if (_fileType == 6) { - MessageBox.Show("Wrong Anim Id ( Type )", "Import", MessageBoxButtons.OK, + // Universal: accept the file’s type and persist it via AnimationEdit.LoadFromVD + AnimationEdit.LoadFromVD(_fileType, _currentBody, bin, animTypeFromFile); + Options.ChangedUltimaClass["Animations"] = true; + RefreshNode(_currentBody, animTypeFromFile); + AfterSelectTreeView(this, null); + MessageBox.Show("Finished importing from VD.", "Import Complete", MessageBoxButtons.OK, MessageBoxIcon.Information, MessageBoxDefaultButton.Button1); - return; - } - AnimationEdit.LoadFromVD(_fileType, _currentBody, bin); - } - } - - bool valid = false; - TreeNode node = GetNode(_currentBody); - if (node != null) - { - for (int j = 0; j < animLength; ++j) - { - if (AnimationEdit.IsActionDefined(_fileType, _currentBody, j)) - { - node.Nodes[j].ForeColor = Color.Black; - valid = true; } else { - node.Nodes[j].ForeColor = Color.Red; + // Legacy anim1..5: enforce slot’s current type + int animLength = Animations.GetAnimLength(_currentBody, _fileType); + int currentType = (animLength == 22) ? 0 : (animLength == 13 ? 1 : 2); + if (animTypeFromFile != currentType) + { + MessageBox.Show("Wrong Anim Id ( Type )", "Import", MessageBoxButtons.OK, + MessageBoxIcon.Information, MessageBoxDefaultButton.Button1); + return; + } + + AnimationEdit.LoadFromVD(_fileType, _currentBody, bin, animTypeFromFile); + + bool valid = false; + TreeNode node = GetNode(_currentBody); + if (node != null) + { + for (int j = 0; j < animLength; ++j) + { + if (AnimationEdit.IsActionDefined(_fileType, _currentBody, j)) + { + node.Nodes[j].ForeColor = Color.Black; + valid = true; + } + else + { + node.Nodes[j].ForeColor = Color.Red; + } + } + node.ForeColor = valid ? Color.Black : Color.Red; + } + + Options.ChangedUltimaClass["Animations"] = true; + AfterSelectTreeView(this, null); + + MessageBox.Show("Finished importing from VD.", "Import Complete", MessageBoxButtons.OK, + MessageBoxIcon.Information, MessageBoxDefaultButton.Button1); } } - node.ForeColor = valid ? Color.Black : Color.Red; } - - Options.ChangedUltimaClass["Animations"] = true; - AfterSelectTreeView(this, null); - - MessageBox.Show("Finished", "Import", MessageBoxButtons.OK, MessageBoxIcon.Information, - MessageBoxDefaultButton.Button1); } } + private void OnClickExportToVD(object sender, EventArgs e) { if (_fileType == 0) @@ -2049,9 +2190,9 @@ private void AddAnimationX1(Color customConvert, Bitmap bmp) for (int index = 0; index < frameCount; index++) { bitBmp[index].SelectActiveFrame(dimension, index); - bitBmp[index] = ConvertBmpAnim(bitBmp[index], (int) numericUpDownRed.Value, - (int) numericUpDownGreen.Value, (int) numericUpDownBlue.Value); - edit.AddFrame(bitBmp[index], bitBmp[index].Width/2); + bitBmp[index] = ConvertBmpAnim(bitBmp[index], (int)numericUpDownRed.Value, + (int)numericUpDownGreen.Value, (int)numericUpDownBlue.Value); + edit.AddFrame(bitBmp[index], bitBmp[index].Width / 2); TreeNode node = GetNode(_currentBody); if (node != null) { @@ -2429,9 +2570,9 @@ private AnimIdx Cv5AnimIdxPositions(int frameCount, Bitmap[] bitBmp, FrameDimens for (int index = frameCount / 8 * 4; index < frameCount / 8 * 5; index++) { bitBmp[index].SelectActiveFrame(dimension, index); - bitBmp[index] = ConvertBmpAnimCv5(bitBmp[index], (int) numericUpDownRed.Value, (int) numericUpDownGreen.Value, - (int) numericUpDownBlue.Value); - edit.AddFrame(bitBmp[index], bitBmp[index].Width/2); + bitBmp[index] = ConvertBmpAnimCv5(bitBmp[index], (int)numericUpDownRed.Value, (int)numericUpDownGreen.Value, + (int)numericUpDownBlue.Value); + edit.AddFrame(bitBmp[index], bitBmp[index].Width / 2); TreeNode node = GetNode(_currentBody); if (node != null) { @@ -2479,9 +2620,9 @@ private AnimIdx Cv5AnimIdxPositions(int frameCount, Bitmap[] bitBmp, FrameDimens for (int index = 0; index < frameCount / 8; index++) { bitBmp[index].SelectActiveFrame(dimension, index); - bitBmp[index] = ConvertBmpAnimCv5(bitBmp[index], (int) numericUpDownRed.Value, (int) numericUpDownGreen.Value, - (int) numericUpDownBlue.Value); - edit.AddFrame(bitBmp[index], bitBmp[index].Width/2); + bitBmp[index] = ConvertBmpAnimCv5(bitBmp[index], (int)numericUpDownRed.Value, (int)numericUpDownGreen.Value, + (int)numericUpDownBlue.Value); + edit.AddFrame(bitBmp[index], bitBmp[index].Width / 2); TreeNode node = GetNode(_currentBody); if (node != null) { @@ -2529,9 +2670,9 @@ private AnimIdx Cv5AnimIdxPositions(int frameCount, Bitmap[] bitBmp, FrameDimens for (int index = frameCount / 8 * 5; index < frameCount / 8 * 6; index++) { bitBmp[index].SelectActiveFrame(dimension, index); - bitBmp[index] = ConvertBmpAnimCv5(bitBmp[index], (int) numericUpDownRed.Value, (int) numericUpDownGreen.Value, - (int) numericUpDownBlue.Value); - edit.AddFrame(bitBmp[index], bitBmp[index].Width/2); + bitBmp[index] = ConvertBmpAnimCv5(bitBmp[index], (int)numericUpDownRed.Value, (int)numericUpDownGreen.Value, + (int)numericUpDownBlue.Value); + edit.AddFrame(bitBmp[index], bitBmp[index].Width / 2); TreeNode node = GetNode(_currentBody); if (node != null) { @@ -2579,9 +2720,9 @@ private AnimIdx Cv5AnimIdxPositions(int frameCount, Bitmap[] bitBmp, FrameDimens for (int index = frameCount / 8 * 1; index < frameCount / 8 * 2; index++) { bitBmp[index].SelectActiveFrame(dimension, index); - bitBmp[index] = ConvertBmpAnimCv5(bitBmp[index], (int) numericUpDownRed.Value, (int) numericUpDownGreen.Value, - (int) numericUpDownBlue.Value); - edit.AddFrame(bitBmp[index], bitBmp[index].Width/2); + bitBmp[index] = ConvertBmpAnimCv5(bitBmp[index], (int)numericUpDownRed.Value, (int)numericUpDownGreen.Value, + (int)numericUpDownBlue.Value); + edit.AddFrame(bitBmp[index], bitBmp[index].Width / 2); TreeNode node = GetNode(_currentBody); if (node != null) { @@ -2629,9 +2770,9 @@ private AnimIdx Cv5AnimIdxPositions(int frameCount, Bitmap[] bitBmp, FrameDimens for (int index = frameCount / 8 * 6; index < frameCount / 8 * 7; index++) { bitBmp[index].SelectActiveFrame(dimension, index); - bitBmp[index] = ConvertBmpAnimCv5(bitBmp[index], (int) numericUpDownRed.Value, (int) numericUpDownGreen.Value, - (int) numericUpDownBlue.Value); - edit.AddFrame(bitBmp[index], bitBmp[index].Width/2); + bitBmp[index] = ConvertBmpAnimCv5(bitBmp[index], (int)numericUpDownRed.Value, (int)numericUpDownGreen.Value, + (int)numericUpDownBlue.Value); + edit.AddFrame(bitBmp[index], bitBmp[index].Width / 2); TreeNode node = GetNode(_currentBody); if (node != null) { @@ -3165,9 +3306,9 @@ private AnimIdx KrAnimIdxPositions(int frameCount, Bitmap[] bitBmp, FrameDimensi for (int index = frameCount / 5 * 0; index < frameCount / 5 * 1; index++) { bitBmp[index].SelectActiveFrame(dimension, index); - bitBmp[index] = ConvertBmpAnimKr(bitBmp[index], (int) numericUpDownRed.Value, (int) numericUpDownGreen.Value, - (int) numericUpDownBlue.Value); - edit.AddFrame(bitBmp[index], bitBmp[index].Width/2); + bitBmp[index] = ConvertBmpAnimKr(bitBmp[index], (int)numericUpDownRed.Value, (int)numericUpDownGreen.Value, + (int)numericUpDownBlue.Value); + edit.AddFrame(bitBmp[index], bitBmp[index].Width / 2); TreeNode node = GetNode(_currentBody); if (node != null) { @@ -3215,9 +3356,9 @@ private AnimIdx KrAnimIdxPositions(int frameCount, Bitmap[] bitBmp, FrameDimensi for (int index = frameCount / 5 * 1; index < frameCount / 5 * 2; index++) { bitBmp[index].SelectActiveFrame(dimension, index); - bitBmp[index] = ConvertBmpAnimKr(bitBmp[index], (int) numericUpDownRed.Value, (int) numericUpDownGreen.Value, - (int) numericUpDownBlue.Value); - edit.AddFrame(bitBmp[index], bitBmp[index].Width/2); + bitBmp[index] = ConvertBmpAnimKr(bitBmp[index], (int)numericUpDownRed.Value, (int)numericUpDownGreen.Value, + (int)numericUpDownBlue.Value); + edit.AddFrame(bitBmp[index], bitBmp[index].Width / 2); TreeNode node = GetNode(_currentBody); if (node != null) { @@ -3265,9 +3406,9 @@ private AnimIdx KrAnimIdxPositions(int frameCount, Bitmap[] bitBmp, FrameDimensi for (int index = frameCount / 5 * 2; index < frameCount / 5 * 3; index++) { bitBmp[index].SelectActiveFrame(dimension, index); - bitBmp[index] = ConvertBmpAnimKr(bitBmp[index], (int) numericUpDownRed.Value, (int) numericUpDownGreen.Value, - (int) numericUpDownBlue.Value); - edit.AddFrame(bitBmp[index], bitBmp[index].Width/2); + bitBmp[index] = ConvertBmpAnimKr(bitBmp[index], (int)numericUpDownRed.Value, (int)numericUpDownGreen.Value, + (int)numericUpDownBlue.Value); + edit.AddFrame(bitBmp[index], bitBmp[index].Width / 2); TreeNode node = GetNode(_currentBody); if (node != null) { @@ -3315,9 +3456,9 @@ private AnimIdx KrAnimIdxPositions(int frameCount, Bitmap[] bitBmp, FrameDimensi for (int index = frameCount / 5 * 3; index < frameCount / 5 * 4; index++) { bitBmp[index].SelectActiveFrame(dimension, index); - bitBmp[index] = ConvertBmpAnimKr(bitBmp[index], (int) numericUpDownRed.Value, (int) numericUpDownGreen.Value, - (int) numericUpDownBlue.Value); - edit.AddFrame(bitBmp[index], bitBmp[index].Width/2); + bitBmp[index] = ConvertBmpAnimKr(bitBmp[index], (int)numericUpDownRed.Value, (int)numericUpDownGreen.Value, + (int)numericUpDownBlue.Value); + edit.AddFrame(bitBmp[index], bitBmp[index].Width / 2); TreeNode node = GetNode(_currentBody); if (node != null) { @@ -3365,9 +3506,9 @@ private AnimIdx KrAnimIdxPositions(int frameCount, Bitmap[] bitBmp, FrameDimensi for (int index = frameCount / 5 * 4; index < frameCount / 5 * 5; index++) { bitBmp[index].SelectActiveFrame(dimension, index); - bitBmp[index] = ConvertBmpAnimKr(bitBmp[index], (int) numericUpDownRed.Value, (int) numericUpDownGreen.Value, - (int) numericUpDownBlue.Value); - edit.AddFrame(bitBmp[index], bitBmp[index].Width/2); + bitBmp[index] = ConvertBmpAnimKr(bitBmp[index], (int)numericUpDownRed.Value, (int)numericUpDownGreen.Value, + (int)numericUpDownBlue.Value); + edit.AddFrame(bitBmp[index], bitBmp[index].Width / 2); TreeNode node = GetNode(_currentBody); if (node != null) { @@ -3892,7 +4033,7 @@ private void ConvertAndSetPaletteWithReducer() for (int x = 0; x < 5; x++) { AnimIdx edit = AnimationEdit.GetAnimation(_fileType, _currentBody, _currentAction, _currentDir); - PaletteReducer((int) numericUpDown6.Value, (int) numericUpDown7.Value, (int) numericUpDown8.Value, edit); + PaletteReducer((int)numericUpDown6.Value, (int)numericUpDown7.Value, (int)numericUpDown8.Value, edit); SetPaletteBox(); FramesListView.Invalidate(); Options.ChangedUltimaClass["Animations"] = true; @@ -4178,5 +4319,102 @@ public void PaletteReducer(int redP, int greenP, int blueP, AnimIdx animIdx) } } } + + private bool IsBodyEmpty(int fileType, int bodyId) + { + // An empty body means no actions are defined in any direction + for (int a = 0; a < 35; a++) + { + if (AnimationEdit.IsActionDefined(fileType, bodyId, a)) + return false; + } + return true; + } + + + private void OnClickConvertType(object sender, EventArgs e) + { + if (_fileType == 0) + return; + + // Ensure there is a selected top-level node (body) + if (AnimationListTreeView.SelectedNode == null || AnimationListTreeView.SelectedNode.Parent != null) + { + MessageBox.Show("Please select a body (top-level) node to convert.", "Convert", MessageBoxButtons.OK, MessageBoxIcon.Information); + return; + } + + int bodyId = (int)AnimationListTreeView.SelectedNode.Tag; + + // Only allow conversion when the body has no frames + if (!IsBodyEmpty(_fileType, bodyId)) + { + MessageBox.Show("Conversion is allowed only on empty entries.", "Convert", MessageBoxButtons.OK, MessageBoxIcon.Information); + return; + } + + // Determine requested target type from the menu item's Tag + if (sender is not ToolStripMenuItem mi || mi.Tag is not string tag) + return; + + int newBodyType = 2; // default P + switch (tag) + { + case "convertLow": // L : Low (Animals) + newBodyType = 1; + break; + case "convertHigh": // H : High (Monsters) + newBodyType = 0; + break; + case "convertEquipment": // P : Equipment (Peoples) + newBodyType = 2; + break; + default: + return; + } + + // Persist BodyType to cache/idx Extra without creating frames + AnimationEdit.SetBodyType(_fileType, bodyId, newBodyType); + + // Mark modified + Options.ChangedUltimaClass["Animations"] = true; + + // Fast, surgical UI update of only this node + RefreshNode(bodyId, newBodyType); + + // If this body is currently “active”, refresh the right panel + if (AnimationListTreeView.SelectedNode != null && + AnimationListTreeView.SelectedNode.Parent == null && + (int)AnimationListTreeView.SelectedNode.Tag == bodyId) + { + AfterSelectTreeView(this, null); + } + } + + private void ContextMenuStripTreeView_Opening(object sender, System.ComponentModel.CancelEventArgs e) + { + if (_fileType != 6) + { + convertToolStripMenuItem1.Visible = false; + return; + } + convertToolStripMenuItem1.Visible = true; + + // Default to disabled; we'll enable only for empty top-level nodes + bool enableConvert = false; + + TreeNode node = AnimationListTreeView.SelectedNode; + if (node != null && node.Parent == null) // body-level node + { + int bodyId = (int)node.Tag; + enableConvert = IsBodyEmpty(_fileType, bodyId); + } + + // Toggle Convert.. and its items + convertToolStripMenuItem1.Enabled = enableConvert; + lLowAnimalsToolStripMenuItem.Enabled = enableConvert; + hHighMonstersToolStripMenuItem.Enabled = enableConvert; + pEquipmentPeoplesToolStripMenuItem.Enabled = enableConvert; + } } } diff --git a/UoFiddler.Controls/Forms/AnimationEditForm.resx b/UoFiddler.Controls/Forms/AnimationEditForm.resx index e474dbfd..b9db0c78 100644 --- a/UoFiddler.Controls/Forms/AnimationEditForm.resx +++ b/UoFiddler.Controls/Forms/AnimationEditForm.resx @@ -129,12 +129,6 @@ 343, 17 - - 196, 12 - - - 448, 47 - 448, 47 @@ -142,15 +136,18 @@ iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAAL - DAAACwwBP0AiyAAAAUhJREFUeF7tlttpw1AQRAX5TQmpIi4k6STpxOnELsRVOB0kvwZnLkEgxMhrPb27 - dw4cMP7aEejYjRBCCCGEEEJM5Ahf/j/WyRX+wk/4VL6ojfIAWk9wB6ui+wCKF/gFn2EV9B9A6zd8h+lh - 47umjyQb3Td1JNngIVNGkg29ZbpIspH3mCaSbNwYw0eSjRpr6EiyQVMNGUk2ZI7hIslGLGGYSLLjl9R9 - JNnRS+s6kuzgtXQZSXbomrqLJDtyC91Ekh23hWf4Bh8OO25Nyyuwh1W+AiWCr9AV7NCl/YEfsMqfwQOs - 8o+Qm8hZsOPn6C5yFmzEVF1GzoINGavryFmwQWN0HzkLNuoew0TOgo27ZbjIWbCRQ4aMnAUb2jd05CzY - 4K7hI2fBRhfTRM6iPzxd5Cy641NGzqIMTx05i/SRE0IIIYQQQmSjaf4Ajal9ky/a+mMAAAAASUVORK5C + DAAACwwBP0AiyAAAAUhJREFUeF7tlsFNA0EQBFviSwiOAgKBTEwmkAkEQhQmA/gigfZxklWyPPad156d + 65LqY+4xfZILS8YYY4wxxhgzjw9JG364Jv4k/Uh6kXTHP66B9gImPyU98oHq7L+A5q+kN0n3fLAqfAGT + X5Ke+XBFOJyWjyQHH7J0JDn2mCUjyZGR5SLJgadaJpIcdq7DR5KD5jh0JDlmiUNGkiOWOlwkOeBSDhNJ + Hn5p00eSB/cwdSR5bE9TRpJH9jZdJHngtUwTSR52LXeSnnjMLeBhvW1fgde1fgVaBB94wK3hkT38lrRd + 67/B97X+EEoTuQgevtR0kYvggCWmjFwER8wxdeQiOOZc00cugoNOdZjIRXBY5HCRi+DAYw4ZuQiOPOTQ + kYvgWDp85CI4eLJM5CI4vFzkIvbHl4xcRBteOnIR5SNnjDHGGGOMqcc/jal9kyVCOmUAAAAASUVORK5C YII= + + 196, 12 + 309, 47