Files
PCK-Studio/PCK-Studio/Controls/Editor/PckEditor.cs
T
2026-06-29 01:02:21 -04:00

2599 lines
114 KiB
C#

using MetroFramework.Forms;
using OMI.Formats.Behaviour;
using OMI.Formats.Color;
using OMI.Formats.GameRule;
using OMI.Formats.Languages;
using OMI.Formats.Material;
using OMI.Formats.Model;
using OMI.Formats.Pck;
using OMI.Workers;
using OMI.Workers.Behaviour;
using OMI.Workers.Color;
using OMI.Workers.GameRule;
using OMI.Workers.Language;
using OMI.Workers.Material;
using OMI.Workers.Model;
using OMI.Workers.Pck;
using PckStudio.Core;
using PckStudio.Core.Deserializer;
using PckStudio.Core.Extensions;
using PckStudio.Core.FileFormats;
using PckStudio.Core.IO._3DST;
using PckStudio.Core.IO.PckAudio;
using PckStudio.Core.Json;
using PckStudio.Core.Misc;
using PckStudio.Core.Serializer;
using PckStudio.Core.Skin;
using PckStudio.Forms.Additional_Popups;
using PckStudio.Forms.Additional_Popups.Animation;
using PckStudio.Forms.Editor;
using PckStudio.Interfaces;
using PckStudio.Internal;
using PckStudio.Json;
using PckStudio.ModelSupport;
using PckStudio.Popups;
using PckStudio.Properties;
using PckStudio.Rendering;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.IO;
using System.Linq;
using System.Windows.Forms;
namespace PckStudio.Controls
{
internal partial class PckEditor : EditorControl<PackInfo>
{
private bool __modified = false;
private bool _wasModified
{
get => __modified;
set
{
if (__modified != value)
{
__modified = value;
_onModifiedChangeDelegate?.Invoke(value);
}
}
}
public int _packID = -1;
private delegate void OnModifiedChangeDelegate(bool state);
private OnModifiedChangeDelegate _onModifiedChangeDelegate;
private int _timesSaved = 0;
private readonly Dictionary<PckAssetType, Action<PckAsset>> _pckAssetTypeHandler;
public PckEditor(string name, PackInfo packInfo, ISaveContext<PackInfo> saveContext)
: base(name, packInfo, saveContext)
{
InitializeComponent();
_onModifiedChangeDelegate = OnModify;
if (EditorValue.File.TryGetAsset("0", PckAssetType.InfoFile, out PckAsset infoFile) && infoFile != null)
{
string packID;
if (infoFile.TryGetParameter("PACKID", out packID))
{
try
{
_packID = int.Parse(packID);
}
catch
{
_packID = -1;
}
}
EditorValue.File.RemoveAsset(infoFile);
}
BigEndianCheckBox.Visible = packInfo.AllowEndianSwap;
BigEndianCheckBox.Checked = packInfo.Endianness == OMI.ByteOrder.BigEndian;
treeViewMain.TreeViewNodeSorter = new PckNodeSorter();
treeViewMain.DrawMode = TreeViewDrawMode.OwnerDrawText;
treeViewMain.ImageList = new ImageList();
treeViewMain.ImageList.Images.Add(Resources.empty); // no image drawn to allow for custom drawing
int iconSize = 48;
treeViewMain.ImageList.ImageSize = new Size(iconSize, iconSize);
treeViewMain.DrawNode += (s, e) =>
{
e.DrawDefault = true;
Rectangle iconBounds = new Rectangle(
e.Bounds.Left - iconSize - 2,
e.Bounds.Top,
iconSize,
iconSize
);
Image customIcon = GetNodeIcon(e.Node.Tag is PckAsset asset ? asset : null);
if (customIcon != null)
{
e.Graphics.InterpolationMode = InterpolationMode.HighQualityBicubic; // bicubic to make it look better when downscaled
e.Graphics.DrawImage(customIcon, iconBounds);
}
};
skinToolStripMenuItem1.Click += (sender, e) => SetFileType(PckAssetType.SkinFile);
capeToolStripMenuItem.Click += (sender, e) => SetFileType(PckAssetType.CapeFile);
textureToolStripMenuItem.Click += (sender, e) => SetFileType(PckAssetType.TextureFile);
languagesFileLOCToolStripMenuItem.Click += (sender, e) => SetFileType(PckAssetType.LocalisationFile);
gameRulesFileGRFToolStripMenuItem.Click += (sender, e) => SetFileType(PckAssetType.GameRulesFile);
audioPCKFileToolStripMenuItem.Click += (sender, e) => SetFileType(PckAssetType.AudioFile);
coloursCOLFileToolStripMenuItem.Click += (sender, e) => SetFileType(PckAssetType.ColourTableFile);
gameRulesHeaderGRHToolStripMenuItem.Click += (sender, e) => SetFileType(PckAssetType.GameRulesHeader);
skinsPCKToolStripMenuItem.Click += (sender, e) => SetFileType(PckAssetType.SkinDataFile);
modelsFileBINToolStripMenuItem.Click += (sender, e) => SetFileType(PckAssetType.ModelsFile);
behavioursFileBINToolStripMenuItem.Click += (sender, e) => SetFileType(PckAssetType.BehavioursFile);
entityMaterialsFileBINToolStripMenuItem.Click += (sender, e) => SetFileType(PckAssetType.MaterialFile);
imageList.Images.Add("folderIcon", Resources.ZZFolder); // Icon for folders
imageList.Images.Add("classicSkinFileIcon", Resources.CLASSIC_SKIN_ICON); // Icon for Skin files (*.png)
imageList.Images.Add("modernSkinFileIcon", Resources.MODERN_SKIN_ICON); // Icon for Skin files with Modern ANIM flag (*.png)
imageList.Images.Add("slimSkinFileIcon", Resources.SLIM_SKIN_ICON); // Icon for Skin files with Slim ANIM flag (*.png)
imageList.Images.Add("capeFileIcon", Resources.CAPE_ICON); // Icon for Cape files (*.png)
imageList.Images.Add("textureFileIcon", Resources.TEXTURE_ICON); // Icon for Texture files (*.png;*.tga)
imageList.Images.Add("unknownFileIcon", Resources.ZUnknown); // Icon for Unknown formats and UIDataFile (never used in any game version)
imageList.Images.Add("infoFileIcon", Resources.INFO_ICON); // Icon for Info files (0)
imageList.Images.Add("pckFileIcon", Resources.PCK_ICON); // Icon for generic PCK files (*.pck)
imageList.Images.Add("localisationFileIcon", Resources.LOC_ICON); // Icon for string localization files (languages.loc;localisation.loc)
imageList.Images.Add("gameRulesFileIcon", Resources.GRF_ICON); // Icon for Game Rule files (*.grf)
imageList.Images.Add("audioFileIcon", Resources.BINKA_ICON); // Icon for music cue file (audio.pck)
imageList.Images.Add("colourTableFileIcon", Resources.COL_ICON); // Icon for color table files (colours.col)
imageList.Images.Add("gameRulesHeaderIcon", Resources.GRH_ICON); // Icon for Game Rule Header files (*.grh)
imageList.Images.Add("skinDataFileIcon", Resources.SKINS_ICON); // Icon for Skin.pck archives (skins.pck)
imageList.Images.Add("modelsFileIcon", Resources.MODELS_ICON); // Icon for Model files (models.bin)
imageList.Images.Add("behavioursFileIcon", Resources.BEHAVIOURS_ICON); // Icon for Behaviour files (behaviours.bin)
imageList.Images.Add("materialFileIcon", Resources.ENTITY_MATERIALS_ICON); // Icon for Entity Material files (entityMaterials.bin)
_pckAssetTypeHandler = new Dictionary<PckAssetType, Action<PckAsset>>(15)
{
[PckAssetType.SkinFile] = HandleSkinFile,
[PckAssetType.CapeFile] = null,
[PckAssetType.TextureFile] = HandleTextureFile,
[PckAssetType.UIDataFile] = _ => throw new NotSupportedException("unused in-game"),
[PckAssetType.InfoFile] = null,
[PckAssetType.TexturePackInfoFile] = null,
[PckAssetType.LocalisationFile] = HandleLocalisationFile,
[PckAssetType.GameRulesFile] = HandleGameRuleFile,
[PckAssetType.AudioFile] = HandleAudioFile,
[PckAssetType.ColourTableFile] = HandleColourFile,
[PckAssetType.GameRulesHeader] = HandleGameRuleFile,
[PckAssetType.SkinDataFile] = null,
[PckAssetType.ModelsFile] = HandleModelsFile,
[PckAssetType.BehavioursFile] = HandleBehavioursFile,
[PckAssetType.MaterialFile] = HandleMaterialFile,
};
BigEndianCheckBox.CheckedChanged += bigEndianCheckbox_CheckChanged;
}
private void TreeViewMain_DrawNode(object sender, DrawTreeNodeEventArgs e)
{
// If not selected, draw normally
if ((e.State & TreeNodeStates.Selected) == 0)
{
e.DrawDefault = true;
return;
}
// Draw background
e.Graphics.FillRectangle(SystemBrushes.Highlight, e.Bounds);
// Draw text
TextRenderer.DrawText(
e.Graphics,
e.Node.Text,
treeViewMain.Font,
e.Bounds,
SystemColors.HighlightText,
TextFormatFlags.GlyphOverhangPadding
);
}
protected override bool PreSave()
{
EditorValue.Endianness = BigEndianCheckBox.Checked ? OMI.ByteOrder.BigEndian : OMI.ByteOrder.LittleEndian;
// there should only ever by 1 but the game will only read the first one it finds. Check all of them to take potential race conditions or other reading errors or quirks with the game into account - May
foreach (PckAsset skinDataFile in EditorValue.File.GetAssetsByType(PckAssetType.SkinDataFile))
{
try
{
PckFile skinDataPck = new PckFileReader(EditorValue.Endianness).FromStream(new MemoryStream(skinDataFile.Data));
if (EditorValue.File.xmlVersion != 0 && skinDataPck.xmlVersion != EditorValue.File.xmlVersion)
{
MessageBox.Show(this, $"{skinDataFile.Filename} is a SkinData file set to a BOX version value that does not match the BOX version of {TitleName} and can crash the game. Either set the BOX version of this pck to 0 or the same value in \"PCK->Set Skin BOX Version\" or fix the BOX version of {skinDataFile.Filename} by using the context menu tool in \"File Functions->Set BOX Version\".", "Cannot save PCK file", MessageBoxButtons.OK, MessageBoxIcon.Error);
return false;
}
if (skinDataPck.GetAssetsByType(PckAssetType.SkinFile).Count() < 1)
{
MessageBox.Show(this, $"{skinDataFile.Filename} is a SkinData file but stores no skin files, which can cause the game to crash. If skins are already stored outside of this file then this file may be included by mistake. Please delete it or add skins to {skinDataFile.Filename}.", "Cannot save PCK file", MessageBoxButtons.OK, MessageBoxIcon.Error);
return false;
}
}
catch {
MessageBox.Show(this, $"{skinDataFile.Filename} is an invalid SkinData file and can crash the game. Please remove or fix it.", "Cannot save PCK file", MessageBoxButtons.OK, MessageBoxIcon.Error);
return false;
}
}
// Add an info file back to the PCK
if (_packID != -1)
{
PckAsset infoFile = new PckAsset("0", PckAssetType.InfoFile);
infoFile.AddParameter("PACKID", _packID.ToString());
EditorValue.File.InsertAsset(0, infoFile);
}
return true;
}
protected override void PostSave()
{
_timesSaved++;
_wasModified = false;
PckAsset infoFile;
EditorValue.File.TryGetAsset("0", PckAssetType.InfoFile, out infoFile);
if (infoFile != null)
EditorValue.File.RemoveAsset(infoFile);
MessageBox.Show($"{TitleName} saved.", "PCK successfully Saved");
Debug.WriteLine($"_timesSaved: {_timesSaved}");
}
public override void Close()
{
if (_wasModified && MessageBox.Show("Save PCK?", "Modified PCK", MessageBoxButtons.YesNo, MessageBoxIcon.Warning) == DialogResult.Yes)
Save();
}
private void OnModify(bool state)
{
pckFileLabel.Text = state && !pckFileLabel.Text.StartsWith("*") ? "*" + pckFileLabel.Text : pckFileLabel.Text.Substring(1);
}
public override void UpdateView()
{
BuildMainTreeView(true);
}
private void HandleTextureFile(PckAsset asset)
{
_ = asset.IsMipmappedFile() && EditorValue.File.TryGetAsset(asset.GetNormalPath(), PckAssetType.TextureFile, out asset);
if (asset.Size <= 0)
{
Trace.TraceInformation($"[{nameof(PckEditor)}:{nameof(HandleTextureFile)}] '{asset.Filename}' size is 0.");
return;
}
ResourceLocation resourceLocation = ResourceLocation.GetFromPath(asset.Filename);
Debug.WriteLine("Handling Resource file: " + resourceLocation?.ToString());
switch (resourceLocation.Category)
{
case ResourceCategory.Unknown:
Debug.WriteLine($"Unknown Resource Category.");
break;
case ResourceCategory.MobEntityTextures:
case ResourceCategory.ItemEntityTextures:
{
string texturePath = asset.Filename.Substring(0, asset.Filename.Length - Path.GetExtension(asset.Filename).Length);
string[] modelNames = GameModelImporter.ModelMetaData.Where(kv => kv.Value.TextureLocations.Contains(texturePath)).Select(kv => kv.Key).ToArray();
if (modelNames.Length == 0)
{
MessageBox.Show("No Model info found");
return;
}
string modelName = modelNames[0];
if (modelNames.Length > 1)
{
using ItemSelectionPopUp itemSelectionPopUp = new ItemSelectionPopUp(modelNames.ToArray());
itemSelectionPopUp.ButtonText = "View";
itemSelectionPopUp.LabelText = "Models:";
if (itemSelectionPopUp.ShowDialog() != DialogResult.OK || !modelNames.IndexInRange(itemSelectionPopUp.SelectedIndex))
{
return;
}
modelName = modelNames[itemSelectionPopUp.SelectedIndex];
}
Image texture = asset.GetTexture();
string textureName = Path.GetFileName(texturePath);
NamedData<Image> modelTexture = new NamedData<Image>(textureName, texture);
bool hasCustomModel = false;
bool hasDefaultModel = TryGetDefaultEntityModel(modelName, out Model model);
if (EditorValue.File.TryGetAsset("models.bin", PckAssetType.ModelsFile, out PckAsset modelsAsset))
{
ModelContainer models = modelsAsset.GetData(new ModelFileReader());
hasCustomModel = models.ContainsModel(modelName);
if (hasCustomModel)
{
Debug.WriteLine($"Custom model for '{modelName}' found.");
model = models.GetModelByName(modelName);
}
}
if (!hasDefaultModel && !hasCustomModel)
{
MessageBox.Show(this, $"No Model found for: {modelName}");
return;
}
ShowSimpleModelRender(model, modelTexture);
}
break;
case ResourceCategory.ItemAnimation:
case ResourceCategory.BlockAnimation:
Animation animation = asset.GetDeserializedData(AnimationDeserializer.DefaultDeserializer);
string internalName = Path.GetFileNameWithoutExtension(asset.Filename);
IList<JsonTileInfo> textureInfos = resourceLocation.Category == ResourceCategory.ItemAnimation ? Tiles.ItemTileInfos : Tiles.BlockTileInfos;
string displayname = textureInfos.FirstOrDefault(p => p.InternalName == internalName)?.DisplayName ?? internalName;
string[] specialTileNames = { "clock", "compass" };
ISaveContext<Animation> saveContext = new DelegatedSaveContext<Animation>(Settings.Default.AutoSaveChanges, (animation) =>
{
asset.SetSerializedData(animation, AnimationSerializer.DefaultSerializer);
});
using (AnimationEditor animationEditor = new AnimationEditor(animation, saveContext, displayname, !internalName.ToLower().EqualsAny(specialTileNames)))
{
if (animationEditor.ShowDialog(this) == DialogResult.OK)
{
_wasModified = true;
BuildMainTreeView();
}
}
break;
case ResourceCategory.ParticleAtlas:
case ResourceCategory.MoonPhaseAtlas:
case ResourceCategory.ItemAtlas:
case ResourceCategory.BlockAtlas:
case ResourceCategory.BannerAtlas:
case ResourceCategory.PaintingAtlas:
case ResourceCategory.ExplosionAtlas:
case ResourceCategory.ExperienceOrbAtlas:
case ResourceCategory.MapIconAtlas:
case ResourceCategory.AdditionalMapIconsAtlas:
Atlas atlas = asset.GetDeserializedData(new AtlasDeserializer(resourceLocation));
ColorContainer colorContainer = default;
if (EditorValue.File.TryGetAsset("colours.col", PckAssetType.ColourTableFile, out PckAsset colAsset))
colorContainer = colAsset.GetData(new COLFileReader());
ITryGet<string, Animation> tryGetAnimation = TryGet<string, Animation>.FromDelegate((string key, out Animation animation) =>
{
bool found = EditorValue.File.TryGetAsset(key + ".png", PckAssetType.TextureFile, out PckAsset foundAsset) ||
EditorValue.File.TryGetAsset(key + ".tga", PckAssetType.TextureFile, out foundAsset);
if (found)
{
animation = foundAsset.GetDeserializedData(AnimationDeserializer.DefaultDeserializer);
return true;
}
animation = default;
return false;
});
ITryGet<string, ISaveContext<Animation>> tryGetAnimationSaveContext = TryGet<string, ISaveContext<Animation>>
.FromDelegate((string key, out ISaveContext<Animation> animationSaveContext) =>
{
bool found = EditorValue.File.TryGetAsset(key + ".png", PckAssetType.TextureFile, out PckAsset foundAsset) ||
EditorValue.File.TryGetAsset(key + ".tga", PckAssetType.TextureFile, out foundAsset);
if (found)
{
animationSaveContext = new DelegatedSaveContext<Animation>(Settings.Default.AutoSaveChanges, (animation) =>
foundAsset.SetSerializedData(animation, AnimationSerializer.DefaultSerializer));
return true;
}
// you could validate the key(animationAssetPath) for validity. -miku
animationSaveContext = new DelegatedSaveContext<Animation>(Settings.Default.AutoSaveChanges, (animation) =>
{
if (animation.FrameCount == 0)
{
Debug.WriteLine("New animation has 0 frames. Aborting saving.");
return;
}
PckAsset newAnimationAsset = EditorValue.File.CreateNewAsset(key + ".png", PckAssetType.TextureFile);
newAnimationAsset.SetSerializedData(animation, AnimationSerializer.DefaultSerializer);
BuildMainTreeView();
});
return true;
});
ISaveContext<Atlas> textureAtlasSaveContext = new DelegatedSaveContext<Atlas>(Settings.Default.AutoSaveChanges, atlas => asset.SetTexture(atlas));
var viewer = new TextureAtlasEditor(atlas, textureAtlasSaveContext, resourceLocation, colorContainer, tryGetAnimation, tryGetAnimationSaveContext);
if (viewer.ShowDialog(this) == DialogResult.OK)
{
_wasModified = true;
BuildMainTreeView();
}
break;
default:
Debug.WriteLine($"Unhandled Resource Category: {resourceLocation.Category}");
break;
}
}
private void HandleGameRuleFile(PckAsset asset)
{
const string cDEFLATE = "PS3";
const string cXMEM = "Xbox 360";
const string cZLIB = "Other Platforms";
ItemSelectionPopUp dialog = new ItemSelectionPopUp(cZLIB, cDEFLATE, cXMEM);
dialog.LabelText = "Type";
dialog.ButtonText = "Ok";
if (dialog.ShowDialog() != DialogResult.OK)
return;
GameRuleFile.CompressionType compressiontype = dialog.SelectedItem switch
{
cDEFLATE => GameRuleFile.CompressionType.Deflate,
cXMEM => GameRuleFile.CompressionType.XMem,
cZLIB => GameRuleFile.CompressionType.Zlib,
_ => GameRuleFile.CompressionType.Unknown
};
GameRuleFile grf = asset.GetData(new GameRuleFileReader(compressiontype));
ISaveContext<GameRuleFile> saveContext = new DelegatedSaveContext<GameRuleFile>(Settings.Default.AutoSaveChanges, (gameRuleFile) =>
{
asset.SetData(new GameRuleFileWriter(gameRuleFile));
});
using GameRuleFileEditor grfEditor = new GameRuleFileEditor(grf, saveContext);
if (grfEditor.ShowDialog(this) == DialogResult.OK)
{
_wasModified = true;
UpdateRichPresence();
}
}
private void HandleAudioFile(PckAsset asset)
{
try
{
OMI.ByteOrder endianness = BigEndianCheckBox.Checked ? OMI.ByteOrder.BigEndian : OMI.ByteOrder.LittleEndian;
ISaveContext<PckAudioFile> saveContext = new DelegatedSaveContext<PckAudioFile>(Settings.Default.AutoSaveChanges, (audioFile) =>
{
asset.SetData(new PckAudioFileWriter(audioFile, endianness));
});
PckAudioFile audioFile = asset.GetData(new PckAudioFileReader(endianness));
using AudioEditor audioEditor = new AudioEditor(audioFile, saveContext);
_wasModified = audioEditor.ShowDialog(this) == DialogResult.OK;
}
catch (OverflowException)
{
MessageBox.Show(this, $"Failed to open {asset.Filename}\n" +
"Try converting the file by using the \"Misc. Functions/Set PCK Endianness\" tool and try again.",
"Error", MessageBoxButtons.OK, MessageBoxIcon.Warning);
}
catch (Exception ex)
{
MessageBox.Show($"Failed to open {asset.Filename}\n" + ex.Message,
"Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
private void HandleLocalisationFile(PckAsset asset)
{
LOCFile locFile = asset.GetData(new LOCFileReader());
ISaveContext<LOCFile> saveContext = new DelegatedSaveContext<LOCFile>(Settings.Default.AutoSaveChanges, (locFile) =>
{
asset.SetData(new LOCFileWriter(locFile, 2));
});
using LOCEditor locedit = new LOCEditor(locFile, saveContext);
_wasModified = locedit.ShowDialog(this) == DialogResult.OK;
UpdateRichPresence();
}
private void HandleColourFile(PckAsset asset)
{
ColorContainer colorContainer = asset.GetData(new COLFileReader());
ISaveContext<ColorContainer> saveContext = new DelegatedSaveContext<ColorContainer>(Settings.Default.AutoSaveChanges, (colorContainer) =>
{
asset.SetData(new COLFileWriter(colorContainer));
});
using COLEditor diag = new COLEditor(colorContainer, saveContext);
_wasModified = diag.ShowDialog(this) == DialogResult.OK;
}
private void HandleSkinFile(PckAsset asset)
{
Skin skin = asset.GetSkin();
if (asset.HasParameter("CAPEPATH"))
{
string capeAssetPath = asset.GetParameter("CAPEPATH");
if (EditorValue.File.TryGetAsset(capeAssetPath, PckAssetType.CapeFile, out PckAsset capeAsset))
{
skin.CapeTexture = capeAsset.GetTexture();
}
}
ISaveContext<Skin> saveContext = new DelegatedSaveContext<Skin>(Settings.Default.AutoSaveChanges, (customSkin) =>
{
if (!TryGetLocFile(out LOCFile locFile))
Debug.WriteLine("Failed to aquire loc file.");
asset.SetSkin(customSkin, locFile);
});
if (skin.Model.AdditionalBoxes.FindAll(box => box.Scale != 0).Count > 0 && EditorValue.File.xmlVersion != 3 &&
MessageBox.Show(this, "This skin has custom scaled boxes but the PCK file's current BOX Version does not support it. Would you like to set the PCK's BOX version to 3? This feature only works in TU69 and above.", "BOX Version mismatch.", MessageBoxButtons.YesNo) == DialogResult.Yes)
{
// set the xmlVersion of the parent pck to 3
(ParentForm as MainForm).setXMLVersion3MenuItem.PerformClick();
}
using CustomSkinEditor skinEditor = new CustomSkinEditor(skin, saveContext, EditorValue.File.xmlVersion);
if (skinEditor.ShowDialog() == DialogResult.OK)
{
parameterValueTextBox.Text = ParameterTypeTextBox.Text = string.Empty;
_wasModified = true;
ReloadParameterTreeView();
SetFileType(PckAssetType.SkinFile); // just a dirty way to get the right icon in case the anim was changed
}
}
private void HandleModelsFile(PckAsset asset)
{
ModelContainer modelContainer = asset.GetData(new ModelFileReader());
if (modelContainer.ModelCount == 0)
{
MessageBox.Show("No models found.", "Empty Model file", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
TryGetDelegate<string, Image> tryGetTexture = (string path, out Image img) =>
{
bool found = EditorValue.File.TryGetAsset(path + ".png", PckAssetType.TextureFile, out PckAsset asset) ||
EditorValue.File.TryGetAsset(path + ".tga", PckAssetType.TextureFile, out asset);
img = found ? asset.GetTexture() : default;
return found;
};
TrySetDelegate<string, Image> trySetTexture = (string path, Image img) =>
{
bool found = EditorValue.File.TryGetAsset(path + ".png", PckAssetType.TextureFile, out PckAsset foundAsset) ||
EditorValue.File.TryGetAsset(path + ".tga", PckAssetType.TextureFile, out foundAsset);
PckAsset asset = foundAsset ?? EditorValue.File.CreateNewAsset(path + ".png", PckAssetType.TextureFile);
asset.SetTexture(img);
return true;
};
bool hasMaterialAsset = EditorValue.File.TryGetAsset("entityMaterials.bin", PckAssetType.MaterialFile, out PckAsset entityMaterialAsset);
IReadOnlyDictionary<string, MaterialContainer.Material> entityMaterials =
hasMaterialAsset
? entityMaterialAsset?.GetData(new MaterialFileReader()).ToDictionary(mat => mat.Name)
: new Dictionary<string, MaterialContainer.Material>();
ISaveContext<ModelContainer> saveContext = new DelegatedSaveContext<ModelContainer>(Settings.Default.AutoSaveChanges, (modelContainer) =>
{
asset.SetData(new ModelFileWriter(modelContainer, modelContainer.Version));
});
var editor = new ModelEditor(modelContainer, saveContext, TryGetSet<string, Image>.FromDelegates(tryGetTexture, trySetTexture), TryGet<string, MaterialContainer.Material>.FromDelegate(entityMaterials.TryGetValue));
if (editor.ShowDialog() == DialogResult.OK)
{
BuildMainTreeView();
_wasModified = true;
return;
}
}
private void HandleBehavioursFile(PckAsset asset)
{
BehaviourFile behaviourFile = asset.GetData(new BehavioursReader());
ISaveContext<BehaviourFile> saveContext = new DelegatedSaveContext<BehaviourFile>(Settings.Default.AutoSaveChanges, (behaviourFile) =>
{
asset.SetData(new BehavioursWriter(behaviourFile));
});
using BehaviourEditor edit = new BehaviourEditor(behaviourFile, saveContext);
_wasModified = edit.ShowDialog(this) == DialogResult.OK;
}
private void HandleMaterialFile(PckAsset asset)
{
MaterialContainer materials = asset.GetData(new MaterialFileReader());
ISaveContext<MaterialContainer> saveContext = new DelegatedSaveContext<MaterialContainer>(Settings.Default.AutoSaveChanges, (materials) =>
{
asset.SetData(new MaterialFileWriter(materials));
});
using MaterialsEditor edit = new MaterialsEditor(materials, saveContext);
_wasModified = edit.ShowDialog(this) == DialogResult.OK;
}
private void CheckForPasswordAndRemove()
{
if (EditorValue.File.TryGetAsset("0", PckAssetType.InfoFile, out PckAsset asset))
{
asset.RemoveParameters("LOCK");
}
}
/// <summary>
/// wrapper that allows the use of <paramref name="name"/> in <code>TreeNode.Nodes.Find(<paramref name="name"/>, ...)</code> and <code>TreeNode.Nodes.ContainsKey(<paramref name="name"/>)</code>
/// </summary>
/// <param name="name"></param>
/// <param name="tag"></param>
/// <returns>new Created TreeNode</returns>
private static TreeNode CreateNode(string name, object tag = null)
{
TreeNode node = new TreeNode(name);
node.Name = name;
node.Tag = tag;
return node;
}
private TreeNode BuildNodeTreeBySeperator(TreeNodeCollection root, string path, char seperator)
{
_ = root ?? throw new ArgumentNullException(nameof(root));
if (!path.Contains(seperator))
{
TreeNode finalNode = CreateNode(path);
root.Add(finalNode);
return finalNode;
}
string nodeText = path.Substring(0, path.IndexOf(seperator));
string subPath = path.Substring(path.IndexOf(seperator) + 1);
if (string.IsNullOrWhiteSpace(nodeText))
{
return BuildNodeTreeBySeperator(root, subPath, seperator);
}
bool alreadyExists = root.ContainsKey(nodeText);
TreeNode subNode = alreadyExists ? root[nodeText] : CreateNode(nodeText);
if (!alreadyExists)
root.Add(subNode);
return BuildNodeTreeBySeperator(subNode.Nodes, subPath, seperator);
}
private void BuildPckTreeView(TreeNodeCollection root, PckFile pckFile, bool resetNodes = false)
{
foreach (PckAsset asset in pckFile.GetAssets())
{
TreeNode node = BuildNodeTreeBySeperator(root, asset.Filename, '/');
node.Tag = asset;
node.ImageKey = node.SelectedImageKey = GetNodeIconKey(asset, resetNodes);
}
}
private void BuildMainTreeView(bool resetNodes = false)
{
// In case the Rename function was just used and the selected node name no longer matches the file name
string selectedNodeText = treeViewMain.SelectedNode is TreeNode node ? node.FullPath : string.Empty;
previewPictureBox.Image = Resources.NoImageFound;
treeParameters.Nodes.Clear();
treeViewMain.Nodes.Clear();
BuildPckTreeView(treeViewMain.Nodes, EditorValue.File, resetNodes);
treeViewMain.Sort();
TreeNode[] selectedNodes = treeViewMain.FindPath(selectedNodeText);
if (selectedNodes.Length > 0)
{
treeViewMain.SelectedNode = selectedNodes[0];
}
}
private string GetDefaultSkinNodeIconKey(SkinANIM anim)
{
if (anim.GetFlag(SkinAnimFlag.SLIM_MODEL))
return "slimSkinFileIcon";
else if (anim.GetFlag(SkinAnimFlag.MODERN_WIDE_MODEL))
return "modernSkinFileIcon"; // slim gets priority just like in game
return "classicSkinFileIcon"; // classic skin model icon
}
private Image DrawCapeIcon(Image capeTexture)
{
Bitmap customIcon = new Bitmap(Resources.CUSTOM_SKIN_ICON.Width, Resources.CUSTOM_SKIN_ICON.Height);
float textureScaleX = capeTexture.Width / 64f; // minecraft capes always have a width of 64
float textureScaleY = capeTexture.Height / 32f; // minecraft capes always have a height of 32
float width = 10;
float height = 16;
float depth = 1;
// this math is basically to ensure the face is stretched if the texture is improper
Rectangle faceRect = new Rectangle(
(int)(depth * textureScaleX),
(int)(depth * textureScaleY),
(int)(width * textureScaleX),
(int)(height * textureScaleY)
);
Image capeFace = capeTexture.GetArea(faceRect);
using (Graphics gfx = Graphics.FromImage(customIcon))
{
gfx.InterpolationMode = InterpolationMode.NearestNeighbor;
gfx.PixelOffsetMode = PixelOffsetMode.HighQuality;
gfx.SmoothingMode = SmoothingMode.None;
gfx.DrawImage(capeFace, 56, 12, 145, 232); // position for the cape on the icon
}
return customIcon;
}
private string GetCapeNodeIconKey(PckAsset cape, bool reset)
{
if (!Settings.Default.UseCustomCapeIcons)
return "capeFileIcon"; // default cape icon
Image capeTexture = null;
try
{
capeTexture = cape.GetTexture();
}
catch
{
return "capeFileIcon"; // fallback if invalid skin file
}
string id = Path.GetFileNameWithoutExtension(cape.Filename);
if (imageList.Images.ContainsKey(id) && !reset)
{
return id;
}
else
{
Bitmap customIcon = new Bitmap(Resources.CUSTOM_SKIN_ICON.Width, Resources.CUSTOM_SKIN_ICON.Height);
using (Graphics gfx = Graphics.FromImage(customIcon))
{
gfx.InterpolationMode = InterpolationMode.NearestNeighbor;
gfx.PixelOffsetMode = PixelOffsetMode.HighQuality;
gfx.SmoothingMode = SmoothingMode.None;
gfx.Fill(new Rectangle(0, 0, customIcon.Width, customIcon.Height), Color.FromArgb(0xFF, 0x5D, 0x9C, 0xEC));
gfx.DrawImage(DrawCapeIcon(capeTexture), 0, 0); // the cape icon is already the same dimensions as the custom icon
gfx.DrawImage(Resources.CUSTOM_SKIN_ICON, 0, 0);
}
// replace icon by removing and importing at index
int index = imageList.Images.IndexOfKey(id);
if (index != -1)
{
imageList.Images.RemoveByKey(id);
imageList.Images.Insert(index, id, customIcon);
}
else
imageList.Images.Add(id, customIcon);
return id;
}
}
private string GetSkinNodeIconKey(Skin skin, bool reset)
{
if (skin == null)
return GetDefaultSkinNodeIconKey(SkinANIM.Empty);
else if (!Settings.Default.UseCustomSkinIcons)
return GetDefaultSkinNodeIconKey(skin.Anim);
string id = "dlcskin" + skin.Identifier.ToString();
if (imageList.Images.ContainsKey(id) && !reset)
{
return id;
}
else
{
int skinIconWidth = Resources.SKINS_ICON.Width;
int skinIconHeight = Resources.SKINS_ICON.Height;
Bitmap customIcon = new Bitmap(skinIconWidth, skinIconHeight);
using (Graphics gfx = Graphics.FromImage(customIcon))
{
gfx.InterpolationMode = InterpolationMode.NearestNeighbor;
gfx.PixelOffsetMode = PixelOffsetMode.HighQuality;
gfx.SmoothingMode = SmoothingMode.None;
gfx.Fill(new Rectangle(0, 0, skinIconWidth, skinIconHeight), Color.FromArgb(0xFF, 0x5D, 0x9C, 0xEC));
// get the bust crop for the image
// this is handled externally to use the skin data to ensure skins are positioned in context of the default player head
Image croppedPaperDoll = skin.DrawPaperDoll(xmlVersion: EditorValue.File.xmlVersion, bustCrop: true);
gfx.DrawImage(croppedPaperDoll, 0, 0); // the crop bust is already the same dimensions as the custom icon
gfx.DrawImage(Resources.CUSTOM_SKIN_ICON, 0, 0); // draw border on top
}
// replace icon by removing and importing at index
int index = imageList.Images.IndexOfKey(id);
if (index != -1)
{
imageList.Images.RemoveByKey(id);
imageList.Images.Insert(index, id, customIcon);
}
else
imageList.Images.Add(id, customIcon);
}
return id;
}
private Image GetNodeIcon(PckAsset asset = null)
{
if (asset == null)
return Resources.ZZFolder;
return imageList.Images[GetNodeIconKey(asset)];
}
private string GetNodeIconKey(PckAsset asset, bool reset = false)
{
Skin skin = null;
try
{
if (asset.Type == PckAssetType.SkinFile)
skin = asset.GetSkin();
}
catch
{
skin = null;
}
return asset.Type switch
{
PckAssetType.SkinFile => GetSkinNodeIconKey(skin, reset),
PckAssetType.CapeFile => GetCapeNodeIconKey(asset, reset),
PckAssetType.TextureFile => "textureFileIcon",
PckAssetType.InfoFile => "infoFileIcon",
PckAssetType.TexturePackInfoFile => "pckFileIcon",
PckAssetType.LocalisationFile => "localisationFileIcon",
PckAssetType.GameRulesFile => "gameRulesFileIcon",
PckAssetType.AudioFile => "audioFileIcon",
PckAssetType.ColourTableFile => "colourTableFileIcon",
PckAssetType.GameRulesHeader => "gameRulesHeaderIcon",
PckAssetType.SkinDataFile => "skinDataFileIcon",
PckAssetType.ModelsFile => "modelsFileIcon",
PckAssetType.BehavioursFile => "behavioursFileIcon",
PckAssetType.MaterialFile => "materialFileIcon",
// unknown file format
_ => "unknownFileIcon",
};
}
private List<TreeNode> GetAllChildNodes(TreeNodeCollection root)
{
List<TreeNode> childNodes = new List<TreeNode>();
foreach (TreeNode node in root)
{
childNodes.Add(node);
if (node.Nodes.Count > 0)
{
childNodes.AddRange(GetAllChildNodes(node.Nodes));
}
}
return childNodes;
}
private bool TryGetLocFile(out LOCFile locFile)
{
if (!EditorValue.File.TryGetAsset("localisation.loc", PckAssetType.LocalisationFile, out PckAsset locAsset) &&
!EditorValue.File.TryGetAsset("languages.loc", PckAssetType.LocalisationFile, out locAsset))
{
locFile = null;
return false;
}
try
{
locFile = locAsset.GetData(new LOCFileReader());
return true;
}
catch (Exception ex)
{
Debug.WriteLine(ex.Message);
}
locFile = null;
return false;
}
private bool TrySetLocFile(in LOCFile locFile)
{
if (!EditorValue.File.TryGetAsset("localisation.loc", PckAssetType.LocalisationFile, out PckAsset locAsset) &&
!EditorValue.File.TryGetAsset("languages.loc", PckAssetType.LocalisationFile, out locAsset))
{
return false;
}
try
{
locAsset.SetData(new LOCFileWriter(locFile, 2));
return true;
}
catch (Exception ex)
{
Debug.WriteLine(ex.Message);
}
return false;
}
private void ReloadParameterTreeView()
{
treeParameters.Nodes.Clear();
if (treeViewMain.SelectedNode is TreeNode node &&
node.Tag is PckAsset asset)
{
foreach (KeyValuePair<string, string> parameter in asset.GetParameters())
{
treeParameters.Nodes.Add(CreateNode(parameter.Key, parameter));
}
}
}
private void UpdateRichPresence()
{
if (EditorValue is not null &&
TryGetLocFile(out LOCFile locfile) &&
locfile.HasLocEntry("IDS_DISPLAY_NAME") &&
locfile.Languages.Contains("en-EN"))
{
RPC.SetPresence("Editing a Pack:", $" > {locfile.GetLocEntry("IDS_DISPLAY_NAME", "en-EN")}");
return;
}
// default
RPC.SetPresence("An Open Source .PCK File Editor");
}
private static PckAsset CreateNewAudioAsset(bool isLittle, PckAudioFile audioFile)
{
PckAsset newAsset = new PckAsset("audio.pck", PckAssetType.AudioFile);
newAsset.SetData(new PckAudioFileWriter(audioFile, isLittle ? OMI.ByteOrder.LittleEndian : OMI.ByteOrder.BigEndian));
return newAsset;
}
private static PckAudioFile CreateNewAudioFile()
{
PckAudioFile audioFile = new PckAudioFile();
audioFile.AddCategory(PckAudioFile.AudioCategory.EAudioType.Overworld);
audioFile.AddCategory(PckAudioFile.AudioCategory.EAudioType.Nether);
audioFile.AddCategory(PckAudioFile.AudioCategory.EAudioType.End);
return audioFile;
}
private void importFileToolStripMenuItem_Click(object sender, EventArgs e)
{
using var ofd = new OpenFileDialog();
ofd.Filter = "All files (*.*)|*.*";
ofd.Multiselect = false;
if (ofd.ShowDialog(this) == DialogResult.OK)
{
ImportFiles(Path.GetDirectoryName(ofd.FileName), new[] { ofd.FileName }, String.Empty);
}
return;
}
private void folderToolStripMenuItem_Click(object sender, EventArgs e)
{
TextPrompt folderNamePrompt = new TextPrompt();
if (treeViewMain.SelectedNode is not null)
folderNamePrompt.contextLabel.Text =
$"New folder at the location of \"{(
!treeViewMain.SelectedNode.IsTagOfType<PckAsset>()
? "/" + treeViewMain.SelectedNode.FullPath
: treeViewMain.SelectedNode.Parent == null ? "/" : "/" + treeViewMain.SelectedNode.Parent.FullPath)}\"";
folderNamePrompt.OKButtonText = "Add";
if (folderNamePrompt.ShowDialog(this) == DialogResult.OK)
{
TreeNode folerNode = CreateNode(folderNamePrompt.NewText);
folerNode.ImageIndex = 0;
folerNode.SelectedImageIndex = 0;
TreeNodeCollection nodeCollection = treeViewMain.Nodes;
if (treeViewMain.SelectedNode is TreeNode node)
{
if (node.Tag is PckAsset asset &&
asset.Type != PckAssetType.TexturePackInfoFile &&
asset.Type != PckAssetType.SkinDataFile)
{
if (node.Parent is TreeNode parentNode)
{
nodeCollection = parentNode.Nodes;
}
}
else
nodeCollection = node.Nodes;
}
nodeCollection.Add(folerNode);
}
}
private void SetFileType(PckAssetType type)
{
if (treeViewMain.SelectedNode.TryGetTagData(out PckAsset asset))
{
Debug.WriteLine($"Setting {asset.Type} to {type}");
asset.Type = type;
TreeNode node = treeViewMain.SelectedNode;
node.ImageKey = node.SelectedImageKey = GetNodeIconKey(asset, true);
}
}
private void treeViewMain_AfterSelect(object sender, TreeViewEventArgs e)
{
ReloadParameterTreeView();
ParameterTypeTextBox.Text = parameterValueTextBox.Text = labelImageSize.Text = string.Empty;
buttonEdit.Visible = false;
previewPictureBox.Image = Resources.NoImageFound;
viewFileInfoToolStripMenuItem.Visible = false;
if (!e.Node.TryGetTagData(out PckAsset asset))
{
return;
}
viewFileInfoToolStripMenuItem.Visible = true;
if (asset.Type == PckAssetType.SkinFile)
{
buttonEdit.Text = "EDIT SKIN";
buttonEdit.Visible = true;
}
switch (asset.Type)
{
case PckAssetType.SkinFile:
case PckAssetType.CapeFile:
case PckAssetType.TextureFile:
{
try
{
Image img = asset.GetTexture();
previewPictureBox.Image = img;
labelImageSize.Text = $"{previewPictureBox.Image.Size.Width}x{previewPictureBox.Image.Size.Height}";
}
catch(Exception ex)
{
previewPictureBox.Image = Resources.NoImageFound;
MessageBox.Show(this, "Not a valid .PNG or .TGA image or invalid image data found");
}
if (asset.Type != PckAssetType.TextureFile)
break;
ResourceLocation resourceLocation = ResourceLocation.GetFromPath(asset.Filename);
if (resourceLocation is null || resourceLocation.Category == ResourceCategory.Unknown)
break;
if (resourceLocation.Category == ResourceCategory.ItemAnimation ||
resourceLocation.Category == ResourceCategory.BlockAnimation &&
!asset.IsMipmappedFile())
{
buttonEdit.Text = "EDIT TILE ANIMATION";
buttonEdit.Visible = true;
break;
}
if (resourceLocation.Category == ResourceCategory.MobEntityTextures || resourceLocation.Category == ResourceCategory.ItemEntityTextures)
{
buttonEdit.Text = "EDIT ENTITY MODEL";
buttonEdit.Visible = true;
break;
}
buttonEdit.Text = "EDIT TEXTURE ATLAS";
buttonEdit.Visible = true;
}
break;
case PckAssetType.LocalisationFile:
buttonEdit.Text = "EDIT LOC";
buttonEdit.Visible = true;
break;
case PckAssetType.AudioFile:
buttonEdit.Text = "EDIT MUSIC CUES";
buttonEdit.Visible = true;
break;
case PckAssetType.ColourTableFile when asset.Filename == "colours.col":
buttonEdit.Text = "EDIT COLORS";
buttonEdit.Visible = true;
break;
case PckAssetType.BehavioursFile when asset.Filename == "behaviours.bin":
buttonEdit.Text = "EDIT BEHAVIOURS";
buttonEdit.Visible = true;
break;
default:
buttonEdit.Visible = false;
break;
}
}
private void treeViewMain_DoubleClick(object sender, EventArgs e)
{
if (treeViewMain.SelectedNode is TreeNode t && t.Tag is PckAsset asset)
{
if (asset.Size <= 0)
{
Trace.WriteLine($"'{asset.Filename}' has no data attached.", category: nameof(treeViewMain_DoubleClick));
return;
}
_pckAssetTypeHandler[asset.Type]?.Invoke(asset);
}
}
// Most of the code below is modified code from this link:
// https://docs.microsoft.com/en-us/dotnet/api/system.windows.forms.treeview.itemdrag?view=windowsdesktop-6.0
// - MayNL
private void treeViewMain_ItemDrag(object sender, ItemDragEventArgs e)
{
if (e.Button != MouseButtons.Left || e.Item is not TreeNode node)
return;
if ((node.TryGetTagData(out PckAsset asset) && EditorValue.File.Contains(asset.Filename, asset.Type)) || node.Parent is TreeNode)
{
// TODO: add (mouse) scrolling while dragging item(s)
treeViewMain.DoDragDrop(node, DragDropEffects.Scroll | DragDropEffects.Move);
}
}
private void treeViewMain_DragOver(object sender, DragEventArgs e)
{
Point dragLocation = new Point(e.X, e.Y);
TreeNode node = treeViewMain.GetNodeAt(treeViewMain.PointToClient(dragLocation));
treeViewMain.SelectedNode = node.IsTagOfType<PckAsset>() ? null : node;
}
private void treeViewMain_DragEnter(object sender, DragEventArgs e)
{
e.Effect = e.Data.GetDataPresent(DataFormats.FileDrop) ? DragDropEffects.Copy : e.AllowedEffect;
BringToFront();
Focus();
treeViewMain.Focus();
}
private void treeViewMain_DragDrop(object sender, DragEventArgs e)
{
// Retrieve the client coordinates of the drop location.
Point dragLocation = new Point(e.X, e.Y);
Point targetPoint = treeViewMain.PointToClient(dragLocation);
if (!treeViewMain.ClientRectangle.Contains(targetPoint))
return;
// Retrieve the node at the drop location.
TreeNode targetNode = treeViewMain.GetNodeAt(targetPoint);
if (e.Data.GetDataPresent(DataFormats.FileDrop) && e.Data.GetData(DataFormats.FileDrop) is string[] filesDropped)
{
IEnumerable<string> files = filesDropped.Where(File.Exists);
IEnumerable<string> directoryFiles = filesDropped
.Where(f => (File.GetAttributes(f) & FileAttributes.Directory) != 0)
.SelectMany(dir => Directory.GetFiles(dir, "*.*", SearchOption.AllDirectories));
string baseDirectory = Path.GetDirectoryName(filesDropped.First());
string finalDropPath = targetNode != null ? (targetNode.Tag != null ? Path.GetDirectoryName(targetNode?.FullPath) : targetNode?.FullPath) : "";
IEnumerable<string> importPaths = files.Concat(directoryFiles);
ImportFiles(baseDirectory, importPaths, string.IsNullOrWhiteSpace(finalDropPath) ? string.Empty : finalDropPath);
return;
}
string dataFormat = typeof(TreeNode).FullName;
if (targetNode is null)
return;
if (!e.Data.GetDataPresent(dataFormat))
return;
bool isTargetPckFile = targetNode.IsTagOfType<PckAsset>();
TreeNode draggedNode = e.Data.GetData(dataFormat) as TreeNode;
if (draggedNode == null)
{
Debug.WriteLine("Dragged node is null.");
return;
}
if (targetNode.Equals(draggedNode))
{
Debug.WriteLine("Dragged node was not moved.");
return;
}
if (targetNode.Equals(draggedNode.Parent))
{
Debug.WriteLine("target node is parent of dragged node... nothing done.");
return;
}
if (draggedNode.Equals(targetNode.Parent))
{
Debug.WriteLine("dragged node is parent of target node... nothing done.");
return;
}
if (targetNode.Parent == null && isTargetPckFile && draggedNode.Parent == null)
{
Debug.WriteLine("target node is file and is in the root... nothing done.");
return;
}
if ((targetNode.Parent?.Equals(draggedNode.Parent) ?? false) && isTargetPckFile)
{
Debug.WriteLine("target node and dragged node have the same parent... nothing done.");
return;
}
Debug.WriteLine($"Target drop location is {(isTargetPckFile ? "file" : "folder")}.");
// Retrieve the node that was dragged.
if (draggedNode.TryGetTagData(out PckAsset draggedAsset) &&
targetNode.FullPath != draggedAsset.Filename)
{
Debug.WriteLine(draggedAsset.Filename + " was droped onto " + targetNode.FullPath);
string newFilePath = Path.Combine(isTargetPckFile
? Path.GetDirectoryName(targetNode.FullPath)
: targetNode.FullPath, Path.GetFileName(draggedAsset.Filename));
Debug.WriteLine("New filepath: " + newFilePath);
draggedAsset.Filename = newFilePath;
_wasModified = true;
BuildMainTreeView();
return;
}
else
{
IEnumerable<PckAsset> pckFiles = GetAllChildNodes(draggedNode.Nodes).Where(t => t.IsTagOfType<PckAsset>()).Select(t => t.Tag as PckAsset);
string oldPath = draggedNode.FullPath;
string newPath = Path.Combine(isTargetPckFile ? Path.GetDirectoryName(targetNode.FullPath) : targetNode.FullPath, draggedNode.Text).Replace('\\', '/');
foreach (PckAsset pckFile in pckFiles)
{
pckFile.Filename = Path.Combine(newPath, pckFile.Filename.Substring(oldPath.Length + 1)).Replace('\\', '/');
}
_wasModified = true;
BuildMainTreeView();
}
}
private void ImportFiles(string baseDirectory, IEnumerable<string> files, string prefix)
{
int fileCount = files.Count();
bool singleFile = fileCount == 1;
int addedCount = 0;
int skippedFiles = 0;
int skipAttempts = 3;
int typeDuplication = 0;
PckAssetType lastSelectedAssetType = PckAssetType.SkinFile;
bool addSkin = false;
LOCFile locFile = null;
TryGetLocFile(out locFile);
foreach (var filepath in files)
{
if (filepath.EndsWith(".txt") && !singleFile)
{
string originalFile = Path.Combine(
Path.GetDirectoryName(filepath)!,
Path.GetFileNameWithoutExtension(filepath));
if (File.Exists(originalFile))
continue;
}
string assetPath = Path.Combine(prefix + filepath.Substring(baseDirectory.Length)).TrimStart('/', '\\');
PckAssetType assetType = lastSelectedAssetType;
bool askForAssetType = true;
string parameterFile = filepath + ".txt";
PckAsset importedAsset = new PckAsset(assetPath, 0);
if (File.Exists(parameterFile))
{
importedAsset.DeserializeParameters(File.ReadAllLines(parameterFile));
string typeParam = "ASSET_TYPE";
askForAssetType = String.IsNullOrWhiteSpace(importedAsset.GetParameter(typeParam));
if(!askForAssetType)
{
importedAsset.Type = (PckAssetType)int.Parse(importedAsset.GetParameter(typeParam));
importedAsset.RemoveParameter(typeParam);
}
}
if (askForAssetType || singleFile)
{
using AddFilePrompt addFile = new AddFilePrompt(assetPath);
if (singleFile && !askForAssetType)
addFile.Filetype = importedAsset.Type;
if (addFile.ShowDialog(this) != DialogResult.OK)
{
skippedFiles++;
skipAttempts--;
if (skipAttempts > 0)
continue;
int remainingFileCount = fileCount - addedCount - skippedFiles;
DialogResult abortFurtherImport = MessageBox.Show($"Do you wan't to abort further file imports?\n{remainingFileCount} file(s) left.", "Abort further import", MessageBoxButtons.YesNo, MessageBoxIcon.Question, MessageBoxDefaultButton.Button2);
if (abortFurtherImport == DialogResult.Yes)
{
skippedFiles += remainingFileCount;
break;
}
skipAttempts = 3;
continue;
}
assetType = addFile.Filetype;
assetPath = addFile.Filepath;
if (lastSelectedAssetType == assetType)
typeDuplication++;
lastSelectedAssetType = addFile.Filetype;
if (typeDuplication > 1)
{
DialogResult useSameTypeForRest = MessageBox.Show($"Do you want to import all typeless remaining files as {lastSelectedAssetType}?", "Import all as", MessageBoxButtons.YesNo, MessageBoxIcon.Question, MessageBoxDefaultButton.Button1);
if (useSameTypeForRest == DialogResult.Yes)
{
askForAssetType = false;
}
}
importedAsset.Filename = assetPath;
importedAsset.Type = assetType;
}
importedAsset.SetData(File.ReadAllBytes(filepath));
if (EditorValue.File.Contains(importedAsset.Filename, importedAsset.Type))
{
MessageBox.Show(this, $"'{assetPath}' of type {assetType} already exists.\nSkiping file.", "File already exists", MessageBoxButtons.OK, MessageBoxIcon.Warning, MessageBoxDefaultButton.Button1);
Debug.WriteLine($"'{assetPath}' of type {assetType} already exists.\nSkipping file.");
skippedFiles++;
continue;
}
EditorValue.File.AddAsset(importedAsset);
if(importedAsset.Type == PckAssetType.SkinFile && locFile != null)
{
const string displayNameLabel = "DISPLAYNAME";
const string displayNameIDLabel = "DISPLAYNAMEID";
const string themeNameLabel = "THEMENAME";
const string themeNameIDLabel = "THEMENAMEID";
string displayName;
string skin_key;
string themeName;
string themeNameID;
importedAsset.TryGetParameter(displayNameLabel, out displayName);
if (!importedAsset.TryGetParameter(displayNameIDLabel, out skin_key))
{
// if failed, try LOC_KEY
importedAsset.TryGetParameter("LOC_KEY", out skin_key);
}
importedAsset.TryGetParameter(themeNameLabel, out themeName);
importedAsset.TryGetParameter(themeNameIDLabel, out themeNameID);
if (!locFile.HasLocEntry(skin_key) && !String.IsNullOrEmpty(displayName))
locFile.AddLocKey(skin_key, displayName);
if (!locFile.HasLocEntry(themeNameID) && !String.IsNullOrEmpty(themeName))
locFile.AddLocKey(themeNameID, themeName);
addSkin = true;
}
addedCount++;
}
Trace.TraceInformation("[{0}] Imported {1} file(s).", nameof(ImportFiles), addedCount);
Trace.TraceInformation("[{0}] Skipped {1} file(s).", nameof(ImportFiles), skippedFiles);
if (addedCount > 0)
{
_wasModified = true;
BuildMainTreeView();
if(locFile != null && addSkin)
TrySetLocFile(locFile);
}
}
private IEnumerable<TreeNode> GetEndingNodes(TreeNodeCollection collection)
{
List<TreeNode> trailingNodes = new List<TreeNode>(collection.Count);
foreach (TreeNode node in collection)
{
if (node.Nodes.Count > 0)
{
trailingNodes.AddRange(GetEndingNodes(node.Nodes));
continue;
}
trailingNodes.Add(node);
}
return trailingNodes;
}
private void createSkinToolStripMenuItem_Click(object sender, EventArgs e)
{
using (AddSkinPrompt addNewSkinDialog = new AddSkinPrompt(EditorValue.File.xmlVersion))
if (addNewSkinDialog.ShowDialog() == DialogResult.OK)
{
TryGetLocFile(out LOCFile locFile);
PckAsset skinAsset = addNewSkinDialog.NewSkin.CreateFile(locFile);
EditorValue.File.AddAsset(skinAsset);
bool hasSkinsFolder = treeViewMain.Nodes.ContainsKey("Skins");
if (hasSkinsFolder)
skinAsset.Filename = skinAsset.Filename.Insert(0, "Skins/"); // Then Skins folder
EditorValue.File.AddAsset(skinAsset);
if (addNewSkinDialog.NewSkin.HasCape)
{
PckAsset capeFile = addNewSkinDialog.NewSkin.CreateCapeFile();
if (hasSkinsFolder)
capeFile.Filename = capeFile.Filename.Insert(0, "Skins/"); // Then Skins folder
EditorValue.File.AddAsset(capeFile);
}
TrySetLocFile(locFile);
_wasModified = true;
BuildMainTreeView();
}
}
private void createAnimatedTextureToolStripMenuItem_Click(object sender, EventArgs e)
{
using ChangeTile diag = new ChangeTile();
if (diag.ShowDialog(this) != DialogResult.OK)
return;
string animationFilepath = $"{ResourceLocation.GetPathFromCategory(diag.Category)}/{diag.SelectedTile.InternalName}.png";
if (EditorValue.File.Contains(animationFilepath, PckAssetType.TextureFile))
{
MessageBox.Show(this, $"{diag.SelectedTile.DisplayName} is already present.", "File already present");
return;
}
Animation newAnimation = default;
ISaveContext<Animation> saveContext = new DelegatedSaveContext<Animation>(Settings.Default.AutoSaveChanges, (animation) =>
{
newAnimation = animation;
});
using AnimationEditor animationEditor = new AnimationEditor(Animation.CreateEmpty(), saveContext, diag.SelectedTile.DisplayName, !diag.SelectedTile.InternalName.EqualsAny("clock", "compass"));
if (animationEditor.ShowDialog() == DialogResult.OK && newAnimation is not null)
{
_wasModified = true;
PckAsset asset = EditorValue.File.CreateNewAsset(animationFilepath, PckAssetType.TextureFile);
asset.SetSerializedData(newAnimation, AnimationSerializer.DefaultSerializer);
BuildMainTreeView();
ReloadParameterTreeView();
}
}
private void audiopckToolStripMenuItem_Click(object sender, EventArgs e)
{
if (EditorValue.File.Contains(PckAssetType.AudioFile))
{
// the chance of this happening is really really slim but just in case
MessageBox.Show(this, "There is already an audio file in this PCK!", "Can't create audio.pck");
return;
}
PckAudioFile newAudioFile = CreateNewAudioFile();
PckAsset newAudioAsset = CreateNewAudioAsset(BigEndianCheckBox.Checked, newAudioFile);
ISaveContext<PckAudioFile> saveContext = new DelegatedSaveContext<PckAudioFile>(Settings.Default.AutoSaveChanges, (audioFile) =>
{
newAudioAsset.SetData(new PckAudioFileWriter(audioFile, BigEndianCheckBox.Checked ? OMI.ByteOrder.BigEndian : OMI.ByteOrder.LittleEndian));
});
AudioEditor diag = new AudioEditor(newAudioFile, saveContext);
if (diag.ShowDialog(this) == DialogResult.OK)
{
EditorValue.File.AddAsset(newAudioAsset);
}
diag.Dispose();
BuildMainTreeView();
}
private void colourscolToolStripMenuItem_Click(object sender, EventArgs e)
{
if (EditorValue.File.TryGetAsset("colours.col", PckAssetType.ColourTableFile, out _))
{
MessageBox.Show(this, "A color table file already exists in this PCK and a new one cannot be created.", "Operation aborted");
return;
}
PckAsset newColorAsset = EditorValue.File.CreateNewAsset("colours.col", PckAssetType.ColourTableFile);
newColorAsset.SetData(Resources.tu69colours);
BuildMainTreeView();
}
private void CreateSkinsPCKToolStripMenuItem1_Click(object sender, EventArgs e)
{
if (EditorValue.File.TryGetAsset("Skins.pck", PckAssetType.SkinDataFile, out _))
{
MessageBox.Show(this, "A Skins.pck file already exists in this PCK and a new one cannot be created.", "Operation aborted");
return;
}
EditorValue.File.CreateNewAsset("Skins.pck", PckAssetType.SkinDataFile, new PckFileWriter(new PckFile(3, 3),
BigEndianCheckBox.Checked ? OMI.ByteOrder.BigEndian : OMI.ByteOrder.LittleEndian));
BuildMainTreeView();
}
private void behavioursbinToolStripMenuItem_Click(object sender, EventArgs e)
{
if (EditorValue.File.TryGetAsset("behaviours.bin", PckAssetType.BehavioursFile, out _))
{
MessageBox.Show(this, "A behaviours file already exists in this PCK and a new one cannot be created.", "Operation aborted");
return;
}
EditorValue.File.CreateNewAsset("behaviours.bin", PckAssetType.BehavioursFile, new BehavioursWriter(new BehaviourFile()));
BuildMainTreeView();
}
private void entityMaterialsbinToolStripMenuItem_Click(object sender, EventArgs e)
{
if (EditorValue.File.TryGetAsset("entityMaterials.bin", PckAssetType.MaterialFile, out _))
{
MessageBox.Show(this, "A behaviours file already exists in this PCK and a new one cannot be created.", "Operation aborted");
return;
}
var materialContainer = new MaterialContainer();
materialContainer.InitializeDefault();
EditorValue.File.CreateNewAsset("entityMaterials.bin", PckAssetType.MaterialFile, new MaterialFileWriter(materialContainer));
BuildMainTreeView();
}
private void importFolder(object sender, EventArgs e)
{
OpenFolderDialog contents = new OpenFolderDialog();
if (contents.ShowDialog(Handle) == true)
{
//checks to make sure selected path exist
if (!Directory.Exists(contents.ResultPath))
{
MessageBox.Show(this, $"Target directory \"{contents.ResultPath}\" was missing or moved.", "Import failed", MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}
string[] filesImported = Directory.GetFiles(contents.ResultPath);
IEnumerable<string> files = filesImported.Where(File.Exists);
IEnumerable<string> directoryFiles = filesImported
.Where(f => (File.GetAttributes(f) & FileAttributes.Directory) != 0)
.SelectMany(dir => Directory.GetFiles(dir, "*.*", SearchOption.AllDirectories));
TreeNode targetNode = treeViewMain.SelectedNode ?? null;
string finalDropPath = targetNode != null ? (targetNode.Tag != null ? Path.GetDirectoryName(targetNode?.FullPath) : targetNode?.FullPath) : "";
IEnumerable<string> importPaths = files.Concat(directoryFiles);
ImportFiles(contents.ResultPath, importPaths, string.IsNullOrWhiteSpace(finalDropPath) ? string.Empty : finalDropPath);
}
}
private void as3DSTextureFileToolStripMenuItem_Click(object sender, EventArgs e)
{
if (treeViewMain.SelectedNode.TryGetTagData(out PckAsset asset) &&
asset.Type == PckAssetType.SkinFile)
{
SaveFileDialog saveFileDialog = new SaveFileDialog();
saveFileDialog.Filter = "3DS Texture|*.3dst";
saveFileDialog.DefaultExt = ".3dst";
if (saveFileDialog.ShowDialog(this) == DialogResult.OK)
{
Image img = asset.GetTexture();
var writer = new _3DSTextureWriter(img);
writer.WriteToFile(saveFileDialog.FileName);
}
}
}
private void generateMipMapTextureToolStripMenuItem_Click(object sender, EventArgs e)
{
if (treeViewMain.SelectedNode.Tag is PckAsset asset && asset.Type == PckAssetType.TextureFile)
{
string textureDirectory = Path.GetDirectoryName(asset.Filename);
string textureName = Path.GetFileNameWithoutExtension(asset.Filename);
if (asset.IsMipmappedFile())
return;
string textureExtension = Path.GetExtension(asset.Filename);
using NumericPrompt numericPrompt = new NumericPrompt(0);
numericPrompt.Minimum = 2;
numericPrompt.Maximum = 5; // 5 is the presumed max MipMap level
numericPrompt.TextLabel.Text = "Level";
if (numericPrompt.ShowDialog(this) == DialogResult.OK)
{
int level = numericPrompt.SelectedValueAsInt;
string mippedPath = $"{textureDirectory}/{textureName}MipMapLevel{level}{textureExtension}";
Debug.WriteLine(mippedPath);
if (EditorValue.File.HasAsset(mippedPath, PckAssetType.TextureFile))
EditorValue.File.RemoveAsset(EditorValue.File.GetAsset(mippedPath, PckAssetType.TextureFile));
PckAsset mipMappedAsset = new PckAsset(mippedPath, PckAssetType.TextureFile);
Image originalTexture = asset.GetTexture();
int newWidth = Math.Max(originalTexture.Width / (int)Math.Pow(2, level - 1), 1);
int newHeight = Math.Max(originalTexture.Height / (int)Math.Pow(2, level - 1), 1);
Rectangle tileArea = new Rectangle(0, 0, newWidth, newHeight);
Image mippedTexture = new Bitmap(newWidth, newHeight);
using (Graphics gfx = Graphics.FromImage(mippedTexture))
{
gfx.SmoothingMode = SmoothingMode.AntiAlias;
gfx.InterpolationMode = InterpolationMode.NearestNeighbor;
gfx.PixelOffsetMode = PixelOffsetMode.HighQuality;
gfx.DrawImage(originalTexture, tileArea);
}
mipMappedAsset.SetTexture(mippedTexture);
EditorValue.File.InsertAsset(EditorValue.File.IndexOfAsset(asset) + 1, mipMappedAsset);
BuildMainTreeView();
_wasModified = true;
}
}
}
private void viewFileInfoToolStripMenuItem_Click(object sender, EventArgs e)
{
if (treeViewMain.SelectedNode.Tag is PckAsset asset)
{
MessageBox.Show(
$"Asset path: {asset.Filename}" +
$"\nAsset type: {(int)asset.Type} ({asset.Type})" +
$"\nAsset size: {asset.Size}" +
$"\nParameter count: {asset.ParameterCount}"
, Path.GetFileName(asset.Filename) + " Asset info");
}
}
private void extractToolStripMenuItem_Click(object sender, EventArgs e)
{
TreeNode node = treeViewMain.SelectedNode;
if (node == null)
{
MessageBox.Show(this, "The selected node was null. Please select a node and try again.", "Node not extracted");
return;
}
if (node.Tag == null)
{
OpenFolderDialog dialog = new OpenFolderDialog();
dialog.Title = @"Select destination folder";
if (dialog.ShowDialog(Handle) == true)
extractFolder(dialog.ResultPath);
}
else if (node.TryGetTagData(out PckAsset asset))
{
using SaveFileDialog exFile = new SaveFileDialog();
exFile.FileName = Path.GetFileName(asset.Filename);
exFile.Filter = Path.GetExtension(asset.Filename).Replace(".", string.Empty) + " File|*" + Path.GetExtension(asset.Filename);
if (exFile.ShowDialog(this) != DialogResult.OK ||
// Makes sure chosen directory isn't null or whitespace AKA makes sure its usable
string.IsNullOrWhiteSpace(Path.GetDirectoryName(exFile.FileName)))
{
MessageBox.Show(this, "The chosen directory is invalid. Please choose a different one and try again.", "Node not extracted");
return;
}
extractFile(exFile.FileName, asset);
}
// Verification that file extraction path was successful
MessageBox.Show(this, $"\"{node.Text}\" successfully extracted");
}
private void extractFolder(string outPath)
{
TreeNode node = treeViewMain.SelectedNode;
string selectedFolder = node.FullPath;
foreach (PckAsset asset in EditorValue.File.GetAssets().Where(asset => asset.Filename.StartsWith(selectedFolder)))
{
TreeNode node = treeViewMain.SelectedNode;
string abbreviatedPath = Path.GetDirectoryName(asset.Filename);
int startIndex = abbreviatedPath.IndexOf(node.Text);
abbreviatedPath = abbreviatedPath.Substring(startIndex, abbreviatedPath.Length - startIndex);
string finalPath = ($"{outPath}/{abbreviatedPath}/").Replace('\\', '/');
if (!Directory.Exists(finalPath))
Directory.CreateDirectory(finalPath);
extractFile(finalPath + "/" + Path.GetFileName(asset.Filename), asset);
}
}
private void extractFile(string outFilePath, PckAsset asset)
{
File.WriteAllBytes(outFilePath, asset.Data);
// ASSET_TYPE is a temporary custom parameter to track what kind of asset the file was on extraction
string typeParam = "ASSET_TYPE";
List<string> parameters = parameters = asset.SerializeParameters().ToList();
parameters.Insert(0, $"{typeParam} {(int)asset.Type}");
File.WriteAllLines($"{outFilePath}.txt", parameters.ToArray());
asset.RemoveParameter(typeParam);
}
private void cloneFileToolStripMenuItem_Click(object sender, EventArgs e)
{
TreeNode node = treeViewMain.SelectedNode;
if (node == null || !node.IsTagOfType<PckAsset>())
return;
string path = node.FullPath;
using TextPrompt diag = new TextPrompt(node.Tag is null ? Path.GetFileName(node.FullPath) : node.FullPath);
diag.contextLabel.Text = $"Creating a clone of \"{path}\". Ensure that the path isn't yet.";
diag.OKButtonText = "Clone";
if (diag.ShowDialog(this) == DialogResult.OK)
{
if (node.Tag is PckAsset asset)
{
TreeNode newNode = new TreeNode();
newNode.Text = Path.GetFileName(diag.NewText);
var newFile = new PckAsset(diag.NewText, asset.Type);
foreach (KeyValuePair<string, string> parameter in asset.GetParameters())
{
newFile.AddParameter(parameter);
}
newFile.SetData(asset.Data);
newFile.Filename = diag.NewText;
newNode.Tag = newFile;
newNode.ImageIndex = node.ImageIndex;
newNode.SelectedImageIndex = node.SelectedImageIndex;
if (GetAllChildNodes(treeViewMain.Nodes).FirstOrDefault(n => n.FullPath == diag.NewText) is not null)
{
MessageBox.Show(
this,
$"A file with the path \"{diag.NewText}\" already exists. " +
$"Please try again with a different name.",
"Key already exists");
return;
}
TreeNodeCollection nodeCollection = node.Parent?.Nodes ?? treeViewMain.Nodes;
nodeCollection.Insert(node.Index + 1, newNode);
EditorValue.File.InsertAsset(node.Index + 1, newFile);
BuildMainTreeView();
_wasModified = true;
}
}
}
private void renameFileToolStripMenuItem_Click(object sender, EventArgs e)
{
TreeNode node = treeViewMain.SelectedNode;
if (node == null)
return;
string path = node.FullPath;
bool isFile = node.TryGetTagData(out PckAsset asset);
using TextPrompt diag = new TextPrompt(isFile ? asset.Filename : Path.GetFileName(node.FullPath));
if (diag.ShowDialog(this) == DialogResult.OK)
{
if (isFile)
{
if (EditorValue.File.Contains(diag.NewText, asset.Type))
{
MessageBox.Show(this, $"{diag.NewText} already exists", "File already exists");
return;
}
asset.Filename = diag.NewText;
}
else // folders
{
node.Text = diag.NewText;
foreach (TreeNode childNode in GetAllChildNodes(node.Nodes))
{
if (childNode.Tag is PckAsset folderAsset)
{
if (folderAsset.Filename == diag.NewText)
continue;
folderAsset.Filename = childNode.FullPath;
}
}
}
_wasModified = true;
BuildMainTreeView();
}
}
private void replaceToolStripMenuItem_Click(object sender, EventArgs e)
{
if (treeViewMain.SelectedNode.Tag is PckAsset asset)
{
using var ofd = new OpenFileDialog();
// Suddenly, and randomly, this started throwing an exception because it wasn't formatted correctly? So now it's formatted correctly and now displays the file type name in the dialog.
string extra_extensions = "";
switch (asset.Type)
{
case PckAssetType.TextureFile:
if (Path.GetExtension(asset.Filename) == ".png")
extra_extensions = ";*.tga";
else if (Path.GetExtension(asset.Filename) == ".tga")
extra_extensions = ";*.png";
break;
}
string fileExt = Path.GetExtension(asset.Filename);
ofd.Filter = $"{asset.Type} (*{fileExt}{extra_extensions})|*{fileExt}{extra_extensions}";
if (ofd.ShowDialog(this) == DialogResult.OK)
{
byte[] fileData = File.ReadAllBytes(ofd.FileName);
bool isSkin = asset.Type == PckAssetType.SkinFile;
bool isCape = asset.Type == PckAssetType.CapeFile;
if (Settings.Default.ValidateImageDimension && (isSkin || isCape))
{
try
{
Image texture = Image.FromStream(new MemoryStream(fileData));
bool isEqualDimensions = texture.Width == texture.Height;
bool isHalfHeight = texture.Height == (texture.Width / 2);
Console.WriteLine($"{texture.Width} {texture.Height}");
if (isSkin && !isEqualDimensions && !isHalfHeight)
{
MessageBox.Show("The selected image does not suit a skin texture.", "Invalid image dimensions.", MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}
if (isCape && !isHalfHeight)
{
MessageBox.Show("The selected image does not suit a cape texture.", "Invalid image dimensions.", MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}
texture.Dispose();
}
catch
{
MessageBox.Show("The selected file is not a valid image.", "Invalid image.", MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}
}
string newFileExt = Path.GetExtension(ofd.FileName);
asset.SetData(File.ReadAllBytes(ofd.FileName));
asset.Filename = asset.Filename.Replace(fileExt, newFileExt);
_wasModified = true;
if(asset.Type == PckAssetType.SkinFile || asset.Type == PckAssetType.CapeFile)
{
GetNodeIconKey(asset, true); // reset node icon
}
BuildMainTreeView();
}
return;
}
MessageBox.Show(this, "Can't replace a folder.");
}
/// <summary>
/// Action to run before an asset will be deleted
/// </summary>
/// <param name="asset">Asset to remove</param>
/// <returns>True if the remove should be canceled, otherwise False</returns>
private bool BeforeFileRemove(PckAsset asset)
{
string itemPath = ResourceLocation.GetPathFromCategory(ResourceCategory.ItemAnimation);
// warn the user about deleting compass.png and clock.png
if (asset.Type == PckAssetType.TextureFile &&
(asset.Filename == itemPath + "/compass.png" || asset.Filename == itemPath + "/clock.png"))
{
if (MessageBox.Show(this, "Are you sure want to delete this file? If \"compass.png\" or \"clock.png\" are missing, your game will crash upon loading this pack.", "Warning",
MessageBoxButtons.YesNo, MessageBoxIcon.Warning) == DialogResult.No)
return true;
}
// remove loc key if its a skin/cape
if (asset.Type == PckAssetType.SkinFile || asset.Type == PckAssetType.CapeFile)
{
if (TryGetLocFile(out LOCFile locFile))
{
if (asset.TryGetParameter("THEMENAMEID", out string value))
locFile.RemoveLocKey(value);
if (asset.TryGetParameter("DISPLAYNAMEID", out value))
locFile.RemoveLocKey(value);
TrySetLocFile(locFile);
}
}
return false;
}
private void deleteFileToolStripMenuItem_Click(object sender, EventArgs e)
{
TreeNode node = treeViewMain.SelectedNode;
if (node == null)
return;
string path = node.FullPath;
if (node.TryGetTagData(out PckAsset asset))
{
if (!BeforeFileRemove(asset) && EditorValue.File.RemoveAsset(asset))
{
node.Remove();
_wasModified = true;
}
}
else if (MessageBox.Show(this, "Are you sure want to delete this folder? All contents will be deleted", "Warning",
MessageBoxButtons.YesNo, MessageBoxIcon.Warning) == DialogResult.Yes)
{
string pckFolderDir = node.FullPath;
EditorValue.File.RemoveAll(file => file.Filename.StartsWith(pckFolderDir) && !BeforeFileRemove(file));
node.Remove();
_wasModified = true;
}
}
private void treeParameter_AfterSelect(object sender, TreeViewEventArgs e)
{
if (e.Node is TreeNode t && t.Tag is KeyValuePair<string, string> parameter)
{
ParameterTypeTextBox.Text = parameter.Key;
parameterValueTextBox.Text = parameter.Value;
}
}
private void treeViewMain_KeyDown(object sender, KeyEventArgs e)
{
switch (e.KeyCode)
{
case Keys.Delete:
deleteFileToolStripMenuItem_Click(sender, e);
break;
case Keys.F2:
renameFileToolStripMenuItem_Click(sender, e);
break;
}
}
private void treeViewMain_BeforeLabelEdit(object sender, NodeLabelEditEventArgs e)
{
// for now name edits are done through the 'rename' context menu item
// TODO: add folder renaming
//e.CancelEdit = e.Node.Tag is PckAsset;
e.CancelEdit = true;
}
private void editAllParametersToolStripMenuItem_Click(object sender, EventArgs e)
{
if (treeViewMain.SelectedNode.TryGetTagData(out PckAsset asset))
{
IEnumerable<string> parameters = asset.SerializeParameters();
using (var input = new MultiTextPrompt(parameters))
{
if (input.ShowDialog(this) == DialogResult.OK)
{
asset.ClearParameters();
asset.DeserializeParameters(input.TextOutput);
ReloadParameterTreeView();
_wasModified = true;
}
}
}
}
private void WriteSkinAdjustments(PckAsset asset, SkinANIM anim, SkinGameFlags gameFlags)
{
if (asset.HasParameter("ANIM"))
asset.SetParameter("ANIM", anim.ToString());
else
asset.AddParameter(new KeyValuePair<string, string>("ANIM", anim.ToString()));
if (asset.HasParameter("GAME_FLAGS"))
asset.SetParameter("GAME_FLAGS", gameFlags.ToString());
else
asset.AddParameter(new KeyValuePair<string, string>("ANIM", gameFlags.ToString()));
ReloadParameterTreeView();
SetFileType(PckAssetType.SkinFile); // just a dirty way to get the right icon in case the anim was changed
_wasModified = true;
}
private void EditSkinAdjustments(PckAsset asset)
{
using SkinAdjustmentsEditor diag = new SkinAdjustmentsEditor(asset.GetSkin());
if (diag.ShowDialog(this) == DialogResult.OK)
{
WriteSkinAdjustments(asset, diag.anim, diag.gameFlags);
}
}
private void treeParameter_DoubleClick(object sender, EventArgs e)
{
if (treeParameters.SelectedNode is TreeNode subnode && subnode.Tag is KeyValuePair<string, string> parameter &&
treeViewMain.SelectedNode is TreeNode node && node.Tag is PckAsset asset)
{
if (asset.HasParameter(parameter.Key))
{
switch (parameter.Key)
{
case "ANIM" when asset.Type == PckAssetType.SkinFile:
case "GAME_FLAGS" when asset.Type == PckAssetType.SkinFile:
EditSkinAdjustments(asset);
return; // no error handling nor manual editing needed for skin adjustments
case "BOX" when asset.Type == PckAssetType.SkinFile:
try
{
using BoxEditor diag = new BoxEditor(parameter.Value, EditorValue.File.xmlVersion);
if (diag.ShowDialog(this) == DialogResult.OK)
{
asset.SetParameter(asset.GetParameterIndex(parameter), new KeyValuePair<string, string>("BOX", diag.Result.ToString()));
ReloadParameterTreeView();
_wasModified = true;
}
return;
}
catch (Exception ex)
{
Debug.WriteLine(ex.Message);
Trace.WriteLine("Invalid BOX value: " + parameter.Value);
MessageBox.Show(this, "Failed to parse BOX value, aborting to normal functionality.");
}
break;
default:
break;
}
using (AddParameterPrompt addParameter = new AddParameterPrompt(parameter))
{
if (addParameter.ShowDialog(this) == DialogResult.OK)
{
asset.SetParameter(asset.GetParameterIndex(parameter), addParameter.Parameter);
ReloadParameterTreeView();
_wasModified = true;
}
}
}
}
}
private void treeParameter_KeyDown(object sender, KeyEventArgs e)
{
if (e.KeyData == Keys.Delete)
deleteParameterToolStripMenuItem_Click(sender, e);
}
private void addMultipleParametersToolStripMenuItem1_Click(object sender, EventArgs e)
{
if (treeViewMain.SelectedNode.TryGetTagData(out PckAsset asset))
{
using var input = new MultiTextPrompt();
if (input.ShowDialog(this) == DialogResult.OK)
{
asset.DeserializeParameters(input.TextOutput);
ReloadParameterTreeView();
_wasModified = true;
}
}
}
private void addBOXParameterToolStripMenuItem1_Click(object sender, EventArgs e)
{
if (treeViewMain.SelectedNode is TreeNode t && t.Tag is PckAsset asset)
{
using BoxEditor diag = new BoxEditor(SkinBOX.DefaultHead, EditorValue.File.xmlVersion);
if (diag.ShowDialog(this) == DialogResult.OK)
{
asset.AddParameter("BOX", diag.Result);
ReloadParameterTreeView();
_wasModified = true;
}
return;
}
}
private void addSkinAdjustmentParametersToolStripMenuItem_Click(object sender, EventArgs e)
{
if (treeViewMain.SelectedNode.TryGetTagData(out PckAsset asset))
{
EditSkinAdjustments(asset);
return;
}
}
private void deleteParameterToolStripMenuItem_Click(object sender, EventArgs e)
{
if (treeParameters.SelectedNode is TreeNode t && t.Tag is KeyValuePair<string, string> parameter &&
treeViewMain.SelectedNode is TreeNode main && main.Tag is PckAsset asset &&
asset.RemoveParameter(parameter))
{
treeParameters.SelectedNode.Remove();
_wasModified = true;
}
}
private void addParameterToolStripMenuItem_Click(object sender, EventArgs e)
{
if (treeViewMain.SelectedNode is TreeNode t &&
t.Tag is PckAsset asset)
{
using AddParameterPrompt addParameter = new AddParameterPrompt();
if (addParameter.ShowDialog(this) == DialogResult.OK)
{
asset.AddParameter(addParameter.Parameter);
ReloadParameterTreeView();
_wasModified = true;
}
}
}
private static bool TryGetDefaultEntityModel(string modelName, out Model model)
{
if (!GameModelImporter.DefaultModels.TryGetValue(modelName, out DefaultModel defaultModel) || defaultModel is null)
{
model = default;
return false;
}
model = new Model(modelName, new Size((int)defaultModel.TextureSize.X, (int)defaultModel.TextureSize.Y));
foreach (DefaultPart defaultPart in defaultModel.Parts)
{
ModelPart modelPart = new ModelPart(defaultPart.Name, "", defaultPart.Translation, defaultPart.Rotation, System.Numerics.Vector3.Zero);
modelPart.AddBoxes(defaultPart.Boxes.Select(defaultBox => new ModelBox(defaultBox.Position, defaultBox.Size, defaultBox.Uv, defaultBox.Inflate, defaultBox.Mirror)));
model.AddPart(modelPart);
}
return true;
}
private void ShowSimpleModelRender(Model model, NamedData<Image> modelTexture)
{
MetroForm form = new MetroForm();
form.Icon = Resources.ProjectLogo;
form.Theme = MetroFramework.MetroThemeStyle.Dark;
form.Style = MetroFramework.MetroColorStyle.Silver;
form.StartPosition = FormStartPosition.CenterParent;
form.Text = $"{model.Name} - {modelTexture.Name}";
form.Size = new Size(600, 500);
form.MinimumSize = new Size(300, 300);
void ExportToolStripItem_Click(object sender, EventArgs e)
{
GameModelImporter.Default.ExportSettings.CreateModelOutline =
MessageBox.Show(
$"Do you wish to have all model parts contained in a group called '{model.Name}'?",
"Group model parts", MessageBoxButtons.YesNo, MessageBoxIcon.Question) == DialogResult.Yes;
using SaveFileDialog openFileDialog = new SaveFileDialog();
openFileDialog.FileName = model.Name;
openFileDialog.Filter = GameModelImporter.Default.SupportedModelFileFormatsFilter;
if (openFileDialog.ShowDialog(this) == DialogResult.OK)
{
var modelInfo = new GameModelInfo(model, new NamedData<Image>[1] { modelTexture });
GameModelImporter.Default.Export(openFileDialog.FileName, modelInfo);
}
}
ToolStripItem exportToolStripItem = new ToolStripButton("Export");
exportToolStripItem.Click += ExportToolStripItem_Click;
MenuStrip menu = new MenuStrip();
menu.BackColor = Color.FromArgb(35, 35, 35);
menu.ForeColor = Color.WhiteSmoke;
menu.Anchor = AnchorStyles.Top;
menu.Dock = DockStyle.Top;
menu.Items.Add(exportToolStripItem);
ModelRenderer renderer = new ModelRenderer();
form.Controls.Add(menu);
form.Controls.Add(renderer);
renderer.VSync = true;
renderer.BackColor = Color.FromArgb(30, 30, 30);
renderer.Dock = DockStyle.Fill;
renderer.Texture = modelTexture.Value;
renderer.LoadModel(model);
renderer.ResetCamera();
form.ShowDialog(this);
renderer.Dispose();
form.Dispose();
}
private void PckEditor_Load(object sender, EventArgs e)
{
CheckForPasswordAndRemove();
BuildMainTreeView();
UpdateRichPresence();
}
private void buttonEdit_Click(object sender, EventArgs e)
{
treeViewMain_DoubleClick(sender, e);
}
private void SetPckXMLVersion(int xmlVersion)
{
bool isBigEndian = BigEndianCheckBox.Checked;
try
{
if (treeViewMain.SelectedNode.Tag is PckAsset asset && (asset.Type is PckAssetType.AudioFile || asset.Type is PckAssetType.SkinDataFile || asset.Type is PckAssetType.TexturePackInfoFile))
{
OMI.ByteOrder endianness = isBigEndian ? OMI.ByteOrder.BigEndian : OMI.ByteOrder.LittleEndian;
IDataFormatReader reader = new PckFileReader(endianness);
object pck = reader.FromStream(new MemoryStream(asset.Data));
IDataFormatWriter writer = new PckFileWriter((PckFile)pck, endianness, xmlVersion); // write the pck file as given xmlversion
asset.SetData(writer);
_wasModified = true;
MessageBox.Show($"\"{asset.Filename}\" successfully set to BOX version {xmlVersion}", "Set PCK BOX Version");
}
}
catch (OverflowException)
{
MessageBox.Show(this, $"\"PCK cannot be set because the file endianness is not {(isBigEndian ? "big" : "little")}.", "Unable to set PCK BOX Version");
return;
}
catch (Exception ex)
{
MessageBox.Show(this, ex.Message, "Not a valid PCK file");
return;
}
}
private void setBoxVersion0ToolStripMenuItem_Click(object sender, EventArgs e) => SetPckXMLVersion(0);
private void setBoxVersion1ToolStripMenuItem_Click(object sender, EventArgs e) => SetPckXMLVersion(1);
private void setBoxVersion2ToolStripMenuItem_Click(object sender, EventArgs e) => SetPckXMLVersion(2);
private void setBoxVersion3ToolStripMenuItem_Click(object sender, EventArgs e) => SetPckXMLVersion(3);
private void SetModelVersion(int version)
{
if (treeViewMain.SelectedNode.Tag is PckAsset asset && asset.Type is PckAssetType.ModelsFile)
{
try
{
ModelContainer container = asset.GetData(new ModelFileReader());
if (container.Version == version)
{
MessageBox.Show(
this,
$"This model container is already Version {version + 1}.",
"Can't convert", MessageBoxButtons.OK, MessageBoxIcon.Error
);
return;
}
if (version == 2 &&
MessageBox.Show(
this,
"Conversion to 1.14 models.bin format does not yet support parent declaration and may not be 100% accurate.\n" +
"Would you like to continue?", "Warning", MessageBoxButtons.YesNo, MessageBoxIcon.Warning) != DialogResult.Yes
)
{
return;
}
if (container.Version > 1 &&
MessageBox.Show(
this,
"Conversion from 1.14 models.bin format does not yet support parent parts and may not be 100% accurate.\n" +
"Would you like to continue?", "Warning", MessageBoxButtons.YesNo, MessageBoxIcon.Warning) != DialogResult.Yes
)
{
return;
}
asset.SetData(new ModelFileWriter(container, version));
_wasModified = true;
MessageBox.Show(
this,
$"\"{asset.Filename}\" successfully converted to Version {version + 1} format.",
"Converted model container file"
);
}
catch (Exception ex)
{
MessageBox.Show(this, ex.Message, "Not a valid model container file.");
return;
}
}
}
private void setModelVersion1ToolStripMenuItem_Click(object sender, EventArgs e) => SetModelVersion(0);
private void setModelVersion2ToolStripMenuItem_Click(object sender, EventArgs e) => SetModelVersion(1);
private void setModelVersion3ToolStripMenuItem_Click(object sender, EventArgs e) => SetModelVersion(2);
private void treeViewMain_NodeMouseClick(object sender, TreeNodeMouseClickEventArgs e)
{
if (e.Node is not null)
treeViewMain.SelectedNode = e.Node;
}
private void exportIconToolStripMenuItem_Click(object sender, EventArgs e)
{
if (treeViewMain?.SelectedNode.TryGetTagData(out PckAsset asset) ?? false)
{
if(asset.Type != PckAssetType.CapeFile)
{
return;
}
using SaveFileDialog saveFileDialog = new SaveFileDialog();
saveFileDialog.FileName = Path.GetFileName(asset.Filename);
saveFileDialog.Filter = Path.GetExtension(asset.Filename).Replace(".", string.Empty) + " File|*" + Path.GetExtension(asset.Filename);
if (saveFileDialog.ShowDialog(this) != DialogResult.OK ||
// Makes sure chosen directory isn't null or whitespace AKA makes sure its usable
string.IsNullOrWhiteSpace(Path.GetDirectoryName(saveFileDialog.FileName)))
{
MessageBox.Show(this, "The chosen directory is invalid. Please choose a different one and try again.", "Node not extracted");
return;
}
Image capeTexture = null;
try
{
capeTexture = asset.GetTexture();
}
catch
{
MessageBox.Show(this, "Failed to get image data from Cape", "Icon not exported");
return;
}
DrawCapeIcon(capeTexture).Save(saveFileDialog.FileName);
}
}
private void exportSkinIcon(bool cropped = false)
{
if (treeViewMain?.SelectedNode.TryGetTagData(out PckAsset asset) ?? false)
{
if (asset.Type != PckAssetType.SkinFile)
{
return;
}
using SaveFileDialog saveFileDialog = new SaveFileDialog();
saveFileDialog.FileName = Path.GetFileName(asset.Filename);
saveFileDialog.Filter = Path.GetExtension(asset.Filename).Replace(".", string.Empty) + " File|*" + Path.GetExtension(asset.Filename);
if (saveFileDialog.ShowDialog(this) != DialogResult.OK ||
// Makes sure chosen directory isn't null or whitespace AKA makes sure its usable
string.IsNullOrWhiteSpace(Path.GetDirectoryName(saveFileDialog.FileName)))
{
MessageBox.Show(this, "The chosen directory is invalid. Please choose a different one and try again.", "Node not extracted");
return;
}
Skin skin = null;
try
{
skin = asset.GetSkin();
}
catch
{
MessageBox.Show(this, "Failed to get skin from node", "Icon not exported");
return;
}
skin.DrawPaperDoll(bustCrop: cropped).Save(saveFileDialog.FileName);
}
}
private void fullBodyToolStripMenuItem_Click(object sender, EventArgs e) => exportSkinIcon();
private void croppedToolStripMenuItem_Click(object sender, EventArgs e) => exportSkinIcon(cropped: true);
private void addOFFSETParameterToolStripMenuItem_Click(object sender, EventArgs e)
{
if (treeViewMain?.SelectedNode.TryGetTagData(out PckAsset asset) ?? false)
{
Skin skin = null;
try
{
skin = asset.GetSkin();
}
catch {
MessageBox.Show(this, "Failed to get skin", "Parameter not added");
return;
}
// code borrowed from CustomSkinEditor
var offsets = skin.Model.PartOffsets.Select(offset => offset.Type).ToList();
string[] available = SkinPartOffset.ValidModelOffsetTypes.Where(s => !offsets.Contains(s)).ToArray();
if(available.Length == 0)
{
MessageBox.Show(this, "All possible offset types are already present on this skin.", "Parameter not added");
return;
}
using ItemSelectionPopUp typeSelection = new ItemSelectionPopUp(available);
const float cOffsetMaximum = 100_000f;
using NumericPrompt valuePrompt = new NumericPrompt(0f, -cOffsetMaximum, cOffsetMaximum);
valuePrompt.DecimalPlaces = 1;
valuePrompt.ValueStep = (decimal)0.1f;
valuePrompt.OKButton.Text = "Add";
if (typeSelection.ShowDialog() == DialogResult.OK && valuePrompt.ShowDialog() == DialogResult.OK)
{
asset.AddParameter(SkinPartOffset.FromString($"{typeSelection.SelectedItem} Y {(float)valuePrompt.SelectedValue}").ToParameter());
ReloadParameterTreeView();
_wasModified = true;
}
}
}
private void bigEndianCheckbox_CheckChanged(object sender, EventArgs e)
{
OMI.ByteOrder endianness = BigEndianCheckBox.Checked ? OMI.ByteOrder.BigEndian : OMI.ByteOrder.LittleEndian;
foreach (PckAsset asset in EditorValue.File.GetAssets())
{
try
{
if (asset.Type is PckAssetType.AudioFile || asset.Type is PckAssetType.SkinDataFile || asset.Type is PckAssetType.TexturePackInfoFile)
{
IDataFormatReader reader = asset.Type is PckAssetType.AudioFile
? new PckAudioFileReader(endianness == OMI.ByteOrder.BigEndian ? OMI.ByteOrder.LittleEndian : OMI.ByteOrder.BigEndian)
: new PckFileReader(endianness == OMI.ByteOrder.BigEndian ? OMI.ByteOrder.LittleEndian : OMI.ByteOrder.BigEndian);
object pck = reader.FromStream(new MemoryStream(asset.Data));
IDataFormatWriter writer = asset.Type is PckAssetType.AudioFile
? new PckAudioFileWriter((PckAudioFile)pck, endianness)
: new PckFileWriter((PckFile)pck, endianness);
asset.SetData(writer);
_wasModified = true;
MessageBox.Show($"\"{asset.Filename}\" successfully converted to {(endianness == OMI.ByteOrder.LittleEndian ? "little" : "big")} endian.", "Converted PCK file");
}
}
catch (OverflowException)
{
MessageBox.Show(this, $"\"{asset.Filename}\" was not a valid {(endianness != OMI.ByteOrder.LittleEndian ? "little" : "big")} endian PCK File.", "Failed to convert PCK file");
continue;
}
catch (Exception ex)
{
MessageBox.Show(this, $"\"{asset.Filename}\" was not a valid PCK File.", "Failed to convert PCK file");
continue;
}
}
}
private void contextMenuPCKEntries_Opening(object sender, System.ComponentModel.CancelEventArgs e)
{
generateMipMapTextureToolStripMenuItem1.Visible = false;
setModelContainerFormatToolStripMenuItem.Visible = false;
setSubPCKBOXVersionToolStripMenuItem.Visible = false;
exportToolStripMenuItem.Visible = false;
toolStripSeparator5.Visible = false;
toolStripSeparator6.Visible = false;
exportIconToolStripMenuItem.Visible = false;
fullBodyToolStripMenuItem.Visible = false;
croppedToolStripMenuItem.Visible = false;
if (treeViewMain?.SelectedNode.TryGetTagData(out PckAsset asset) ?? false)
{
replaceToolStripMenuItem.Visible = true;
cloneFileToolStripMenuItem.Visible = true;
setFileTypeToolStripMenuItem.Visible = true;
toolStripSeparator5.Visible = true;
toolStripSeparator6.Visible = true;
bool customSkinIcons = Settings.Default.UseCustomSkinIcons;
switch (asset.Type)
{
case PckAssetType.SkinFile:
exportToolStripMenuItem.Visible = true;
exportIconToolStripMenuItem.Visible = customSkinIcons; // only enable if setting is true, no point in exporting custom icons otherwise
fullBodyToolStripMenuItem.Visible = customSkinIcons; // these 2 are sub menu items of export icon - May
croppedToolStripMenuItem.Visible = customSkinIcons;
break;
case PckAssetType.CapeFile:
exportIconToolStripMenuItem.Visible = Settings.Default.UseCustomCapeIcons;
break;
case PckAssetType.TextureFile:
generateMipMapTextureToolStripMenuItem1.Visible = true;
break;
case PckAssetType.ModelsFile:
setModelContainerFormatToolStripMenuItem.Visible = true;
break;
case PckAssetType.SkinDataFile:
setSubPCKBOXVersionToolStripMenuItem.Visible = true;
break;
}
}
else
{
replaceToolStripMenuItem.Visible = false;
cloneFileToolStripMenuItem.Visible = false;
setFileTypeToolStripMenuItem.Visible = false;
}
}
}
}