Move Common functionality to Core project & rendering and Model support as well

This commit is contained in:
miku-666
2025-09-01 23:03:39 +02:00
parent 698056a0a0
commit 9656c8b48d
177 changed files with 5979 additions and 1279 deletions

193
PckStudio.Core/Animation.cs Normal file
View File

@@ -0,0 +1,193 @@
/* Copyright (c) 2023-present miku-666
* This software is provided 'as-is', without any express or implied
* warranty. In no event will the authors be held liable for any damages
* arising from the use of this software.
*
* Permission is granted to anyone to use this software for any purpose,
* including commercial applications, and to alter it and redistribute it
* freely, subject to the following restrictions:
*
* 1. The origin of this software must not be misrepresented; you must not
* claim that you wrote the original software. If you use this software
* in a product, an acknowledgment in the product documentation would be
* appreciated but is not required.
* 2. Altered source versions must be plainly marked as such, and must not be
* misrepresented as being the original software.
* 3. This notice may not be removed or altered from any source distribution.
**/
using System;
using System.Collections.Generic;
using System.Drawing;
using PckStudio.Core.Extensions;
using System.Text;
using System.Collections.ObjectModel;
using System.Linq;
namespace PckStudio.Core
{
public sealed class Animation
{
public const int MinimumFrameTime = 1;
public int FrameCount => _frames.Count;
public int TextureCount => _textures.Count;
public bool Interpolate { get; set; } = false;
private readonly List<Image> _textures;
private readonly IList<Frame> _frames = new List<Frame>();
private object _syncLock = new object();
public Animation(IEnumerable<Image> textures, bool initFramesFromTextures = false)
{
_textures = new List<Image>(textures);
if (initFramesFromTextures)
AddTexturesAsFrames(MinimumFrameTime);
}
public class Frame
{
public readonly Image Texture;
public int Ticks
{
get
{
return _ticks;
}
set
{
lock(_syncObject)
{
_ticks = value;
}
}
}
private int _ticks;
private object _syncObject = new object();
public Frame(Image texture) : this(texture, MinimumFrameTime)
{ }
public Frame(Image texture, int frameTime)
{
Texture = texture;
Ticks = frameTime;
}
}
private void CheckTextureIndex(int index)
{
if (!_textures.IndexInRange(index))
throw new ArgumentOutOfRangeException(nameof(index));
}
public Frame AddFrame(int textureIndex, int frameTime)
{
CheckTextureIndex(textureIndex);
Frame frame = new Frame(_textures[textureIndex], frameTime);
_frames.Add(frame);
return frame;
}
private void AddTexturesAsFrames(int frameTime)
{
for (int i = 0; i < TextureCount; i++)
{
AddFrame(i, frameTime);
}
}
public bool RemoveFrame(int frameIndex)
{
_frames.RemoveAt(frameIndex);
return true;
}
public Frame GetFrame(int index) => _frames[index];
public IReadOnlyCollection<Frame> GetFrames()
{
return new ReadOnlyCollection<Frame>(_frames);
}
public IReadOnlyCollection<Frame> GetInterpolatedFrames()
{
if (Interpolate)
{
return new ReadOnlyCollection<Frame>(InternalGetInterpolatedFrames().ToList());
}
return GetFrames();
}
private IEnumerable<Frame> InternalGetInterpolatedFrames()
{
for (int i = 0; i < FrameCount; i++)
{
Frame currentFrame = _frames[i];
Frame nextFrame = _frames[0];
if (i + 1 < FrameCount)
nextFrame = _frames[i + 1];
for (int tick = 0; tick < currentFrame.Ticks; tick++)
{
double delta = 1.0f - tick / (double)currentFrame.Ticks;
yield return new Frame(currentFrame.Texture.Interpolate(nextFrame.Texture, delta));
}
}
yield break;
}
public IReadOnlyCollection<Image> GetTextures()
{
return _textures;
}
public int GetTextureIndex(Image frameTexture)
{
_ = frameTexture ?? throw new ArgumentNullException(nameof(frameTexture));
return _textures.IndexOf(frameTexture);
}
public void SetFrame(int frameIndex, Frame frame)
{
lock(_syncLock)
{
_frames[frameIndex] = frame;
}
}
public void SetFrame(int frameIndex, int textureIndex, int frameTime = MinimumFrameTime)
{
CheckTextureIndex(textureIndex);
SetFrame(frameIndex, new Frame(_textures[textureIndex], frameTime));
}
public void SetFrameTicks(int ticks)
{
lock(_syncLock)
{
foreach (Frame frame in _frames)
{
frame.Ticks = ticks;
}
}
}
public void SwapFrames(int sourceIndex, int destinationIndex)
{
lock(_syncLock)
{
_frames.Swap(sourceIndex, destinationIndex);
}
}
public static Animation CreateEmpty()
{
return new Animation(Array.Empty<Image>());
}
}
}

View File

@@ -0,0 +1,113 @@
/* Copyright (c) 2023-present miku-666
* This software is provided 'as-is', without any express or implied
* warranty. In no event will the authors be held liable for any damages
* arising from the use of this software.
*
* Permission is granted to anyone to use this software for any purpose,
* including commercial applications, and to alter it and redistribute it
* freely, subject to the following restrictions:
*
* 1.The origin of this software must not be misrepresented; you must not
* claim that you wrote the original software. If you use this software
* in a product, an acknowledgment in the product documentation would be
* appreciated but is not required.
* 2. Altered source versions must be plainly marked as such, and must not be
* misrepresented as being the original software.
* 3. This notice may not be removed or altered from any source distribution.
**/
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Configuration;
namespace PckStudio.Core.App
{
public sealed class SettingsManager
{
public bool IsReadOnly => _isReadOnly;
public const string KeyToStringContextKeyConst = "keyToString";
private Dictionary<string, Action<object>> _registery = new Dictionary<string, Action<object>>();
private object _newValue = null;
private ApplicationSettingsBase _settings = null;
private bool _isReadOnly;
private class InternalSettings : ApplicationSettingsBase
{ }
public SettingsManager(ApplicationSettingsBase settings, bool isReadOnly = false)
{
_settings = settings;
_isReadOnly = isReadOnly;
settings.PropertyChanged += PropertyChangedHandler;
settings.SettingChanging += SettingChangingHandler;
}
public ApplicationSettingsBase GetSettings() => _settings;
public static SettingsManager CreateSettings()
{
return new SettingsManager(new InternalSettings());
}
public bool RegisterPropertyChangedCallback<TSettingsType>(string propertyName, Action<TSettingsType> callback)
{
Type propertyType = _settings[propertyName].GetType();
if (!propertyType.Equals(typeof(TSettingsType)))
{
return false;
}
return RegisterPropertyChangedCallback(propertyName, delegate (object obj) { callback((TSettingsType)obj); });
}
public bool RegisterPropertyChangedCallback(string propertyName, Action callback)
{
return RegisterPropertyChangedCallback(propertyName, delegate (object _) { callback(); });
}
private bool RegisterPropertyChangedCallback(string propertyName, Action<object> callback)
{
if (_registery.ContainsKey(propertyName))
return false;
_registery.Add(propertyName, callback);
return true;
}
private void PropertyChangedHandler(object sender, PropertyChangedEventArgs e)
{
if (_registery.ContainsKey(e.PropertyName))
{
_registery[e.PropertyName]?.Invoke(_newValue);
_newValue = null;
}
}
private void SettingChangingHandler(object sender, SettingChangingEventArgs e)
{
if (_registery.ContainsKey(e.SettingName))
{
_newValue = e.NewValue;
}
}
public bool AddSetting<T>(string name, T initialValue, string description, Action<T> callback)
{
if (_isReadOnly)
throw new SettingsPropertyIsReadOnlyException("Can't add setting. Underlying SettingsBase is readonly.");
if (!_settings.Context.ContainsKey(KeyToStringContextKeyConst))
_settings.Context.Add(KeyToStringContextKeyConst, new Dictionary<string, string>());
var settingsProperty = new SettingsProperty(
name, typeof(T), null, false, default(T), SettingsSerializeAs.String, null, false, false);
_settings.Properties.Add(settingsProperty);
_settings.PropertyValues.Add(new SettingsPropertyValue(settingsProperty) { PropertyValue = initialValue });
if (_settings.Context[KeyToStringContextKeyConst] is Dictionary<string, string> dict)
dict.Add(name, description);
callback(initialValue);
return RegisterPropertyChangedCallback(name, callback);
}
}
}

View File

@@ -0,0 +1,63 @@
using System;
using System.IO;
using System.Windows.Forms;
using AutoUpdaterDotNET;
using Newtonsoft.Json;
using PckStudio.Core.Json;
namespace PckStudio.Core.App
{
public static class Updater
{
private static Uri _appCast;
private static bool _autoUpdate;
public static void Initialize(Uri appCast, bool autoUpdate = false)
{
_appCast = appCast;
_autoUpdate = autoUpdate;
//AutoUpdater.ClearAppDirectory = true;
#if DEBUG
AutoUpdater.ReportErrors = true;
#endif
AutoUpdater.DownloadPath = Application.StartupPath;
AutoUpdater.ExecutablePath = "./PCK-Studio.exe";
AutoUpdater.TopMost = true;
string jsonPath = Path.Combine(Environment.CurrentDirectory, "updates.json");
AutoUpdater.PersistenceProvider = new JsonFilePersistenceProvider(jsonPath);
AutoUpdater.ParseUpdateInfoEvent += AutoUpdaterOnParseUpdateInfoEvent;
//AutoUpdater.Icon = Resources.ProjectLogo.ToBitmap();
if (_autoUpdate)
{
UpdateToLatest();
}
}
public static void SetOwner(Form owner) => AutoUpdater.SetOwner(owner);
public static void UpdateToLatest()
{
#if NDEBUG
string url = $"{_appCast}/main/Version.json";
AutoUpdater.Start(url);
#endif
}
private static void AutoUpdaterOnParseUpdateInfoEvent(ParseUpdateInfoEventArgs args)
{
UpdateInformation json = JsonConvert.DeserializeObject<UpdateInformation>(args.RemoteData);
args.UpdateInfo = new UpdateInfoEventArgs
{
CurrentVersion = json.Version,
DownloadURL = json.Url,
ChangelogURL = json.Changelog,
Mandatory = new Mandatory()
{
Value = json.Mandatory,
}
};
}
}
}

View File

@@ -0,0 +1,37 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using PckStudio.Core.Extensions;
namespace PckStudio.Core
{
public struct BoundingBox
{
public static BoundingBox Empty = new BoundingBox(OpenTK.Vector3.Zero, OpenTK.Vector3.Zero);
public readonly OpenTK.Vector3 Start;
public readonly OpenTK.Vector3 End;
public readonly OpenTK.Vector3 Center;
public readonly OpenTK.Vector3 Volume;
public BoundingBox(OpenTK.Vector3 start, OpenTK.Vector3 end)
{
Start = start;
End = end;
OpenTK.Vector3 size = End - Start;
Volume = OpenTKExtensions.Abs(size);
Center = start + Volume / 2;
}
public BoundingBox(System.Numerics.Vector3 start, System.Numerics.Vector3 end)
: this(start.ToOpenTKVector(), end.ToOpenTKVector())
{
}
public OpenTK.Matrix4 GetTransform()
{
return OpenTK.Matrix4.CreateScale(Volume) * OpenTK.Matrix4.CreateTranslation(Start);
}
}
}

View File

@@ -0,0 +1,47 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using Newtonsoft.Json.Linq;
using PckStudio.Interfaces;
using PckStudio.Core;
namespace PckStudio.Core
{
public sealed class DelegatedFileSaveContext<T> : ISaveContext<T>
{
public delegate void SerializeDataToStreamDelegate(T value, Stream stream);
public bool AutoSave { get; }
public string Filepath { get; private set; }
private SerializeDataToStreamDelegate _serializeDataDelegate;
private FileDialogFilter _dialogFilter;
public DelegatedFileSaveContext(string filepath, bool autoSave, FileDialogFilter dialogFilter, SerializeDataToStreamDelegate serializeDataDelegate)
{
AutoSave = autoSave;
Filepath = filepath;
_serializeDataDelegate = serializeDataDelegate;
_dialogFilter = dialogFilter;
}
public void Save(T value)
{
if (!File.Exists(Filepath))
{
SaveFileDialog saveFileDialog = new SaveFileDialog();
saveFileDialog.Filter = _dialogFilter.ToString();
if (saveFileDialog.ShowDialog() != DialogResult.OK)
return;
Filepath = saveFileDialog.FileName;
}
using (Stream stream = File.OpenWrite(Filepath))
{
_serializeDataDelegate(value, stream);
}
}
}
}

View File

@@ -0,0 +1,19 @@
using System;
using PckStudio.Interfaces;
namespace PckStudio.Core
{
public class DelegatedSaveContext<T> : ISaveContext<T>
{
private readonly Action<T> _saveAction;
public bool AutoSave { get; }
public void Save(T value) => _saveAction(value);
public DelegatedSaveContext(bool autoSave, Action<T> saveAction)
{
AutoSave = autoSave;
_saveAction = saveAction;
}
}
}

View File

@@ -0,0 +1,115 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Newtonsoft.Json.Linq;
using OMI.Formats.Pck;
using PckStudio.Core;
using PckStudio.Core.Extensions;
using PckStudio.Interfaces;
namespace PckStudio.Core.Deserializer
{
public sealed class AnimationDeserializer : IPckAssetDeserializer<Animation>
{
public static readonly AnimationDeserializer DefaultDeserializer = new AnimationDeserializer();
public Animation Deserialize(PckAsset asset)
{
_ = asset ?? throw new ArgumentNullException(nameof(asset));
if (asset.Size > 0)
{
Image texture = asset.GetTexture();
IEnumerable<Image> frameTextures = texture.Split(ImageLayoutDirection.Vertical);
string animString = asset.GetProperty("ANIM");
bool animStringIsEmpty = string.IsNullOrEmpty(animString);
Animation animation = new Animation(frameTextures, animStringIsEmpty);
if (!animStringIsEmpty)
DeserializeAnimationAnim(ref animation, animString);
return animation;
}
return Animation.CreateEmpty();
}
private static bool DeserializeAnimationAnim(ref Animation animation, string animString)
{
animString = animString.Trim();
animation.Interpolate = animString.StartsWith("#");
animString = animation.Interpolate ? animString.Substring(1) : animString;
string[] animData = animString.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
if (animData.Length <= 0)
{
Trace.TraceError($"[{nameof(AnimationExtensions)}:{nameof(DeserializeAnimationAnim)}] Failed to parse anim. animData was empty.");
return false;
}
int lastFrameTime = Animation.MinimumFrameTime;
foreach (string frameInfo in animData)
{
string[] frameData = frameInfo.Split('*');
int currentFrameIndex = 0;
int.TryParse(frameData[0], out currentFrameIndex);
// Some textures like the Halloween 2015's Lava texture don't have a
// frame time parameter for certain frames.
// This will detect that and place the last frame time in its place.
// This is accurate to console edition behavior.
// - MattNL
int currentFrameTime = frameData.Length < 2 || string.IsNullOrEmpty(frameData[1]) ? lastFrameTime : int.Parse(frameData[1]);
animation.AddFrame(currentFrameIndex, currentFrameTime);
lastFrameTime = currentFrameTime;
}
return true;
}
public Animation DeserializeJavaAnimation(JObject jsonObject, Image texture)
{
IEnumerable<Image> textures = texture.Split(ImageLayoutDirection.Vertical);
Animation result = new Animation(textures);
if (jsonObject["animation"] is not JToken animation)
return result;
int frameTime = Animation.MinimumFrameTime;
if (animation["frametime"] is JToken frametime_token && frametime_token.Type == JTokenType.Integer)
frameTime = (int)frametime_token;
if (animation["interpolate"] is JToken interpolate_token && interpolate_token.Type == JTokenType.Boolean)
result.Interpolate = (bool)interpolate_token;
if (animation["frames"] is JToken frames_token && frames_token.Type == JTokenType.Array)
{
foreach (JToken frame in frames_token.Children())
{
if (frame.Type == JTokenType.Object &&
frame["index"] is JToken frame_index &&
frame_index.Type == JTokenType.Integer &&
frame["time"] is JToken frame_time &&
frame_time.Type == JTokenType.Integer)
{
Debug.WriteLine("Index: {0}, Time: {1}", frame_index, frame_time);
result.AddFrame((int)frame_index, (int)frame_time);
}
else if (frame.Type == JTokenType.Integer)
{
Debug.WriteLine("Index: {0}, Time: {1}", frame, frameTime);
result.AddFrame((int)frame, frameTime);
}
}
return result;
}
for (int i = 0; i < result.TextureCount; i++)
{
result.AddFrame(i, frameTime);
}
return result;
}
}
}

View File

@@ -0,0 +1,31 @@
using System;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using OMI.Formats.Pck;
using PckStudio.Interfaces;
using PckStudio.Core.IO.TGA;
namespace PckStudio.Core.Deserializer
{
internal sealed class ImageDeserializer : IPckAssetDeserializer<Image>
{
public static readonly ImageDeserializer DefaultDeserializer = new ImageDeserializer();
// TODO: replace empty image with image displaying something went wrong
private static Image EmptyImage = new Bitmap(1, 1, PixelFormat.Format32bppArgb);
public Image Deserialize(PckAsset asset)
{
_ = asset ?? throw new ArgumentNullException(nameof(asset));
if (asset.Size == 0)
return EmptyImage;
using var stream = new MemoryStream(asset.Data);
Image img = Path.GetExtension(asset.Filename) == ".tga"
? TGADeserializer.DeserializeFromStream(stream)
: Image.FromStream(stream);
return img.RawFormat != ImageFormat.Jpeg || img.RawFormat != ImageFormat.Png ? new Bitmap(img) : img;
}
}
}

View File

@@ -0,0 +1,24 @@
using System.Drawing;
using AnimatedGif;
namespace PckStudio.Core.Extensions
{
public static class AnimationExtensions
{
public static Image CreateAnimationImage(this Animation animation)
{
if (animation.FrameCount == 0)
{
return null;
}
var ms = new System.IO.MemoryStream();
var generateor = new AnimatedGifCreator(ms, GameConstants.GameTickInMilliseconds, 0);
foreach (Animation.Frame frame in animation.GetInterpolatedFrames())
{
generateor.AddFrame(frame.Texture, frame.Ticks * GameConstants.GameTickInMilliseconds, GifQuality.Bit8);
}
ms.Position = 0;
return Image.FromStream(ms);
}
}
}

View File

@@ -0,0 +1,14 @@
namespace PckStudio.Core.Extensions
{
public enum BlendMode
{
Add,
Subtract,
Multiply,
Average,
DescendingOrder,
AscendingOrder,
Screen,
Overlay
}
}

View File

@@ -0,0 +1,16 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace PckStudio.Core.Extensions
{
public static class BoundingBoxExtensions
{
public static BoundingBox GetEnclosingBoundingBox(this IEnumerable<BoundingBox> boundingBoxes)
{
return boundingBoxes.DefaultIfEmpty().Aggregate((a, b) => new BoundingBox(OpenTK.Vector3.ComponentMin(a.Start, b.Start), OpenTK.Vector3.ComponentMax(a.End, b.End)));
}
}
}

View File

@@ -0,0 +1,73 @@
using System.Drawing;
using System.Numerics;
namespace PckStudio.Core.Extensions
{
public static class ColorExtensions
{
/// <summary>
/// Normalizes the Color between 0.0 - 1.0
/// </summary>
/// <returns></returns>
public static Vector4 Normalize(this Color color)
{
return new Vector4(color.R / 255f, color.G / 255f, color.B / 255f, color.A / 255f);
}
public static Color Inversed(this Color color)
{
return Color.FromArgb(color.A, 255 - color.R, 255 - color.G, 255 - color.B);
}
public static Color GreyScaled(this Color color)
{
int greyScaleValue = (color.R + color.G + color.B) / 3;
return Color.FromArgb(color.A, greyScaleValue, greyScaleValue, greyScaleValue);
}
public static int ToBGR(this Color color)
{
return color.B << 16 | color.G << 8 | color.R;
}
public static byte BlendValues(byte source, byte overlay, BlendMode blendType)
{
return (byte)MathExtensions.Clamp(BlendValues(source / 255f, overlay / 255f, blendType) * 255, 0, 255);
}
public static float BlendValues(float source, float overlay, BlendMode blendType)
{
source = MathExtensions.Clamp(source, 0.0f, 1.0f);
overlay = MathExtensions.Clamp(overlay, 0.0f, 1.0f);
float resultValue = blendType switch
{
BlendMode.Add => source + overlay,
BlendMode.Subtract => source - overlay,
BlendMode.Multiply => source * overlay,
BlendMode.Average => (source + overlay) / 2.0f,
BlendMode.AscendingOrder => source > overlay ? overlay : source,
BlendMode.DescendingOrder => source < overlay ? overlay : source,
BlendMode.Screen => 1f - (1f - source) * (1f - overlay),
BlendMode.Overlay => source < 0.5f ? 2f * source * overlay : 1f - 2f * (1f - source) * (1f - overlay),
_ => 0.0f
};
return MathExtensions.Clamp(resultValue, 0.0f, 1.0f);
}
public static byte Mix(double ratio, byte val1, byte val2)
{
ratio = MathExtensions.Clamp(ratio, 0.0, 1.0);
return (byte)(ratio * val1 + (1.0 - ratio) * val2);
}
public static Color Mix(this Color c1, Color c2, double ratio)
{
ratio = MathExtensions.Clamp(ratio, 0.0, 1.0);
return Color.FromArgb(c1.A,
Mix(ratio, c1.R, c2.R),
Mix(ratio, c1.G, c2.G),
Mix(ratio, c1.B, c2.B)
);
}
}
}

View File

@@ -0,0 +1,51 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace PckStudio.Core.Extensions
{
public static class CursorExtensions
{
[StructLayout(LayoutKind.Sequential)]
struct PointStruct
{
public Int32 x;
public Int32 y;
}
[StructLayout(LayoutKind.Sequential)]
struct CursorInfoStruct
{
/// <summary> The structure size in bytes that must be set via calling Marshal.SizeOf(typeof(CursorInfoStruct)).</summary>
public Int32 cbSize;
/// <summary> The cursor state: 0 == hidden, 1 == showing, 2 == suppressed (is supposed to be when finger touch is used, but in practice finger touch results in 0, not 2)</summary>
public Int32 flags;
/// <summary> A handle to the cursor. </summary>
public IntPtr hCursor;
/// <summary> The cursor screen coordinates.</summary>
public PointStruct pt;
}
/// <summary> Must initialize cbSize</summary>
[DllImport("user32.dll")]
static extern bool GetCursorInfo(ref CursorInfoStruct pci);
public static bool IsVisible(this Cursor _)
{
CursorInfoStruct pci = new CursorInfoStruct();
pci.cbSize = Marshal.SizeOf(typeof(CursorInfoStruct));
GetCursorInfo(ref pci);
// const Int32 hidden = 0x00;
const Int32 showing = 0x01;
// const Int32 suppressed = 0x02;
bool isVisible = ((pci.flags & showing) != 0);
return isVisible;
}
}
}

View File

@@ -0,0 +1,38 @@
using System.Collections.Generic;
using System.Linq;
namespace PckStudio.Core.Extensions
{
public static class EnumerableExtensions
{
public static IEnumerable<(int index, T value)>enumerate<T>(this IEnumerable<T> array)
{
int i = 0;
foreach (T item in array)
{
yield return (i++, item);
}
yield break;
}
public static bool EqualsAny<T>(this T type, params T[] items)
{
foreach (T item in items)
{
if (item.Equals(type))
return true;
}
return false;
}
public static bool ContainsAny<T>(this IEnumerable<T> array, params T[] items)
{
foreach (T item in array)
{
if (items.Contains(item))
return true;
}
return false;
}
}
}

View File

@@ -0,0 +1,44 @@
using System.Drawing;
using System.Drawing.Drawing2D;
namespace PckStudio.Core.Extensions
{
public struct GraphicsConfig
{
public GraphicsConfig()
{
CompositingQuality = default;
InterpolationMode = default;
SmoothingMode = default;
PixelOffsetMode = default;
CompositingMode = default;
}
public CompositingMode CompositingMode { get; set; }
public CompositingQuality CompositingQuality { get; set; }
public InterpolationMode InterpolationMode { get; set; }
public SmoothingMode SmoothingMode { get; set; }
public PixelOffsetMode PixelOffsetMode { get; set; }
}
public static class GraphicsExtensions
{
public static void ApplyConfig(this Graphics graphics, GraphicsConfig config)
{
graphics.CompositingMode = config.CompositingMode;
graphics.CompositingQuality = config.CompositingQuality;
graphics.InterpolationMode = config.InterpolationMode;
graphics.SmoothingMode = config.SmoothingMode;
graphics.PixelOffsetMode = config.PixelOffsetMode;
}
public static Graphics Fill(this Graphics graphics, Rectangle area, Color color)
{
Region clip = graphics.Clip;
graphics.SetClip(area, CombineMode.Replace);
graphics.Clear(color);
graphics.SetClip(clip, CombineMode.Replace);
return graphics;
}
}
}

View File

@@ -0,0 +1,273 @@
/* Copyright (c) 2023-present miku-666
* This software is provided 'as-is', without any express or implied
* warranty. In no event will the authors be held liable for any damages
* arising from the use of this software.
*
* Permission is granted to anyone to use this software for any purpose,
* including commercial applications, and to alter it and redistribute it
* freely, subject to the following restrictions:
*
* 1.The origin of this software must not be misrepresented; you must not
* claim that you wrote the original software. If you use this software
* in a product, an acknowledgment in the product documentation would be
* appreciated but is not required.
* 2. Altered source versions must be plainly marked as such, and must not be
* misrepresented as being the original software.
* 3. This notice may not be removed or altered from any source distribution.
**/
using System;
using System.Drawing;
using System.Diagnostics;
using System.Drawing.Imaging;
using System.Drawing.Drawing2D;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
namespace PckStudio.Core.Extensions
{
public static class ImageExtensions
{
public static Image ReleaseFromFile(this Image image)
{
Image img = new Bitmap(image);
image.Dispose();
return img;
}
public static Image GetArea(this Image source, Rectangle area)
{
Image tileImage = new Bitmap(area.Width, area.Height);
using (Graphics gfx = Graphics.FromImage(tileImage))
{
gfx.SmoothingMode = SmoothingMode.None;
gfx.InterpolationMode = InterpolationMode.NearestNeighbor;
gfx.PixelOffsetMode = PixelOffsetMode.HighQuality;
gfx.DrawImage(source, new Rectangle(Point.Empty, area.Size), area, GraphicsUnit.Pixel);
}
return tileImage;
}
/// <summary>
/// Creates an IEnumerable by reading in horizontal order
/// </summary>
/// <param name="source">this image</param>
/// <param name="scalar">Indecates width and height of image sub section</param>
/// <returns><see cref="IEnumerable{Image}"/> of type <see cref="Image"/></returns>
public static IEnumerable<Image> SplitHorizontal(this Image source, int scalar)
{
return source.Split(scalar, ImageLayoutDirection.Horizontal);
}
public static IEnumerable<Image> Split(this Image source, int scalar, ImageLayoutDirection layoutDirection)
{
return Split(source, new Size(scalar, scalar), layoutDirection);
}
public static IEnumerable<Image> Split(this Image source, Size size, ImageLayoutDirection imageLayout)
{
int rowCount = source.Width / size.Width;
int columnCount = source.Height / size.Height;
Debug.WriteLine($"Image size: {source.Size}, Area size: {size}, col num: {columnCount}, row num: {rowCount}");
for (int i = 0; i < columnCount * rowCount; i++)
{
int row = Math.DivRem(i, rowCount, out int column);
if (imageLayout == ImageLayoutDirection.Vertical)
column = Math.DivRem(i, columnCount, out row);
Rectangle tileArea = new Rectangle(new Point(column * size.Width, row * size.Height), size);
yield return source.GetArea(tileArea);
}
yield break;
}
public static IEnumerable<Image> Split(this Image source, ImageLayoutDirection layoutDirection)
{
for (int i = 0; i < source.Height / source.Width; i++)
{
ImageSection locationInfo = new ImageSection(source.Size, i, layoutDirection);
yield return source.GetArea(locationInfo.Area);
}
yield break;
}
public static Image Combine(this IEnumerable<Image> sources, ImageLayoutDirection layoutDirection)
{
Size imageSize = CalculateImageSize(sources, layoutDirection);
var image = new Bitmap(imageSize.Width, imageSize.Height);
using (var graphic = Graphics.FromImage(image))
{
foreach ((int i, Image texture) in sources.enumerate())
{
var info = new ImageSection(texture.Size, i, layoutDirection);
graphic.DrawImage(texture, info.Point);
}
}
return image;
}
private static Size CalculateImageSize(IEnumerable<Image> sources, ImageLayoutDirection layoutDirection)
{
Size size = sources.First().Size;
int count = sources.Count();
if (count < 2)
return count < 1 ? Size.Empty : size;
var horizontal = layoutDirection == ImageLayoutDirection.Horizontal;
if (!sources.All(img => img.Size == size))
throw new InvalidOperationException("Images must have the same width and height.");
if (horizontal)
size.Width *= count;
else
size.Height *= count;
return size;
}
public static Image Resize(this Image image, Size size, GraphicsConfig graphicsConfig)
{
return image.Resize(size.Width, size.Height, graphicsConfig);
}
public static Image Resize(this Image image, int width, int height, GraphicsConfig graphicsConfig)
{
var destRect = new Rectangle(0, 0, width, height);
var destImage = new Bitmap(width, height);
destImage.SetResolution(image.HorizontalResolution, image.VerticalResolution);
using (var graphics = Graphics.FromImage(destImage))
{
graphics.ApplyConfig(graphicsConfig);
using (var wrapMode = new ImageAttributes())
{
wrapMode.SetWrapMode(WrapMode.TileFlipXY);
graphics.DrawImage(image, destRect, 0, 0, image.Width, image.Height, GraphicsUnit.Pixel, wrapMode);
}
}
return destImage;
}
public static Image Blend(this Image image, Color overlayColor, BlendMode mode)
{
if (image is not Bitmap baseImage)
return image;
Bitmap bitmapResult = new Bitmap(baseImage.Width, baseImage.Height, PixelFormat.Format32bppArgb);
BitmapData baseImageData = baseImage.LockBits(new Rectangle(Point.Empty, baseImage.Size),
ImageLockMode.ReadWrite, PixelFormat.Format32bppArgb);
BitmapData resultImageData = bitmapResult.LockBits(new Rectangle(Point.Empty, bitmapResult.Size),
ImageLockMode.WriteOnly, PixelFormat.Format32bppArgb);
Parallel.For(0, baseImageData.Stride * baseImageData.Height / 4, (i) =>
{
int k = i * 4;
unsafe
{
int color = Unsafe.Read<int>((baseImageData.Scan0 + k).ToPointer());
byte a = (byte)(color >> 24 & 0xff);
if (a == 0)
{
Unsafe.Write((resultImageData.Scan0 + k).ToPointer(), 0);
return;
}
var b = ColorExtensions.BlendValues((byte)(color >> 0 & 0xff), overlayColor.B, mode);
var g = ColorExtensions.BlendValues((byte)(color >> 8 & 0xff), overlayColor.G, mode);
var r = ColorExtensions.BlendValues((byte)(color >> 16 & 0xff), overlayColor.R, mode);
int blendedValue = a << 24 | r << 16 | g << 8 | b;
Unsafe.Write((resultImageData.Scan0 + k).ToPointer(), blendedValue);
}
});
bitmapResult.UnlockBits(resultImageData);
baseImage.UnlockBits(baseImageData);
return bitmapResult;
}
public static Image Blend(this Image image, Image overlay, BlendMode mode)
{
if (image is not Bitmap baseImage || overlay is not Bitmap overlayImage ||
image.Width != overlay.Width || image.Height != overlay.Height)
return image;
BitmapData baseImageData = baseImage.LockBits(new Rectangle(Point.Empty, baseImage.Size),
ImageLockMode.ReadWrite, PixelFormat.Format32bppArgb);
byte[] baseImageBuffer = new byte[baseImageData.Stride * baseImageData.Height];
Marshal.Copy(baseImageData.Scan0, baseImageBuffer, 0, baseImageBuffer.Length);
BitmapData overlayImageData = overlayImage.LockBits(new Rectangle(Point.Empty, overlayImage.Size),
ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb);
byte[] overlayImageBuffer = new byte[overlayImageData.Stride * overlayImageData.Height];
Marshal.Copy(overlayImageData.Scan0, overlayImageBuffer, 0, overlayImageBuffer.Length);
for (int k = 0; k < baseImageBuffer.Length && k < overlayImageBuffer.Length; k += 4)
{
baseImageBuffer[k + 0] = ColorExtensions.BlendValues(baseImageBuffer[k + 0], overlayImageBuffer[k + 0], mode);
baseImageBuffer[k + 1] = ColorExtensions.BlendValues(baseImageBuffer[k + 1], overlayImageBuffer[k + 1], mode);
baseImageBuffer[k + 2] = ColorExtensions.BlendValues(baseImageBuffer[k + 2], overlayImageBuffer[k + 2], mode);
}
Bitmap bitmapResult = new Bitmap(baseImage.Width, baseImage.Height, PixelFormat.Format32bppArgb);
BitmapData resultImageData = bitmapResult.LockBits(new Rectangle(Point.Empty, bitmapResult.Size),
ImageLockMode.WriteOnly, PixelFormat.Format32bppArgb);
Marshal.Copy(baseImageBuffer, 0, resultImageData.Scan0, baseImageBuffer.Length);
bitmapResult.UnlockBits(resultImageData);
baseImage.UnlockBits(baseImageData);
overlayImage.UnlockBits(overlayImageData);
return bitmapResult;
}
public static Image Interpolate(this Image image1, Image image2, double delta)
{
delta = MathExtensions.Clamp(delta, 0.0, 1.0);
if (image1 is not Bitmap baseImage || image2 is not Bitmap overlayImage ||
image1.Width != image2.Width || image1.Height != image2.Height)
return image1;
BitmapData baseImageData = baseImage.LockBits(new Rectangle(Point.Empty, baseImage.Size),
ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb);
byte[] baseImageBuffer = new byte[baseImageData.Stride * baseImageData.Height];
Marshal.Copy(baseImageData.Scan0, baseImageBuffer, 0, baseImageBuffer.Length);
baseImage.UnlockBits(baseImageData);
BitmapData overlayImageData = overlayImage.LockBits(new Rectangle(Point.Empty, overlayImage.Size),
ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb);
byte[] overlayImageBuffer = new byte[overlayImageData.Stride * overlayImageData.Height];
Marshal.Copy(overlayImageData.Scan0, overlayImageBuffer, 0, overlayImageBuffer.Length);
overlayImage.UnlockBits(overlayImageData);
byte[] finalBuffer = new byte[baseImageData.Stride * baseImageData.Height];
for (int k = 0; k < baseImageBuffer.Length && k < overlayImageBuffer.Length; k += 4)
{
finalBuffer[k + 0] = ColorExtensions.Mix(delta, baseImageBuffer[k + 0], overlayImageBuffer[k + 0]);
finalBuffer[k + 1] = ColorExtensions.Mix(delta, baseImageBuffer[k + 1], overlayImageBuffer[k + 1]);
finalBuffer[k + 2] = ColorExtensions.Mix(delta, baseImageBuffer[k + 2], overlayImageBuffer[k + 2]);
finalBuffer[k + 3] = ColorExtensions.Mix(delta, baseImageBuffer[k + 3], overlayImageBuffer[k + 3]);
}
Bitmap bitmapResult = new Bitmap(baseImage.Width, baseImage.Height, PixelFormat.Format32bppArgb);
BitmapData resultImageData = bitmapResult.LockBits(new Rectangle(Point.Empty, bitmapResult.Size),
ImageLockMode.WriteOnly, PixelFormat.Format32bppArgb);
Marshal.Copy(finalBuffer, 0, resultImageData.Scan0, finalBuffer.Length);
bitmapResult.UnlockBits(resultImageData);
return bitmapResult;
}
}
}

View File

@@ -0,0 +1,8 @@
namespace PckStudio.Core
{
public enum ImageLayoutDirection
{
Horizontal,
Vertical
}
}

View File

@@ -0,0 +1,37 @@
using System.Drawing;
namespace PckStudio.Core.Extensions
{
internal struct ImageSection
{
public readonly Size Size;
public readonly Point Point;
public readonly Rectangle Area;
internal ImageSection(Size originalSize, int index, ImageLayoutDirection layoutDirection)
{
switch(layoutDirection)
{
case ImageLayoutDirection.Horizontal:
{
Size = new Size(originalSize.Height, originalSize.Height);
Point = new Point(index * originalSize.Height, 0);
}
break;
case ImageLayoutDirection.Vertical:
{
Size = new Size(originalSize.Width, originalSize.Width);
Point = new Point(0, index * originalSize.Width);
}
break;
default:
Size = Size.Empty;
Point = new Point(-1, -1);
break;
}
Area = new Rectangle(Point, Size);
}
}
}

View File

@@ -0,0 +1,20 @@
using System.Collections.Generic;
namespace PckStudio.Core.Extensions
{
public static class ListExtensions
{
public static IList<T> Swap<T>(this IList<T> list, int index1, int index2)
{
T temp = list[index1];
list[index1] = list[index2];
list[index2] = temp;
return list;
}
public static bool IndexInRange<T>(this IList<T> list, int index)
{
return index >= 0 && index < list.Count;
}
}
}

View File

@@ -0,0 +1,17 @@
using OMI.Formats.Languages;
namespace PckStudio.Core.Extensions
{
public static class LocFileExtensions
{
public static void InitializeDefault(this LOCFile locFile, string packName) => locFile.Initialize("en-EN", ("IDS_DISPLAY_NAME", packName));
public static void Initialize(this LOCFile locFile, string language, params (string, string)[] locKeyValuePairs)
{
locFile.AddLanguage(language);
foreach ((string, string) locKeyValue in locKeyValuePairs)
locFile.AddLocKey(locKeyValue.Item1, locKeyValue.Item2);
}
}
}

View File

@@ -0,0 +1,37 @@
using OMI.Formats.Material;
namespace PckStudio.Core.Extensions
{
public static class MaterialContainerExtensions
{
private static readonly MaterialContainer.Material[] defaultMaterials = [
new MaterialContainer.Material("bat", "entity_alphatest"),
new MaterialContainer.Material("ender_dragon", "entity_emissive_alpha"),
new MaterialContainer.Material("blaze_head", "entity_emissive_alpha"),
new MaterialContainer.Material("drowned", "entity_emissive_alpha"),
new MaterialContainer.Material("enderman", "entity_emissive_alpha"),
new MaterialContainer.Material("enderman_invisible", "entity_emissive_alpha_only"),
new MaterialContainer.Material("ghast", "entity_emissive_alpha"),
new MaterialContainer.Material("guardian", "entity_alphatest"),
new MaterialContainer.Material("magma_cube", "entity_emissive_alpha"),
new MaterialContainer.Material("zombie_pigman", "entity"),
new MaterialContainer.Material("phantom", "entity_emissive_alpha"),
new MaterialContainer.Material("phantom_invisible", "entity_emissive_alpha_only"),
new MaterialContainer.Material("sheep", "entity_change_color"),
new MaterialContainer.Material("shulker", "entity_change_color"),
new MaterialContainer.Material("skeleton", "entity_alphatest"),
new MaterialContainer.Material("spider", "entity_emissive_alpha"),
new MaterialContainer.Material("spider_invisible", "entity_emissive_alpha_only"),
new MaterialContainer.Material("stray", "entity_alphatest"),
new MaterialContainer.Material("iron_golem", "entity_alphatest"),
new MaterialContainer.Material("wither_boss", "entity_alphatest"),
new MaterialContainer.Material("wither_skeleton", "entity_alphatest"),
new MaterialContainer.Material("wolf", "entity_alphatest_change_color")
];
public static void InitializeDefault(this MaterialContainer materials)
{
materials.AddRange(defaultMaterials);
}
}
}

View File

@@ -0,0 +1,17 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using OMI.Formats.Material;
namespace PckStudio.Core.Extensions
{
public static class MaterialExtensions
{
public static bool HasInvalidEntries(this MaterialContainer materials)
{
return materials.Any(mat => !MaterialContainer.SupportedEntities.Contains(mat.Name) || !MaterialContainer.ValidMaterialTypes.Contains(mat.Type));
}
}
}

View File

@@ -0,0 +1,16 @@
using System;
namespace PckStudio.Core.Extensions
{
public class MathExtensions
{
public static T Clamp<T>(T value, T min, T max) where T : IComparable<T>
{
if (value.CompareTo(min) < 0)
return min;
if (value.CompareTo(max) > 0)
return max;
return value;
}
}
}

View File

@@ -0,0 +1,20 @@
using System;
using OMI.Formats.Model;
using System.Numerics;
namespace PckStudio.Core.Extensions
{
public static class ModelBoxExtension
{
public static BoundingBox GetBoundingBox(this ModelBox modelBox)
{
Vector3 halfSize = modelBox.Size / 2f;
Vector3 halfSizeInflated = new Vector3(modelBox.Inflate) + halfSize;
Vector3 transformedCenter = modelBox.Position + halfSize;
Vector3 start = transformedCenter - halfSizeInflated;
Vector3 end = transformedCenter + halfSizeInflated;
return new BoundingBox(start, end);
}
}
}

View File

@@ -0,0 +1,25 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using OpenTK;
namespace PckStudio.Core.Extensions
{
public static class OpenTKExtensions
{
public static Matrix4 Pivoted(this Matrix4 rotation, Vector3 pivot)
{
var model = Matrix4.CreateTranslation(pivot);
model *= rotation;
model *= Matrix4.CreateTranslation(pivot).Inverted();
return model;
}
public static Vector3 Abs(Vector3 value)
{
return new Vector3(Math.Abs(value.X), Math.Abs(value.Y), Math.Abs(value.Z));
}
}
}

View File

@@ -0,0 +1,226 @@
using System;
using System.IO;
using System.Linq;
using System.Text;
using System.Drawing;
using System.Diagnostics;
using System.Collections.Generic;
using OMI.Formats.Languages;
using OMI.Formats.Pck;
using OMI.Workers;
using PckStudio.Interfaces;
using PckStudio.Core.Deserializer;
using PckStudio.Core.Serializer;
using PckStudio.Core.Skin;
namespace PckStudio.Core.Extensions
{
public static class PckAssetExtensions
{
private const string MipMap = "MipMapLevel";
public static PckAsset CreateNewAssetIf(this PckFile pck, bool condition, string filename, PckAssetType filetype, IDataFormatWriter writer)
{
if (condition)
{
return pck.CreateNewAsset(filename, filetype, writer);
}
return default;
}
public static PckAsset CreateNewAsset(this PckFile pck, string filename, PckAssetType filetype, IDataFormatWriter writer)
{
PckAsset asset = pck.CreateNewAsset(filename, filetype);
asset.SetData(writer);
return asset;
}
public static Image GetTexture(this PckAsset asset)
{
if (asset.Type != PckAssetType.SkinFile &&
asset.Type != PckAssetType.CapeFile &&
asset.Type != PckAssetType.TextureFile)
{
throw new Exception("Asset is not suitable to contain image data.");
}
return asset.GetDeserializedData(ImageDeserializer.DefaultDeserializer);
}
/// <summary>
/// Tries to get the skin id of the skin <paramref name="asset"/>
/// </summary>
/// <param name="asset"></param>
/// <returns>Non-zero base number on success, otherwise 0</returns>
/// <exception cref="InvalidOperationException"></exception>
public static int GetSkinId(this PckAsset asset)
{
if (asset.Type != PckAssetType.SkinFile)
throw new InvalidOperationException("Asset is not a skin.");
const string skinAssetnamePrefix = "dlcskin";
string assetPath = Path.GetFileNameWithoutExtension(asset.Filename);
if (!assetPath.StartsWith(skinAssetnamePrefix))
{
Trace.TraceWarning($"[{nameof(GetSkinId)}] Asset name does not start with '{skinAssetnamePrefix}'");
return 0;
}
int skinId = 0;
if (!int.TryParse(assetPath.Substring(skinAssetnamePrefix.Length), out skinId))
{
Trace.TraceWarning($"[{nameof(GetSkinId)}] Failed to parse Skin Id");
}
return skinId;
}
public static Skin.Skin GetSkin(this PckAsset asset)
{
if (asset.Type != PckAssetType.SkinFile)
throw new InvalidOperationException("Asset is not a skin.");
int skinId = asset.GetSkinId();
string name = asset.GetProperty("DISPLAYNAME");
Image texture = asset.GetTexture();
SkinANIM anim = asset.GetProperty("ANIM", SkinANIM.FromString);
IEnumerable<SkinBOX> boxes = asset.GetMultipleProperties("BOX").Select(kv => SkinBOX.FromString(kv.Value));
IEnumerable<SkinPartOffset> offsets = asset.GetMultipleProperties("OFFSET").Select(kv => SkinPartOffset.FromString(kv.Value));
return new Skin.Skin(name, skinId, texture, anim, boxes, offsets);
}
public static void SetSkin(this PckAsset asset, Skin.Skin skin, LOCFile localizationFile)
{
if (asset.Type != PckAssetType.SkinFile)
throw new InvalidOperationException("Asset is not a skin file");
asset.SetTexture(skin.Texture);
string skinId = skin.Identifier.ToString("d08");
// TODO: keep filepath
asset.Filename = $"dlcskin{skinId}.png";
asset.SetProperty("DISPLAYNAME", skin.MetaData.Name);
if (localizationFile is not null)
{
string skinLocKey = $"IDS_dlcskin{skinId}_DISPLAYNAME";
asset.SetProperty("DISPLAYNAMEID", skinLocKey);
localizationFile.SetLocEntry(skinLocKey, skin.MetaData.Name);
}
if (!string.IsNullOrEmpty(skin.MetaData.Theme))
{
asset.SetProperty("THEMENAME", skin.MetaData.Theme);
if (localizationFile is not null)
{
string skinThemeLocKey = $"IDS_dlcskin{skinId}_THEMENAME";
asset.SetProperty("THEMENAMEID", skinThemeLocKey);
localizationFile.SetLocEntry(skinThemeLocKey, skin.MetaData.Theme);
}
}
if (skin.HasCape)
{
asset.SetProperty("CAPEPATH", $"dlccape{skinId}.png");
}
asset.SetProperty("ANIM", skin.Anim.ToString());
asset.SetProperty("GAME_FLAGS", "0x18");
asset.SetProperty("FREE", "1");
asset.RemoveProperties("BOX");
asset.RemoveProperties("OFFSET");
foreach (SkinBOX box in skin.Model.AdditionalBoxes)
{
asset.AddProperty(box.ToProperty());
}
foreach (SkinPartOffset offset in skin.Model.PartOffsets)
{
asset.AddProperty(offset.ToProperty());
}
}
public static T GetDeserializedData<T>(this PckAsset asset, IPckAssetDeserializer<T> deserializer)
{
return deserializer.Deserialize(asset);
}
public static T GetData<T>(this PckAsset asset, IDataFormatReader<T> formatReader) where T : class
{
using var ms = new MemoryStream(asset.Data);
return formatReader.FromStream(ms);
}
public static void SetSerializedData<T>(this PckAsset asset, T obj, IPckAssetSerializer<T> serializer)
{
serializer.Serialize(obj, ref asset);
}
public static void SetData(this PckAsset asset, IDataFormatWriter formatWriter)
{
using (var stream = new MemoryStream())
{
formatWriter.WriteToStream(stream);
asset.SetData(stream.ToArray());
}
}
public static void SetTexture(this PckAsset asset, Image image)
{
if (asset.Type != PckAssetType.SkinFile &&
asset.Type != PckAssetType.CapeFile &&
asset.Type != PckAssetType.TextureFile)
{
throw new Exception("Asset is not suitable to contain image data.");
}
asset.SetSerializedData(image, ImageSerializer.DefaultSerializer);
}
public static bool IsMipmappedFile(this PckAsset asset)
{
// We only want to test the file name itself. ex: "terrainMipMapLevel2"
string name = Path.GetFileNameWithoutExtension(asset.Filename);
// check if last character is a digit (0-9). If not return false
if (!char.IsDigit(name[name.Length - 1]))
return false;
// If string does not end with MipMapLevel, then it's not MipMapped
if (!name.Remove(name.Length - 1, 1).EndsWith(MipMap))
return false;
return true;
}
public static string GetNormalPath(this PckAsset asset)
{
if (!asset.IsMipmappedFile())
return asset.Filename;
string ext = Path.GetExtension(asset.Filename);
return asset.Filename.Remove(asset.Filename.Length - (MipMap.Length + 1) - ext.Length) + ext;
}
public static void DeserializeProperties(this PckAsset asset, IEnumerable<string> serializedData)
{
IEnumerable<KeyValuePair<string, string>> lines = serializedData
.Select(line => line.Split([' '], 2))
.Where (keyValue => keyValue.Length == 2)
.Select(keyValue => new KeyValuePair<string, string>(keyValue[0].Replace(":", ""), keyValue[1]));
foreach (KeyValuePair<string, string> kv in lines)
{
asset.AddProperty(kv);
}
}
public static IEnumerable<string> SerializeProperties(this PckAsset asset, string seperater = ":")
{
IReadOnlyList<KeyValuePair<string, string>> properties = asset.GetProperties();
return properties.Select(property => property.Key + seperater + property.Value);
}
}
}

View File

@@ -0,0 +1,23 @@
using System;
using System.Reflection;
using System.Windows.Forms;
namespace PckStudio.Core.Extensions
{
public static class PictureBoxExtensions
{
public static bool IsAnimating(this PictureBox pictureBox)
{
FieldInfo fi = typeof(PictureBox).GetField("currentlyAnimating", BindingFlags.NonPublic | BindingFlags.Instance);
return (bool)fi.GetValue(pictureBox);
}
public static void Animate(this PictureBox pictureBox, bool animate)
{
MethodInfo animateMethod = typeof(PictureBox).GetMethod("Animate", BindingFlags.NonPublic | BindingFlags.Instance,
null, new Type[] { typeof(bool) }, null);
animateMethod.Invoke(pictureBox, new object[] { animate });
}
}
}

View File

@@ -0,0 +1,81 @@
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Linq;
using System.Numerics;
using System.Text;
using System.Threading.Tasks;
using PckStudio.Core.Skin;
namespace PckStudio.Core.Extensions
{
public static class SkinBOXExtensions
{
public static GraphicsPath GetUVGraphicsPath(this SkinBOX skinBOX, Vector2 tillingFactor)
{
var types = new byte[9];
var points = new PointF[9];
var path = new GraphicsPath(FillMode.Winding);
Vector2 uv = skinBOX.UV;
Vector3 size = skinBOX.Size;
path.AddRectangle(new RectangleF(new PointF((uv.X ) * tillingFactor.X, (uv.Y + size.Z) * tillingFactor.Y), new SizeF(size.Z * tillingFactor.X, size.Y * tillingFactor.Y)));
path.AddRectangle(new RectangleF(new PointF((uv.X + size.Z ) * tillingFactor.X, (uv.Y + size.Z) * tillingFactor.Y), new SizeF(size.X * tillingFactor.X, size.Y * tillingFactor.Y)));
path.AddRectangle(new RectangleF(new PointF((uv.X + size.Z + size.X ) * tillingFactor.X, (uv.Y + size.Z) * tillingFactor.Y), new SizeF(size.Z * tillingFactor.X, size.Y * tillingFactor.Y)));
path.AddRectangle(new RectangleF(new PointF((uv.X + size.Z * 2 + size.X) * tillingFactor.X, (uv.Y + size.Z) * tillingFactor.Y), new SizeF(size.X * tillingFactor.X, size.Y * tillingFactor.Y)));
path.AddRectangle(new RectangleF(new PointF((uv.X + size.Z ) * tillingFactor.X, (uv.Y ) * tillingFactor.Y), new SizeF(size.X * tillingFactor.X, size.Z * tillingFactor.Y)));
path.AddRectangle(new RectangleF(new PointF((uv.X + size.Z + size.X ) * tillingFactor.X, (uv.Y ) * tillingFactor.Y), new SizeF(size.X * tillingFactor.X, size.Z * tillingFactor.Y)));
return path;
}
public static GraphicsPath GetUVGraphicsPath(this SkinBOX skinBox)
{
return skinBox.GetUVGraphicsPath(Vector2.One);
}
public static string GetOverlayType(this SkinBOX skinBox)
{
if (!skinBox.IsValidType())
return "";
if (skinBox.IsOverlayPart())
return skinBox.Type;
int index = Array.IndexOf(SkinBOX.BaseTypes, skinBox.Type);
return SkinBOX.OverlayTypes.IndexInRange(index) ? SkinBOX.OverlayTypes[index] : "";
}
public static string GetOverlayType(string type)
{
if (!SkinBOX.IsValidType(type))
return "";
if (SkinBOX.IsOverlayPart(type))
return type;
int index = Array.IndexOf(SkinBOX.BaseTypes, type);
return SkinBOX.OverlayTypes.IndexInRange(index) ? SkinBOX.OverlayTypes[index] : "";
}
public static string GetBaseType(this SkinBOX skinBox)
{
if (!skinBox.IsValidType())
return "";
if (skinBox.IsBasePart())
return skinBox.Type;
int index = Array.IndexOf(SkinBOX.OverlayTypes, skinBox.Type);
return SkinBOX.BaseTypes.IndexInRange(index) ? SkinBOX.BaseTypes[index] : "";
}
public static string GetBaseType(string type)
{
if (!SkinBOX.IsValidType(type))
return "";
if (SkinBOX.IsBasePart(type))
return type;
int index = Array.IndexOf(SkinBOX.OverlayTypes, type);
return SkinBOX.BaseTypes.IndexInRange(index) ? SkinBOX.BaseTypes[index] : "";
}
}
}

View File

@@ -0,0 +1,71 @@
using System;
using System.Collections.Generic;
using System.Drawing.Imaging;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using OMI.Formats.Languages;
using OMI.Formats.Pck;
using PckStudio.Core.Skin;
namespace PckStudio.Core.Extensions
{
public static class SkinExtensions
{
public static PckAsset CreateFile(this Skin.Skin skin, LOCFile localizationFile)
{
string skinId = skin.Identifier.ToString("d08");
PckAsset skinFile = new PckAsset($"dlcskin{skinId}.png", PckAssetType.SkinFile);
skinFile.AddProperty("DISPLAYNAME", skin.MetaData.Name);
if (localizationFile is not null)
{
string skinLocKey = $"IDS_dlcskin{skinId}_DISPLAYNAME";
skinFile.AddProperty("DISPLAYNAMEID", skinLocKey);
localizationFile.AddLocKey(skinLocKey, skin.MetaData.Name);
}
if (!string.IsNullOrEmpty(skin.MetaData.Theme))
{
skinFile.AddProperty("THEMENAME", skin.MetaData.Theme);
if (localizationFile is not null)
{
skinFile.AddProperty("THEMENAMEID", $"IDS_dlcskin{skinId}_THEMENAME");
localizationFile.AddLocKey($"IDS_dlcskin{skinId}_THEMENAME", skin.MetaData.Theme);
}
}
if (skin.HasCape)
{
skinFile.AddProperty("CAPEPATH", $"dlccape{skinId}.png");
}
skinFile.AddProperty("ANIM", skin.Anim);
skinFile.AddProperty("GAME_FLAGS", "0x18");
skinFile.AddProperty("FREE", "1");
foreach (SkinBOX box in skin.Model.AdditionalBoxes)
{
skinFile.AddProperty(box.ToProperty());
}
foreach (SkinPartOffset offset in skin.Model.PartOffsets)
{
skinFile.AddProperty(offset.ToProperty());
}
skinFile.SetTexture(skin.Texture);
return skinFile;
}
public static PckAsset CreateCapeFile(this Skin.Skin skin)
{
if (!skin.HasCape)
throw new InvalidOperationException("Skin does not contain a cape.");
string skinId = skin.Identifier.ToString("d08");
PckAsset capeFile = new PckAsset($"dlccape{skinId}.png", PckAssetType.CapeFile);
capeFile.SetTexture(skin.CapeTexture);
return capeFile;
}
}
}

View File

@@ -0,0 +1,29 @@
namespace PckStudio.Core.Extensions
{
public static class NumericsExtensions
{
//internal static Cube ToCube(this SkinBOX skinBOX) => skinBOX.ToCube(0f);
//internal static Cube ToCube(this SkinBOX skinBOX, float inflate, bool flipZMapping = false)
// => new Cube(skinBOX.Pos.ToOpenTKVector(), skinBOX.Size.ToOpenTKVector(), skinBOX.UV.ToOpenTKVector(), skinBOX.Scale + inflate, skinBOX.Mirror, flipZMapping);
public static OpenTK.Vector3 ToOpenTKVector(this System.Numerics.Vector3 vector3)
{
return new OpenTK.Vector3(vector3.X, vector3.Y, vector3.Z);
}
public static OpenTK.Vector2 ToOpenTKVector(this System.Numerics.Vector2 vector2)
{
return new OpenTK.Vector2(vector2.X, vector2.Y);
}
public static System.Numerics.Vector3 ToNumericsVector(this OpenTK.Vector3 vector3)
{
return new System.Numerics.Vector3(vector3.X, vector3.Y, vector3.Z);
}
public static System.Numerics.Vector2 ToNumericsVector(this OpenTK.Vector2 vector2)
{
return new System.Numerics.Vector2(vector2.X, vector2.Y);
}
}
}

View File

@@ -0,0 +1,47 @@
using System.Collections.Generic;
using System.Windows.Forms;
namespace PckStudio.Core.Extensions
{
public static class TreeNodeExtensions
{
public static bool IsTagOfType<T>(this TreeNode node) where T : class
{
return node?.Tag is T;
}
public static bool TryGetTagData<TOut>(this TreeNode node, out TOut tagData) where TOut : class
{
if (node?.Tag is TOut data)
{
tagData = data;
return true;
}
tagData = default;
return false;
}
public static bool Contains(this TreeNode thisNode, TreeNode childNode)
{
if (childNode.Parent == null)
return false;
if (thisNode.Equals(childNode.Parent))
return true;
// If the parent node is not equal to the first node,
// call the TreeNode.Contains recursively using the parent of the node.
return thisNode.Contains(childNode.Parent);
}
public static List<TreeNode> GetChildNodes(this TreeNode thisNode)
{
List<TreeNode> nodes = new List<TreeNode>(thisNode.Nodes.Count);
foreach (TreeNode node in thisNode.Nodes)
{
nodes.Add(node);
nodes.AddRange(node.GetChildNodes());
}
return nodes;
}
}
}

View File

@@ -0,0 +1,28 @@
using System;
using System.Linq;
using System.Windows.Forms;
namespace PckStudio.Core.Extensions
{
public static class TreeViewExtensions
{
public static TreeNode[] FindPath(this TreeView treeView, string path)
{
if (string.IsNullOrWhiteSpace(path))
return Array.Empty<TreeNode>();
if (!path.Contains(treeView.PathSeparator))
{
return treeView.Nodes.Find(path, false);
}
string segment = path.Substring(0, path.IndexOf(treeView.PathSeparator));
if (treeView.Nodes.ContainsKey(segment))
{
TreeNode[] res = treeView.Nodes[segment].GetChildNodes().Where(node => node.FullPath == path).ToArray();
return res;
}
return Array.Empty<TreeNode>();
}
}
}

View File

@@ -0,0 +1,23 @@
using System.IO;
namespace PckStudio.Core
{
public readonly struct FileDialogFilter
{
public readonly string Description;
public readonly string Pattern;
public string Extension => Path.GetExtension(Pattern);
public FileDialogFilter(string description, string pattern)
{
Description = description;
Pattern = pattern;
}
public override string ToString()
{
return $"{Description}|{Pattern}";
}
}
}

View File

@@ -0,0 +1,157 @@
using System;
using System.Collections.Generic;
using System.Linq;
using OMI.Formats.Languages;
namespace PckStudio.Core.FileFormats
{
public class PckAudioFile
{
public class InvalidCategoryException : Exception
{
public InvalidCategoryException(string message) : base(message)
{ }
}
public readonly int type = 1;
public AudioCategory[] Categories => Array.FindAll(_categories, c => c is not null);
private AudioCategory[] _categories { get; } = new AudioCategory[9];
public Dictionary<string, string> Credits { get; } = new Dictionary<string, string>();
public class AudioCategory
{
public enum EAudioType : int
{
Overworld,
Nether,
End,
Creative,
Menu,
Battle,
Tumble,
Glide,
BuildOff,
}
public enum EAudioParameterType : int
{
unk0, // dimension music
unk1, // unused music ?
}
public string Name { get; set; } = string.Empty;
public EAudioType AudioType { get; }
public List<string> SongNames { get; } = new List<string>();
public EAudioParameterType parameterType { get; }
public AudioCategory(string name, EAudioParameterType parameterType, EAudioType audioType)
{
this.Name = name;
this.parameterType = parameterType;
this.AudioType = audioType;
}
}
public string[] GetCredits() => Credits.Values.ToArray();
public string GetCreditsString() => string.Join("\n", Credits.Values.ToArray());
public void AddCredits(params string[] credits)
{
foreach (var credit in credits)
{
AddCredit(credit);
}
}
/// <summary>
/// Applies internal Credits to loc file
/// </summary>
public void ApplyCredits(LOCFile locFile)
{
foreach (KeyValuePair<string, string> credit in Credits)
{
locFile.SetLocEntry(credit.Key, credit.Value);
}
}
/// <summary>
/// Clears and sets the new supplied <paramref name="credits"/>
/// </summary>
public void SetCredits(params string[] credits)
{
Credits.Clear();
AddCredits(credits);
}
public bool SetCredit(string creditId, string s)
{
if (!Credits.ContainsKey(creditId))
return false;
Credits[creditId] = s;
return true;
}
public void AddCredit(string credit)
{
Credits.Add($"IDS_CREDIT{(Credits.Count > 0 ? $"_{Credits.Count+1}" : string.Empty)}", credit);
}
public void AddCreditId(string creditId) => Credits.Add(creditId, string.Empty);
/// <exception cref="InvalidCategoryException"></exception>
public bool HasCategory(AudioCategory.EAudioType category) => GetCategory(category) is AudioCategory;
/// <exception cref="InvalidCategoryException"></exception>
public AudioCategory GetCategory(AudioCategory.EAudioType category)
{
if (category < AudioCategory.EAudioType.Overworld ||
category > AudioCategory.EAudioType.BuildOff)
throw new InvalidCategoryException(nameof(category));
return _categories[(int)category];
}
/// <exception cref="InvalidCategoryException"></exception>
public bool TryGetCategory(AudioCategory.EAudioType category, out AudioCategory audioCategory)
{
if (GetCategory(category) is AudioCategory a)
{
audioCategory = a;
return true;
}
audioCategory = null;
return false;
}
/// <returns>True when category was created, otherwise false</returns>
/// <exception cref="InvalidCategoryException"></exception>
public bool AddCategory(AudioCategory.EAudioParameterType parameterType, AudioCategory.EAudioType category, string name = "")
{
if (category < AudioCategory.EAudioType.Overworld ||
category > AudioCategory.EAudioType.BuildOff)
throw new InvalidCategoryException(nameof(category));
bool exists = HasCategory(category);
if (!exists)
_categories[(int)category] = new AudioCategory(name, parameterType, category);
return !exists;
}
/// <returns>True when category was created, otherwise false</returns>
/// <exception cref="InvalidCategoryException"></exception>
public bool AddCategory(AudioCategory.EAudioType category)
=> AddCategory(AudioCategory.EAudioParameterType.unk0, category);
/// <returns>True when category was removed, otherwise false</returns>
/// <exception cref="InvalidCategoryException"></exception>
public bool RemoveCategory(AudioCategory.EAudioType category)
{
bool exists = HasCategory(category);
if (exists)
_categories[(int)category] = null;
return exists;
}
}
}

View File

@@ -0,0 +1,78 @@
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Numerics;
using System.Text;
using System.Threading.Tasks;
using PckStudio.Core.Extensions;
using PckStudio.Core.Skin;
namespace PckStudio.Core
{
public static class GameConstants
{
public static readonly Vector3 SkinHeadTranslation = Vector3.Zero;
public static readonly Vector3 SkinHeadPivot = Vector3.Zero;
public static readonly Vector3 SkinBodyTranslation = Vector3.Zero;
public static readonly Vector3 SkinBodyPivot = Vector3.Zero;
public static readonly Vector3 SkinRightArmTranslation = new(-5f, 2f, 0f);
public static readonly Vector3 SkinRightArmPivot = new(-6f, 2f, 0f);
public static readonly Vector3 SkinLeftArmTranslation = new(5f, 2f, 0f);
public static readonly Vector3 SkinLeftArmPivot = new(6f, 2f, 0f);
public static readonly Vector3 SkinRightLegTranslation = new(-2f, 12f, 0f);
public static readonly Vector3 SkinRightLegPivot = new(-2f, 12f, 0f);
public static readonly Vector3 SkinLeftLegTranslation = new(2f, 12f, 0f);
public static readonly Vector3 SkinLeftLegPivot = new(2f, 12f, 0f);
private static Dictionary<string, PositioningInfo> _posisioningInfos = new Dictionary<string, PositioningInfo>()
{
["HEAD"] = new PositioningInfo(SkinHeadTranslation, SkinHeadPivot),
["BODY"] = new PositioningInfo(SkinBodyTranslation, SkinBodyPivot),
["ARM0"] = new PositioningInfo(SkinRightArmTranslation, SkinRightArmPivot),
["ARM1"] = new PositioningInfo(SkinLeftArmTranslation, SkinLeftArmPivot),
["LEG0"] = new PositioningInfo(SkinRightLegTranslation, SkinRightLegPivot),
["LEG1"] = new PositioningInfo(SkinLeftLegTranslation, SkinLeftLegPivot),
};
public record struct PositioningInfo(Vector3 Translation, Vector3 Pivot);
public static PositioningInfo GetPositioningInfo(string partName)
{
if (SkinBOX.IsOverlayPart(partName))
partName = SkinBOXExtensions.GetBaseType(partName);
return _posisioningInfos.ContainsKey(partName) ? _posisioningInfos[partName] : default;
}
public static Vector3 GetSkinPartPivot(string partName) => GetPositioningInfo(partName).Pivot;
public static Vector3 GetSkinPartTranslation(string partName) => GetPositioningInfo(partName).Translation;
public const int GameTickInMilliseconds = 50;
// See: https://minecraft.fandom.com/wiki/Dye#Color_values for more information.
public static readonly Color[] DyeColors = [
Color.FromArgb(0xf9fffe), // White
Color.FromArgb(0xf9801d), // Orange
Color.FromArgb(0xc74ebd), // Magenta
Color.FromArgb(0x3ab3da), // Light Blue
Color.FromArgb(0xfed83d), // Yellow
Color.FromArgb(0x80c71f), // Lime
Color.FromArgb(0xf38baa), // Pink
Color.FromArgb(0x474f52), // Gray
Color.FromArgb(0x9d9d97), // Light Gray
Color.FromArgb(0x169c9c), // Cyan
Color.FromArgb(0x8932b8), // Purple
Color.FromArgb(0x3c44aa), // Blue
Color.FromArgb(0x835432), // Brown
Color.FromArgb(0x5e7c16), // Green
Color.FromArgb(0xb02e26), // Red
Color.FromArgb(0x1d1d21), // Black
];
}
}

View File

@@ -0,0 +1,93 @@
using System;
using System.Collections.Generic;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using OMI.Workers;
using OMI;
namespace PckStudio.Core.IO._3DST
{
public class _3DSTextureReader : IDataFormatReader<Image>, IDataFormatReader
{
public Image FromFile(string filename)
{
if (File.Exists(filename))
{
Image img = null;
using (FileStream fs = File.OpenRead(filename))
{
img = FromStream(fs);
}
return img;
}
throw new FileNotFoundException(filename);
}
public Image FromStream(Stream stream)
{
Image img = null;
using (var reader = new EndiannessAwareBinaryReader(stream, Encoding.ASCII, leaveOpen: true, Endianness.LittleEndian))
{
if (reader.ReadString(4) == "3DST")
{
const int offset = 32;
reader.ReadInt32(); // unknown value
_3DSTextureFormat format = reader.ReadInt32() switch
{
0 => _3DSTextureFormat.argb8,
1 => _3DSTextureFormat.rgb8,
2 => _3DSTextureFormat.rgba5551,
3 => _3DSTextureFormat.rgb8,
4 => _3DSTextureFormat.rgba4,
9 => _3DSTextureFormat.la4,
_ => _3DSTextureFormat.dontCare,
};
int width = reader.ReadInt32();
int height = reader.ReadInt32();
int bufferSize = CalcBufferSize(format, width, height);
stream.Seek(offset, SeekOrigin.Begin);
byte[] buffer = new byte[bufferSize];
reader.Read(buffer, 0, bufferSize);
img = TextureCodec.Decode(buffer, width, height, format);
img.RotateFlip(RotateFlipType.RotateNoneFlipY);
}
}
return img;
}
private static int CalcBufferSize(_3DSTextureFormat textureFormat, int width, int height)
{
switch (textureFormat)
{
case _3DSTextureFormat.argb8:
return width * height * 4;
case _3DSTextureFormat.rgb8:
return width * height * 3;
case _3DSTextureFormat.rgba5551:
case _3DSTextureFormat.rgb565:
case _3DSTextureFormat.rgba4:
case _3DSTextureFormat.la8:
case _3DSTextureFormat.hilo8:
return width * height * 2;
case _3DSTextureFormat.l8:
case _3DSTextureFormat.a8:
case _3DSTextureFormat.la4:
case _3DSTextureFormat.etc1a4:
return width * height;
case _3DSTextureFormat.l4:
case _3DSTextureFormat.a4:
case _3DSTextureFormat.etc1:
return width * height >> 1;
default:
throw new InvalidDataException("Invalid texture format on BCH!");
}
}
object IDataFormatReader.FromFile(string filename) => FromFile(filename);
object IDataFormatReader.FromStream(Stream stream) => FromStream(stream);
}
}

View File

@@ -0,0 +1,46 @@
using System;
using System.Drawing;
using System.IO;
using System.Text;
using OMI;
using OMI.Workers;
namespace PckStudio.Core.IO._3DST
{
public class _3DSTextureWriter : IDataFormatWriter
{
private Image _image;
private _3DSTextureFormat _format;
public _3DSTextureWriter(Image image, _3DSTextureFormat format = _3DSTextureFormat.argb8)
{
_image = image;
_format = format;
}
public void WriteToFile(string filename)
{
using(FileStream fs = File.OpenWrite(filename))
{
WriteToStream(fs);
}
}
public void WriteToStream(Stream stream)
{
using (var writer = new EndiannessAwareBinaryWriter(stream, Encoding.ASCII, leaveOpen: true, Endianness.LittleEndian))
{
writer.WriteString("3DST"); // 0
writer.Write(2); // 4 unknown
writer.Write((int)_format); // 8
writer.Write(_image.Width); // 12
writer.Write(_image.Height); // 16
writer.Write(0); // 20
writer.Write(0); // 24
writer.Write(0); // 28
_image.RotateFlip(RotateFlipType.RotateNoneFlipY);
byte[] buffer = TextureCodec.Encode(new Bitmap(_image), _format);
stream.Write(buffer, 0, buffer.Length);
}
}
}
}

View File

@@ -0,0 +1,636 @@
using System;
using System.Drawing;
using System.Drawing.Imaging;
using System.Runtime.InteropServices;
namespace PckStudio.Core.IO._3DST
{
/// <summary>
/// Format of the texture used on the PICA200.
/// </summary>
public enum _3DSTextureFormat
{
argb8 = 0,
rgb8 = 1,
rgba5551 = 2,
rgb565 = 3,
rgba4 = 4,
la8 = 5,
hilo8 = 6,
l8 = 7,
a8 = 8,
la4 = 9,
l4 = 0xa,
a4 = 0xb,
etc1 = 0xc,
etc1a4 = 0xd,
dontCare
}
class TextureCodec
{
private static readonly int[] tileOrder = { 0, 1, 8, 9, 2, 3, 10, 11, 16, 17, 24, 25, 18, 19, 26, 27, 4, 5, 12, 13, 6, 7, 14, 15, 20, 21, 28, 29, 22, 23, 30, 31, 32, 33, 40, 41, 34, 35, 42, 43, 48, 49, 56, 57, 50, 51, 58, 59, 36, 37, 44, 45, 38, 39, 46, 47, 52, 53, 60, 61, 54, 55, 62, 63 };
private static readonly int[,] etc1LUT = { { 2, 8, -2, -8 }, { 5, 17, -5, -17 }, { 9, 29, -9, -29 }, { 13, 42, -13, -42 }, { 18, 60, -18, -60 }, { 24, 80, -24, -80 }, { 33, 106, -33, -106 }, { 47, 183, -47, -183 } };
private static class TextureConverter
{
/// <summary>
/// Gets a Bitmap from a RGBA8 Texture buffer.
/// </summary>
/// <param name="array">The Buffer</param>
/// <param name="width">Width of the Texture</param>
/// <param name="height">Height of the Texture</param>
/// <returns></returns>
public static Bitmap ToBitmap(byte[] array, int width, int height)
{
Bitmap img = new Bitmap(width, height, PixelFormat.Format32bppArgb);
BitmapData imgData = img.LockBits(new Rectangle(0, 0, img.Width, img.Height), ImageLockMode.WriteOnly, PixelFormat.Format32bppArgb);
Marshal.Copy(array, 0, imgData.Scan0, array.Length);
img.UnlockBits(imgData);
return img;
}
/// <summary>
/// Gets a RGBA8 Texture Buffer from a Bitmap.
/// </summary>
/// <param name="img">The Bitmap</param>
/// <returns></returns>
public static byte[] ToArray(Bitmap img)
{
BitmapData imgData = img.LockBits(new Rectangle(0, 0, img.Width, img.Height), ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb);
byte[] array = new byte[imgData.Stride * img.Height];
Marshal.Copy(imgData.Scan0, array, 0, array.Length);
img.UnlockBits(imgData);
return array;
}
}
/// <summary>
/// Decodes a PICA200 Texture.
/// </summary>
/// <param name="data">Buffer with the Texture</param>
/// <param name="width">Width of the Texture</param>
/// <param name="height">Height of the Texture</param>
/// <param name="format">Pixel Format of the Texture</param>
/// <returns></returns>
public static Bitmap Decode(byte[] data, int width, int height, _3DSTextureFormat format)
{
byte[] output = new byte[width * height * 4];
int dataOffset = 0;
bool toggle = false;
switch (format)
{
case _3DSTextureFormat.argb8:
for (int tY = 0; tY < height / 8; tY++)
{
for (int tX = 0; tX < width / 8; tX++)
{
for (int pixel = 0; pixel < 64; pixel++)
{
int x = tileOrder[pixel] % 8;
int y = (tileOrder[pixel] - x) / 8;
int outputOffset = ((tX * 8 + x) + ((tY * 8 + y) * width)) * 4;
Buffer.BlockCopy(data, dataOffset + 1, output, outputOffset, 3);
output[outputOffset + 3] = data[dataOffset];
dataOffset += 4;
}
}
}
break;
case _3DSTextureFormat.rgb8:
for (int tY = 0; tY < height / 8; tY++)
{
for (int tX = 0; tX < width / 8; tX++)
{
for (int pixel = 0; pixel < 64; pixel++)
{
int x = tileOrder[pixel] % 8;
int y = (tileOrder[pixel] - x) / 8;
long outputOffset = ((tX * 8) + x + ((tY * 8 + y) * width)) * 4;
Buffer.BlockCopy(data, dataOffset, output, (int)outputOffset, 3);
output[outputOffset + 3] = 0xff;
dataOffset += 3;
}
}
}
break;
case _3DSTextureFormat.rgba5551:
for (int tY = 0; tY < height / 8; tY++)
{
for (int tX = 0; tX < width / 8; tX++)
{
for (int pixel = 0; pixel < 64; pixel++)
{
int x = tileOrder[pixel] % 8;
int y = (tileOrder[pixel] - x) / 8;
long outputOffset = ((tX * 8) + x + (((tY * 8 + y)) * width)) * 4;
ushort pixelData = (ushort)(data[dataOffset] | (data[dataOffset + 1] << 8));
byte r = (byte)(((pixelData >> 1) & 0x1f) << 3);
byte g = (byte)(((pixelData >> 6) & 0x1f) << 3);
byte b = (byte)(((pixelData >> 11) & 0x1f) << 3);
byte a = (byte)((pixelData & 1) * 0xff);
output[outputOffset] = (byte)(r | (r >> 5));
output[outputOffset + 1] = (byte)(g | (g >> 5));
output[outputOffset + 2] = (byte)(b | (b >> 5));
output[outputOffset + 3] = a;
dataOffset += 2;
}
}
}
break;
case _3DSTextureFormat.rgb565:
for (int tY = 0; tY < height / 8; tY++)
{
for (int tX = 0; tX < width / 8; tX++)
{
for (int pixel = 0; pixel < 64; pixel++)
{
int x = tileOrder[pixel] % 8;
int y = (tileOrder[pixel] - x) / 8;
long outputOffset = ((tX * 8) + x + (((tY * 8 + y)) * width)) * 4;
ushort pixelData = (ushort)(data[dataOffset] | (data[dataOffset + 1] << 8));
byte r = (byte)((pixelData & 0x1f) << 3);
byte g = (byte)(((pixelData >> 5) & 0x3f) << 2);
byte b = (byte)(((pixelData >> 11) & 0x1f) << 3);
output[outputOffset] = (byte)(r | (r >> 5));
output[outputOffset + 1] = (byte)(g | (g >> 6));
output[outputOffset + 2] = (byte)(b | (b >> 5));
output[outputOffset + 3] = 0xff;
dataOffset += 2;
}
}
}
break;
case _3DSTextureFormat.rgba4:
for (int tY = 0; tY < height / 8; tY++)
{
for (int tX = 0; tX < width / 8; tX++)
{
for (int pixel = 0; pixel < 64; pixel++)
{
int x = tileOrder[pixel] % 8;
int y = (tileOrder[pixel] - x) / 8;
long outputOffset = ((tX * 8) + x + (((tY * 8 + y)) * width)) * 4;
ushort pixelData = (ushort)(data[dataOffset] | (data[dataOffset + 1] << 8));
byte r = (byte)((pixelData >> 4) & 0xf);
byte g = (byte)((pixelData >> 8) & 0xf);
byte b = (byte)((pixelData >> 12) & 0xf);
byte a = (byte)(pixelData & 0xf);
output[outputOffset] = (byte)(r | (r << 4));
output[outputOffset + 1] = (byte)(g | (g << 4));
output[outputOffset + 2] = (byte)(b | (b << 4));
output[outputOffset + 3] = (byte)(a | (a << 4));
dataOffset += 2;
}
}
}
break;
case _3DSTextureFormat.la8:
case _3DSTextureFormat.hilo8:
for (int tY = 0; tY < height / 8; tY++)
{
for (int tX = 0; tX < width / 8; tX++)
{
for (int pixel = 0; pixel < 64; pixel++)
{
int x = tileOrder[pixel] % 8;
int y = (tileOrder[pixel] - x) / 8;
long outputOffset = ((tX * 8) + x + (((tY * 8 + y)) * width)) * 4;
output[outputOffset] = data[dataOffset];
output[outputOffset + 1] = data[dataOffset];
output[outputOffset + 2] = data[dataOffset];
output[outputOffset + 3] = data[dataOffset + 1];
dataOffset += 2;
}
}
}
break;
case _3DSTextureFormat.l8:
for (int tY = 0; tY < height / 8; tY++)
{
for (int tX = 0; tX < width / 8; tX++)
{
for (int pixel = 0; pixel < 64; pixel++)
{
int x = tileOrder[pixel] % 8;
int y = (tileOrder[pixel] - x) / 8;
long outputOffset = ((tX * 8) + x + (((tY * 8 + y)) * width)) * 4;
output[outputOffset] = data[dataOffset];
output[outputOffset + 1] = data[dataOffset];
output[outputOffset + 2] = data[dataOffset];
output[outputOffset + 3] = 0xff;
dataOffset++;
}
}
}
break;
case _3DSTextureFormat.a8:
for (int tY = 0; tY < height / 8; tY++)
{
for (int tX = 0; tX < width / 8; tX++)
{
for (int pixel = 0; pixel < 64; pixel++)
{
int x = tileOrder[pixel] % 8;
int y = (tileOrder[pixel] - x) / 8;
long outputOffset = ((tX * 8) + x + (((tY * 8 + y)) * width)) * 4;
output[outputOffset] = 0xff;
output[outputOffset + 1] = 0xff;
output[outputOffset + 2] = 0xff;
output[outputOffset + 3] = data[dataOffset];
dataOffset++;
}
}
}
break;
case _3DSTextureFormat.la4:
for (int tY = 0; tY < height / 8; tY++)
{
for (int tX = 0; tX < width / 8; tX++)
{
for (int pixel = 0; pixel < 64; pixel++)
{
int x = tileOrder[pixel] % 8;
int y = (tileOrder[pixel] - x) / 8;
long outputOffset = ((tX * 8) + x + (((tY * 8 + y)) * width)) * 4;
output[outputOffset] = (byte)(data[dataOffset] >> 4);
output[outputOffset + 1] = (byte)(data[dataOffset] >> 4);
output[outputOffset + 2] = (byte)(data[dataOffset] >> 4);
output[outputOffset + 3] = (byte)(data[dataOffset] & 0xf);
dataOffset++;
}
}
}
break;
case _3DSTextureFormat.l4:
for (int tY = 0; tY < height / 8; tY++)
{
for (int tX = 0; tX < width / 8; tX++)
{
for (int pixel = 0; pixel < 64; pixel++)
{
int x = tileOrder[pixel] % 8;
int y = (tileOrder[pixel] - x) / 8;
long outputOffset = ((tX * 8) + x + (((tY * 8 + y)) * width)) * 4;
byte c = toggle ? (byte)((data[dataOffset++] & 0xf0) >> 4) : (byte)(data[dataOffset] & 0xf);
toggle = !toggle;
c = (byte)((c << 4) | c);
output[outputOffset] = c;
output[outputOffset + 1] = c;
output[outputOffset + 2] = c;
output[outputOffset + 3] = 0xff;
}
}
}
break;
case _3DSTextureFormat.a4:
for (int tY = 0; tY < height / 8; tY++)
{
for (int tX = 0; tX < width / 8; tX++)
{
for (int pixel = 0; pixel < 64; pixel++)
{
int x = tileOrder[pixel] % 8;
int y = (tileOrder[pixel] - x) / 8;
long outputOffset = ((tX * 8) + x + (((tY * 8 + y)) * width)) * 4;
output[outputOffset] = 0xff;
output[outputOffset + 1] = 0xff;
output[outputOffset + 2] = 0xff;
byte a = toggle ? (byte)((data[dataOffset++] & 0xf0) >> 4) : (byte)(data[dataOffset] & 0xf);
toggle = !toggle;
output[outputOffset + 3] = (byte)((a << 4) | a);
}
}
}
break;
case _3DSTextureFormat.etc1:
case _3DSTextureFormat.etc1a4:
byte[] decodedData = etc1Decode(data, width, height, format == _3DSTextureFormat.etc1a4);
int[] etc1Order = etc1Scramble(width, height);
int i = 0;
for (int tY = 0; tY < height / 4; tY++)
{
for (int tX = 0; tX < width / 4; tX++)
{
int TX = etc1Order[i] % (width / 4);
int TY = (etc1Order[i] - TX) / (width / 4);
for (int y = 0; y < 4; y++)
{
for (int x = 0; x < 4; x++)
{
dataOffset = ((TX * 4) + x + (((TY * 4) + y) * width)) * 4;
long outputOffset = ((tX * 4) + x + (((tY * 4 + y)) * width)) * 4;
Buffer.BlockCopy(decodedData, (int)dataOffset, output, (int)outputOffset, 4);
}
}
i += 1;
}
}
break;
}
return TextureConverter.ToBitmap(output, width, height);
}
/// <summary>
/// Encodes a PICA200 Texture.
/// </summary>
/// <param name="img">Input image to be encoded</param>
/// <param name="format">Pixel Format of the Texture</param>
/// <returns></returns>
public static byte[] Encode(Bitmap img, _3DSTextureFormat format)
{
byte[] data = TextureConverter.ToArray(img);
byte[] output = new byte[data.Length];
int outputOffset = 0;
switch (format)
{
case _3DSTextureFormat.argb8:
for (int tY = 0; tY < img.Height / 8; tY++)
{
for (int tX = 0; tX < img.Width / 8; tX++)
{
for (int pixel = 0; pixel < 64; pixel++)
{
int x = tileOrder[pixel] % 8;
int y = (tileOrder[pixel] - x) / 8;
int dataOffset = ((tX * 8) + x + (tY * 8 + y) * img.Width) * 4;
Buffer.BlockCopy(data, dataOffset, output, outputOffset + 1, 3);
output[outputOffset] = data[dataOffset + 3];
outputOffset += 4;
}
}
}
break;
default: throw new NotImplementedException(nameof(format));
}
return output;
}
#region "ETC1"
private static byte[] etc1Decode(byte[] input, int width, int height, bool alpha)
{
byte[] output = new byte[(width * height * 4)];
long offset = 0;
for (int y = 0; y < height / 4; y++)
{
for (int x = 0; x < width / 4; x++)
{
byte[] colorBlock = new byte[8];
byte[] alphaBlock = new byte[8];
if (alpha)
{
for (int i = 0; i < 8; i++)
{
colorBlock[7 - i] = input[offset + 8 + i];
alphaBlock[i] = input[offset + i];
}
offset += 16;
}
else
{
for (int i = 0; i < 8; i++)
{
colorBlock[7 - i] = input[offset + i];
alphaBlock[i] = 0xff;
}
offset += 8;
}
colorBlock = etc1DecodeBlock(colorBlock);
bool toggle = false;
long alphaOffset = 0;
for (int tX = 0; tX < 4; tX++)
{
for (int tY = 0; tY < 4; tY++)
{
int outputOffset = (x * 4 + tX + ((y * 4 + tY) * width)) * 4;
int blockOffset = (tX + (tY * 4)) * 4;
Buffer.BlockCopy(colorBlock, blockOffset, output, outputOffset, 3);
byte a = toggle ? (byte)((alphaBlock[alphaOffset++] & 0xf0) >> 4) : (byte)(alphaBlock[alphaOffset] & 0xf);
output[outputOffset + 3] = (byte)((a << 4) | a);
toggle = !toggle;
}
}
}
}
return output;
}
private static byte[] etc1DecodeBlock(byte[] data)
{
uint blockTop = BitConverter.ToUInt32(data, 0);
uint blockBottom = BitConverter.ToUInt32(data, 4);
bool flip = (blockTop & 0x1000000) > 0;
bool difference = (blockTop & 0x2000000) > 0;
uint r1, g1, b1;
uint r2, g2, b2;
if (difference)
{
r1 = blockTop & 0xf8;
g1 = (blockTop & 0xf800) >> 8;
b1 = (blockTop & 0xf80000) >> 16;
r2 = (uint)((sbyte)(r1 >> 3) + ((sbyte)((blockTop & 7) << 5) >> 5));
g2 = (uint)((sbyte)(g1 >> 3) + ((sbyte)((blockTop & 0x700) >> 3) >> 5));
b2 = (uint)((sbyte)(b1 >> 3) + ((sbyte)((blockTop & 0x70000) >> 11) >> 5));
r1 |= r1 >> 5;
g1 |= g1 >> 5;
b1 |= b1 >> 5;
r2 = (r2 << 3) | (r2 >> 2);
g2 = (g2 << 3) | (g2 >> 2);
b2 = (b2 << 3) | (b2 >> 2);
}
else
{
r1 = blockTop & 0xf0;
g1 = (blockTop & 0xf000) >> 8;
b1 = (blockTop & 0xf00000) >> 16;
r2 = (blockTop & 0xf) << 4;
g2 = (blockTop & 0xf00) >> 4;
b2 = (blockTop & 0xf0000) >> 12;
r1 |= r1 >> 4;
g1 |= g1 >> 4;
b1 |= b1 >> 4;
r2 |= r2 >> 4;
g2 |= g2 >> 4;
b2 |= b2 >> 4;
}
uint table1 = (blockTop >> 29) & 7;
uint table2 = (blockTop >> 26) & 7;
byte[] output = new byte[(4 * 4 * 4)];
if (!flip)
{
for (int y = 0; y <= 3; y++)
{
for (int x = 0; x <= 1; x++)
{
Color color1 = etc1Pixel(r1, g1, b1, x, y, blockBottom, table1);
Color color2 = etc1Pixel(r2, g2, b2, x + 2, y, blockBottom, table2);
int offset1 = (y * 4 + x) * 4;
output[offset1] = color1.B;
output[offset1 + 1] = color1.G;
output[offset1 + 2] = color1.R;
int offset2 = (y * 4 + x + 2) * 4;
output[offset2] = color2.B;
output[offset2 + 1] = color2.G;
output[offset2 + 2] = color2.R;
}
}
}
else
{
for (int y = 0; y <= 1; y++)
{
for (int x = 0; x <= 3; x++)
{
Color color1 = etc1Pixel(r1, g1, b1, x, y, blockBottom, table1);
Color color2 = etc1Pixel(r2, g2, b2, x, y + 2, blockBottom, table2);
int offset1 = (y * 4 + x) * 4;
output[offset1] = color1.B;
output[offset1 + 1] = color1.G;
output[offset1 + 2] = color1.R;
int offset2 = ((y + 2) * 4 + x) * 4;
output[offset2] = color2.B;
output[offset2 + 1] = color2.G;
output[offset2 + 2] = color2.R;
}
}
}
return output;
}
private static Color etc1Pixel(uint r, uint g, uint b, int x, int y, uint block, uint table)
{
int index = x * 4 + y;
uint MSB = block << 1;
int pixel = index < 8
? etc1LUT[table, ((block >> (index + 24)) & 1) + ((MSB >> (index + 8)) & 2)]
: etc1LUT[table, ((block >> (index + 8)) & 1) + ((MSB >> (index - 8)) & 2)];
r = saturate((int)(r + pixel));
g = saturate((int)(g + pixel));
b = saturate((int)(b + pixel));
return Color.FromArgb((int)r, (int)g, (int)b);
}
private static byte saturate(int value)
{
if (value > 0xff)
return 0xff;
if (value < 0)
return 0;
return (byte)(value & 0xff);
}
private static int[] etc1Scramble(int width, int height)
{
//Maybe theres a better way to do this?
int[] tileScramble = new int[((width / 4) * (height / 4))];
int baseAccumulator = 0;
int rowAccumulator = 0;
int baseNumber = 0;
int rowNumber = 0;
for (int tile = 0; tile < tileScramble.Length; tile++)
{
if ((tile % (width / 4) == 0) && tile > 0)
{
if (rowAccumulator < 1)
{
rowAccumulator += 1;
rowNumber += 2;
baseNumber = rowNumber;
}
else
{
rowAccumulator = 0;
baseNumber -= 2;
rowNumber = baseNumber;
}
}
tileScramble[tile] = baseNumber;
if (baseAccumulator < 1)
{
baseAccumulator++;
baseNumber++;
}
else
{
baseAccumulator = 0;
baseNumber += 3;
}
}
return tileScramble;
}
#endregion
}
}

View File

@@ -0,0 +1,139 @@
using OMI;
using OMI.Workers;
using PckStudio.Core.FileFormats;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
namespace PckStudio.Core.IO.PckAudio
{
public class InvalidAudioPckException : Exception
{
public InvalidAudioPckException(string message) : base(message)
{ }
}
public class PckAudioFileReader : IDataFormatReader<PckAudioFile>, IDataFormatReader
{
private PckAudioFile _file;
private Endianness _endianness;
private List<string> LUT = new List<string>();
private List<PckAudioFile.AudioCategory.EAudioType> _OriginalAudioTypeOrder = new List<PckAudioFile.AudioCategory.EAudioType>();
public PckAudioFileReader(Endianness endianness)
{
_endianness = endianness;
}
public PckAudioFile FromFile(string filename)
{
if(File.Exists(filename))
{
PckAudioFile file;
using(FileStream fs = File.OpenRead(filename))
{
file = FromStream(fs);
}
return file;
}
throw new FileNotFoundException(filename);
}
public PckAudioFile FromStream(Stream stream)
{
using (var reader = new EndiannessAwareBinaryReader(stream,
_endianness == Endianness.BigEndian
? Encoding.BigEndianUnicode
: Encoding.Unicode,
leaveOpen: true, _endianness))
{
int pck_type = reader.ReadInt32();
if (pck_type > 0xf00000) // 03 00 00 00 == true
throw new OverflowException(nameof(pck_type));
if (pck_type > 1)
throw new InvalidAudioPckException(nameof(pck_type));
_file = new PckAudioFile();
ReadLookUpTable(reader);
ReadCategories(reader);
ReadCategorySongs(reader);
}
return _file;
}
private void ReadLookUpTable(EndiannessAwareBinaryReader reader)
{
int count = reader.ReadInt32();
LUT = new List<string>(count);
for (int i = 0; i < count; i++)
{
int index = reader.ReadInt32();
string value = ReadString(reader);
LUT.Insert(index, value);
}
}
private void ReadCategories(EndiannessAwareBinaryReader reader)
{
int categoryEntryCount = reader.ReadInt32();
for (; 0 < categoryEntryCount; categoryEntryCount--)
{
var parameterType = (PckAudioFile.AudioCategory.EAudioParameterType)reader.ReadInt32();
var audioType = (PckAudioFile.AudioCategory.EAudioType)reader.ReadInt32();
string name = ReadString(reader);
// AddCategory puts the file's categories out of order and causes some songs to be put in the wrong categories
// This is my simple fix for the issue.
_OriginalAudioTypeOrder.Add(audioType);
_file.AddCategory(parameterType, audioType, name);
}
}
private void ReadCategorySongs(EndiannessAwareBinaryReader reader)
{
List<string> credits = new List<string>();
List<string> creditIds = new List<string>();
foreach (PckAudioFile.AudioCategory.EAudioType c in _OriginalAudioTypeOrder)
{
int audioCount = reader.ReadInt32();
for (; 0 < audioCount; audioCount--)
{
string key = LUT[reader.ReadInt32()];
string value = ReadString(reader);
switch (key)
{
case "CUENAME":
_file.GetCategory(c).SongNames.Add(value);
break;
case "CREDIT":
credits.Add(value);
break;
case "CREDITID":
creditIds.Add(value);
_file.AddCreditId(value);
break;
default:
throw new InvalidDataException(nameof(key));
}
}
}
foreach ((string str, string id) credit in credits.Zip(creditIds, (str, id) => (str, id)))
{
_file.SetCredit(credit.id, credit.str);
}
}
private string ReadString(EndiannessAwareBinaryReader reader)
{
int len = reader.ReadInt32();
string s = reader.ReadString(len);
reader.ReadInt32(); // padding
return s;
}
object IDataFormatReader.FromStream(Stream stream) => FromStream(stream);
object IDataFormatReader.FromFile(string filename) => FromFile(filename);
}
}

View File

@@ -0,0 +1,104 @@
using OMI;
using OMI.Workers;
using PckStudio.Core.FileFormats;
using System.Collections.Generic;
using System.IO;
using System.Text;
namespace PckStudio.Core.IO.PckAudio
{
public class PckAudioFileWriter : IDataFormatWriter
{
private PckAudioFile _file;
private Endianness _endianness;
private static readonly List<string> LUT = new List<string>
{
"CUENAME",
"CREDIT",
"CREDITID"
};
public PckAudioFileWriter(PckAudioFile file, Endianness endianness)
{
_file = file;
_endianness = endianness;
}
public void WriteToFile(string filename)
{
using (FileStream fs = File.OpenWrite(filename))
{
WriteToStream(fs);
}
}
public void WriteToStream(Stream stream)
{
using (var writer = new EndiannessAwareBinaryWriter(stream,
_endianness == Endianness.BigEndian
? Encoding.BigEndianUnicode
: Encoding.Unicode,
leaveOpen: true, _endianness))
{
writer.Write(_file.type);
WriteLookUpTable(writer);
WriteCategories(writer);
WriteCategorySongs(writer);
}
}
private void WriteString(EndiannessAwareBinaryWriter writer, string s)
{
writer.Write(s.Length);
writer.WriteString(s);
writer.Write(0); // padding
}
private void WriteLookUpTable(EndiannessAwareBinaryWriter writer)
{
writer.Write(3);
for (int i = 0; i < 3; i++)
{
writer.Write(i);
WriteString(writer, LUT[i]);
}
}
private void WriteCategories(EndiannessAwareBinaryWriter writer)
{
writer.Write(_file.Categories.Length);
foreach (PckAudioFile.AudioCategory category in _file.Categories)
{
writer.Write((int)category.parameterType);
writer.Write((int)category.AudioType);
WriteString(writer, category.Name);
}
}
private void WriteCategorySongs(EndiannessAwareBinaryWriter writer)
{
bool addCredit = true;
foreach (PckAudioFile.AudioCategory category in _file.Categories)
{
writer.Write(category.SongNames.Count + (addCredit ? _file.Credits.Count * 2 : 0));
foreach (var name in category.SongNames)
{
writer.Write(LUT.IndexOf("CUENAME"));
WriteString(writer, name);
}
if (addCredit)
{
foreach (KeyValuePair<string, string> credit in _file.Credits)
{
writer.Write(LUT.IndexOf("CREDIT"));
WriteString(writer, credit.Value);
writer.Write(LUT.IndexOf("CREDITID"));
WriteString(writer, credit.Key);
}
}
addCredit = false;
}
}
}
}

View File

@@ -0,0 +1,60 @@
/* Copyright (c) 2022-present miku-666
* This software is provided 'as-is', without any express or implied
* warranty. In no event will the authors be held liable for any damages
* arising from the use of this software.
*
* Permission is granted to anyone to use this software for any purpose,
* including commercial applications, and to alter it and redistribute it
* freely, subject to the following restrictions:
*
* 1.The origin of this software must not be misrepresented; you must not
* claim that you wrote the original software. If you use this software
* in a product, an acknowledgment in the product documentation would be
* appreciated but is not required.
* 2. Altered source versions must be plainly marked as such, and must not be
* misrepresented as being the original software.
* 3. This notice may not be removed or altered from any source distribution.
**/
namespace PckStudio.Core.IO.TGA
{
internal enum TGADataTypeCode : byte
{
/// <summary>
/// No image data included.
/// </summary>
NO_DATA = 0,
/// <summary>
/// Uncompressed, color-mapped images.
/// </summary>
COLORMAPPED = 1,
/// <summary>
/// Uncompressed, RGB images.
/// </summary>
RGB = 2,
/// <summary>
/// Uncompressed, black and white images.
/// </summary>
BLACK_WHITE = 3,
/// <summary>
/// Runlength encoded color-mapped images.
/// </summary>
RLE_COLORMAPPED = 9,
/// <summary>
/// Runlength encoded RGB images.
/// </summary>
RLE_RGB = 10,
/// <summary>
/// Compressed, black and white images.
/// </summary>
COMPRESSED_BLACK_WHITE = 11,
/// <summary>
/// Compressed color-mapped data, using Huffman, Delta, and runlength encoding.
/// </summary>
COMPRESSED_RLE_COLORMAPPED = 32,
/// <summary>
/// Compressed color-mapped data, using Huffman, Delta, and runlength encoding. 4-pass quadtree-type process.
/// </summary>
COMPRESSED_RLE_COLORMAPPED_4 = 33,
}
}

View File

@@ -0,0 +1,33 @@
/* Copyright (c) 2022-present miku-666
* This software is provided 'as-is', without any express or implied
* warranty. In no event will the authors be held liable for any damages
* arising from the use of this software.
*
* Permission is granted to anyone to use this software for any purpose,
* including commercial applications, and to alter it and redistribute it
* freely, subject to the following restrictions:
*
* 1.The origin of this software must not be misrepresented; you must not
* claim that you wrote the original software. If you use this software
* in a product, an acknowledgment in the product documentation would be
* appreciated but is not required.
* 2. Altered source versions must be plainly marked as such, and must not be
* misrepresented as being the original software.
* 3. This notice may not be removed or altered from any source distribution.
**/
using System.IO;
using System.Drawing;
namespace PckStudio.Core.IO.TGA
{
internal static class TGADeserializer
{
private static TGAReader _reader = new TGAReader();
public static Image DeserializeFromStream(Stream stream)
{
TGAFileData tgaImg = _reader.FromStream(stream);
return tgaImg.Bitmap;
}
}
}

View File

@@ -0,0 +1,25 @@
using System;
using System.Runtime.Serialization;
namespace PckStudio.Core.IO.TGA
{
[Serializable]
internal class TGAException : Exception
{
public TGAException()
{
}
public TGAException(string message) : base(message)
{
}
public TGAException(string message, Exception innerException) : base(message, innerException)
{
}
protected TGAException(SerializationInfo info, StreamingContext context) : base(info, context)
{
}
}
}

View File

@@ -0,0 +1,63 @@
/* Copyright (c) 2022-present miku-666
* This software is provided 'as-is', without any express or implied
* warranty. In no event will the authors be held liable for any damages
* arising from the use of this software.
*
* Permission is granted to anyone to use this software for any purpose,
* including commercial applications, and to alter it and redistribute it
* freely, subject to the following restrictions:
*
* 1.The origin of this software must not be misrepresented; you must not
* claim that you wrote the original software. If you use this software
* in a product, an acknowledgment in the product documentation would be
* appreciated but is not required.
* 2. Altered source versions must be plainly marked as such, and must not be
* misrepresented as being the original software.
* 3. This notice may not be removed or altered from any source distribution.
**/
using System;
using System.Windows.Forms;
namespace PckStudio.Core.IO.TGA
{
internal struct TGAExtentionData
{
public const short ExtensionSize = 0x1EF;
public string AuthorName;
public string AuthorComment;
public DateTime TimeStamp;
public string JobID;
public TimeSpan JobTime;
public string SoftwareID;
public byte[] SoftwareVersion;
public int KeyColor;
public int PixelAspectRatio;
public int GammaValue;
public int ColorCorrectionOffset;
public int PostageStampOffset;
public int ScanLineOffset;
public byte AttributesType;
public static TGAExtentionData Create()
{
var extensionData = new TGAExtentionData();
extensionData.AuthorName = "";
extensionData.AuthorComment = "";
extensionData.AuthorComment = "";
extensionData.TimeStamp = DateTime.Now;
extensionData.JobID = "";
extensionData.JobTime = new TimeSpan(extensionData.TimeStamp.Hour, extensionData.TimeStamp.Minute, extensionData.TimeStamp.Second);
extensionData.SoftwareID = Application.ProductName;
Version.TryParse(Application.ProductVersion, out Version currentVersion);
extensionData.SoftwareVersion = new byte[] { (byte)currentVersion.Major, (byte)currentVersion.Minor, (byte)currentVersion.Build };
extensionData.KeyColor = 0;
extensionData.PixelAspectRatio = 0;
extensionData.GammaValue = 0;
extensionData.ColorCorrectionOffset = 0;
extensionData.PostageStampOffset = 0;
extensionData.ScanLineOffset = 0;
extensionData.AttributesType = 3;
return extensionData;
}
}
}

View File

@@ -0,0 +1,40 @@
/* Copyright (c) 2022-present miku-666
* This software is provided 'as-is', without any express or implied
* warranty. In no event will the authors be held liable for any damages
* arising from the use of this software.
*
* Permission is granted to anyone to use this software for any purpose,
* including commercial applications, and to alter it and redistribute it
* freely, subject to the following restrictions:
*
* 1.The origin of this software must not be misrepresented; you must not
* claim that you wrote the original software. If you use this software
* in a product, an acknowledgment in the product documentation would be
* appreciated but is not required.
* 2. Altered source versions must be plainly marked as such, and must not be
* misrepresented as being the original software.
* 3. This notice may not be removed or altered from any source distribution.
**/
using System.IO;
using System.Drawing;
namespace PckStudio.Core.IO.TGA
{
internal class TGAFileData
{
public TGAFileData(TGAHeader header, Image bitmap, TGAFooter footer, TGAExtentionData extentionData)
{
if (bitmap.Width != header.Width || bitmap.Height != header.Height)
throw new InvalidDataException("Header resolution doesn't match Image resolution");
Header = header;
Bitmap = bitmap;
Footer = footer;
ExtentionData = extentionData;
}
public readonly TGAHeader Header;
public readonly Image Bitmap;
public readonly TGAFooter Footer;
public readonly TGAExtentionData ExtentionData;
}
}

View File

@@ -0,0 +1,27 @@
/* Copyright (c) 2022-present miku-666
* This software is provided 'as-is', without any express or implied
* warranty. In no event will the authors be held liable for any damages
* arising from the use of this software.
*
* Permission is granted to anyone to use this software for any purpose,
* including commercial applications, and to alter it and redistribute it
* freely, subject to the following restrictions:
*
* 1.The origin of this software must not be misrepresented; you must not
* claim that you wrote the original software. If you use this software
* in a product, an acknowledgment in the product documentation would be
* appreciated but is not required.
* 2. Altered source versions must be plainly marked as such, and must not be
* misrepresented as being the original software.
* 3. This notice may not be removed or altered from any source distribution.
**/
namespace PckStudio.Core.IO.TGA
{
internal struct TGAFooter
{
internal const string Signature = "TRUEVISION-XFILE";
public int ExtensionDataOffset;
public int DeveloperAreaDataOffset;
}
}

View File

@@ -0,0 +1,37 @@
/* Copyright (c) 2022-present miku-666
* This software is provided 'as-is', without any express or implied
* warranty. In no event will the authors be held liable for any damages
* arising from the use of this software.
*
* Permission is granted to anyone to use this software for any purpose,
* including commercial applications, and to alter it and redistribute it
* freely, subject to the following restrictions:
*
* 1.The origin of this software must not be misrepresented; you must not
* claim that you wrote the original software. If you use this software
* in a product, an acknowledgment in the product documentation would be
* appreciated but is not required.
* 2. Altered source versions must be plainly marked as such, and must not be
* misrepresented as being the original software.
* 3. This notice may not be removed or altered from any source distribution.
**/
namespace PckStudio.Core.IO.TGA
{
/// <summary>
/// Resources:
/// <http://www.paulbourke.net/dataformats/tga/>
/// <https://en.wikipedia.org/wiki/Truevision_TGA>
/// </summary>
internal struct TGAHeader
{
public byte[] Id;
public TGADataTypeCode DataTypeCode;
public (byte Type, short Origin/*Offset*/, short Length, byte Depth) Colormap;
public (short X, short Y) Origin;
public short Width;
public short Height;
public byte BitsPerPixel;
public byte ImageDescriptor;
}
}

View File

@@ -0,0 +1,240 @@
/* Copyright (c) 2022-present miku-666
* This software is provided 'as-is', without any express or implied
* warranty. In no event will the authors be held liable for any damages
* arising from the use of this software.
*
* Permission is granted to anyone to use this software for any purpose,
* including commercial applications, and to alter it and redistribute it
* freely, subject to the following restrictions:
*
* 1.The origin of this software must not be misrepresented; you must not
* claim that you wrote the original software. If you use this software
* in a product, an acknowledgment in the product documentation would be
* appreciated but is not required.
* 2. Altered source versions must be plainly marked as such, and must not be
* misrepresented as being the original software.
* 3. This notice may not be removed or altered from any source distribution.
**/
using System;
using System.IO;
using System.Text;
using System.Drawing;
using System.Diagnostics;
using System.Drawing.Imaging;
using System.Runtime.InteropServices;
using OMI.Workers;
using OMI;
namespace PckStudio.Core.IO.TGA
{
internal class TGAReader : IDataFormatReader<TGAFileData>, IDataFormatReader
{
object IDataFormatReader.FromStream(Stream stream) => FromStream(stream);
object IDataFormatReader.FromFile(string filename) => FromFile(filename);
public TGAFileData FromFile(string filename)
{
if (File.Exists(filename))
{
using(FileStream fs = File.OpenRead(filename) )
{
return FromStream(fs);
}
}
throw new FileNotFoundException(filename);
}
public TGAFileData FromStream(Stream stream)
{
using var reader = new EndiannessAwareBinaryReader(stream, Encoding.ASCII, leaveOpen: true, Endianness.LittleEndian);
TGAHeader header = LoadHeader(reader);
Image image = LoadImage(reader, header);
TGAFooter footer = LoadFooter(reader);
TGAExtentionData extentionData = LoadExtentionData(reader, footer);
return new TGAFileData(header, image, footer, extentionData);
}
private static void TGA_HandleRGB(EndiannessAwareBinaryReader reader, TGAHeader header, BitmapData bitmapData)
{
int bytesPerPixel = header.BitsPerPixel / 8;
byte[] data = reader.ReadBytes(header.Height * header.Width * bytesPerPixel);
Marshal.Copy(data, 0, bitmapData.Scan0, data.Length);
}
private static void TGA_HandleNoData(EndiannessAwareBinaryReader _, TGAHeader header, BitmapData bitmapData)
{
Random r = new Random();
byte[] bytes = new byte[bitmapData.Width * bitmapData.Height * 4];
r.NextBytes(bytes);
Marshal.Copy(bytes, 0, bitmapData.Scan0, bytes.Length);
}
private static TGAHeader LoadHeader(EndiannessAwareBinaryReader reader)
{
var header = new TGAHeader();
byte[] bytes = reader.ReadBytes(3);
(var headerIdLength, header.Colormap.Type, header.DataTypeCode) = (bytes[0], bytes[1], (TGADataTypeCode)bytes[2]);
header.Colormap.Origin = reader.ReadInt16();
header.Colormap.Length = reader.ReadInt16();
header.Colormap.Depth = reader.ReadByte();
header.Origin.X = reader.ReadInt16();
header.Origin.Y = reader.ReadInt16();
header.Width = reader.ReadInt16();
header.Height = reader.ReadInt16();
header.BitsPerPixel = reader.ReadByte();
header.ImageDescriptor = reader.ReadByte();
header.Id = reader.ReadBytes(headerIdLength);
DebugLogHeader(header);
return header;
}
private static PixelFormat GetPixelFormat(int bytesPerPixel)
{
return bytesPerPixel switch
{
2 => PixelFormat.Format16bppArgb1555,
3 => PixelFormat.Format24bppRgb,
4 => PixelFormat.Format32bppArgb,
_ => throw new NotSupportedException(nameof(bytesPerPixel))
};
}
private static Image LoadImage(EndiannessAwareBinaryReader reader, TGAHeader header)
{
if (header.DataTypeCode != TGADataTypeCode.RGB)
throw new NotSupportedException(nameof(header.DataTypeCode));
Bitmap bitmap = new Bitmap(header.Width, header.Height);
BitmapData bitmapData = bitmap.LockBits(
new Rectangle(0, 0, header.Width, header.Height),
ImageLockMode.WriteOnly,
GetPixelFormat(header.BitsPerPixel >> 3));
if (header.DataTypeCode == TGADataTypeCode.NO_DATA)
{
TGA_HandleNoData(reader, header, bitmapData);
bitmap.UnlockBits(bitmapData);
return bitmap;
}
TGA_HandleRGB(reader, header, bitmapData);
bitmap.UnlockBits(bitmapData);
bitmap.RotateFlip(RotateFlipType.RotateNoneFlipY);
return bitmap;
}
private static TGAFooter LoadFooter(EndiannessAwareBinaryReader reader)
{
long origin = reader.BaseStream.Position;
reader.BaseStream.Seek(-26, SeekOrigin.End);
TGAFooter footer = new TGAFooter();
footer.ExtensionDataOffset = reader.ReadInt32(); // optional
footer.DeveloperAreaDataOffset = reader.ReadInt32(); // optional
string signature = reader.ReadString(16);
Debug.Assert(signature.Equals(TGAFooter.Signature) || reader.ReadInt16() != 0x002E,
"Footer signature invalid");
reader.BaseStream.Seek(origin, SeekOrigin.Begin);
DebugLogFooter(footer);
return footer;
}
private static TGAExtentionData LoadExtentionData(EndiannessAwareBinaryReader reader, TGAFooter footer)
{
if (footer.ExtensionDataOffset > 0)
{
reader.BaseStream.Seek(footer.ExtensionDataOffset, SeekOrigin.Begin);
if (reader.ReadInt16() == TGAExtentionData.ExtensionSize)
{
TGAExtentionData extentionData = new TGAExtentionData();
extentionData.AuthorName = reader.ReadString(41);
extentionData.AuthorComment = reader.ReadString(324);
short month = reader.ReadInt16();
short day = reader.ReadInt16();
short year = reader.ReadInt16();
short hour = reader.ReadInt16();
short minute = reader.ReadInt16();
short second = reader.ReadInt16();
extentionData.TimeStamp = new DateTime(year, month, day, hour, minute, second);
extentionData.JobID = reader.ReadString(41);
extentionData.JobTime = new TimeSpan(
hours: reader.ReadInt16(),
minutes: reader.ReadInt16(),
seconds: reader.ReadInt16()
);
extentionData.SoftwareID = reader.ReadString(41);
extentionData.SoftwareVersion = reader.ReadBytes(3);
extentionData.KeyColor = reader.ReadInt32();
extentionData.PixelAspectRatio = reader.ReadInt32();
extentionData.GammaValue = reader.ReadInt32();
extentionData.ColorCorrectionOffset = reader.ReadInt32();
extentionData.PostageStampOffset = reader.ReadInt32();
extentionData.ScanLineOffset = reader.ReadInt32();
extentionData.AttributesType = reader.ReadByte();
DebugLogExtentionData(extentionData);
return extentionData;
}
}
return default;
}
[Conditional("DEBUG")]
[DebuggerHidden]
[DebuggerStepThrough]
private static void DebugLogExtentionData(TGAExtentionData extentionData)
{
Debug.WriteLine("-------Extention Data-------", category: nameof(TGAReader));
Debug.WriteLine(string.Format("Author Name: {0}", args: extentionData.AuthorName), category: nameof(TGAReader));
Debug.WriteLine(string.Format("Author Comment: {0}", args: extentionData.AuthorComment), category: nameof(TGAReader));
Debug.WriteLine(string.Format("Time Stamp: {0}", args: extentionData.TimeStamp), category: nameof(TGAReader));
Debug.WriteLine(string.Format("Job ID: {0}", args: extentionData.JobID), category: nameof(TGAReader));
Debug.WriteLine(string.Format("Job Time: {0}", args: extentionData.JobTime), category: nameof(TGAReader));
Debug.WriteLine(string.Format("SoftwareID: {0}", args: extentionData.SoftwareID), category: nameof(TGAReader));
Debug.WriteLine(string.Format("Software Version: {0}.{1}.{2}", extentionData.SoftwareVersion[0], extentionData.SoftwareVersion[1], extentionData.SoftwareVersion[2]), category: nameof(TGAReader));
Debug.WriteLine(string.Format("Key Color: {0}", args: extentionData.KeyColor), category: nameof(TGAReader));
Debug.WriteLine(string.Format("Pixel Aspect Ratio: {0}", args: extentionData.PixelAspectRatio), category: nameof(TGAReader));
Debug.WriteLine(string.Format("Gamma Value: {0}", args: extentionData.GammaValue), category: nameof(TGAReader));
Debug.WriteLine(string.Format("Color Correction Offset: {0}", args: extentionData.ColorCorrectionOffset), category: nameof(TGAReader));
Debug.WriteLine(string.Format("Postage Stamp Offset: {0}", args: extentionData.PostageStampOffset), category: nameof(TGAReader));
Debug.WriteLine(string.Format("Scan Line Offset: {0}", args: extentionData.ScanLineOffset), category: nameof(TGAReader));
Debug.WriteLine(string.Format("Attributes Type: {0}", args: extentionData.AttributesType), category: nameof(TGAReader));
Debug.WriteLine("----------------------------", category: nameof(TGAReader));
}
[Conditional("DEBUG")]
[DebuggerHidden]
[DebuggerStepThrough]
private static void DebugLogHeader(TGAHeader header)
{
Debug.WriteLine("------Header Data------", category: nameof(TGAReader));
Debug.WriteLine(string.Format("ID length: {0}", args: header.Id.Length), category: nameof(TGAReader));
Debug.WriteLineIf(header.Id.Length > 0, $"ID: {header.Id}", category: nameof(TGAReader));
Debug.WriteLine(string.Format("Colourmap type: {0}", args: header.Colormap.Type), category: nameof(TGAReader));
Debug.WriteLine(string.Format("Image type: {0}", args: header.DataTypeCode), category: nameof(TGAReader));
Debug.WriteLine(string.Format("Colour map offset: {0}", args: header.Colormap.Origin), category: nameof(TGAReader));
Debug.WriteLine(string.Format("Colour map length: {0}", args: header.Colormap.Length), category: nameof(TGAReader));
Debug.WriteLine(string.Format("Colour map depth: {0}", args: header.Colormap.Depth), category: nameof(TGAReader));
Debug.WriteLine(string.Format("X origin: {0}", args: header.Origin.X), category: nameof(TGAReader));
Debug.WriteLine(string.Format("Y origin: {0}", args: header.Origin.Y), category: nameof(TGAReader));
Debug.WriteLine(string.Format("Width: {0}", args: header.Width), category: nameof(TGAReader));
Debug.WriteLine(string.Format("Height: {0}", args: header.Height), category: nameof(TGAReader));
Debug.WriteLine(string.Format("Bits per pixel: {0}", args: header.BitsPerPixel), category: nameof(TGAReader));
Debug.WriteLine(string.Format("Descriptor: {0}", args: header.ImageDescriptor), category: nameof(TGAReader));
Debug.WriteLine("-----------------------", category: nameof(TGAReader));
}
[Conditional("DEBUG")]
[DebuggerHidden]
[DebuggerStepThrough]
private static void DebugLogFooter(TGAFooter footer)
{
Debug.WriteLine("-------Footer Data-------", category: nameof(TGAReader));
Debug.WriteLine($"Extension Data Offset: {footer.ExtensionDataOffset:x}", category: nameof(TGAReader));
Debug.WriteLine($"Developer Area Data Offset: {footer.DeveloperAreaDataOffset:x}", category: nameof(TGAReader));
Debug.WriteLine("-----------------------", category: nameof(TGAReader));
}
}
}

View File

@@ -0,0 +1,32 @@
/* Copyright (c) 2022-present miku-666
* This software is provided 'as-is', without any express or implied
* warranty. In no event will the authors be held liable for any damages
* arising from the use of this software.
*
* Permission is granted to anyone to use this software for any purpose,
* including commercial applications, and to alter it and redistribute it
* freely, subject to the following restrictions:
*
* 1.The origin of this software must not be misrepresented; you must not
* claim that you wrote the original software. If you use this software
* in a product, an acknowledgment in the product documentation would be
* appreciated but is not required.
* 2. Altered source versions must be plainly marked as such, and must not be
* misrepresented as being the original software.
* 3. This notice may not be removed or altered from any source distribution.
**/
using System.IO;
using System.Drawing;
namespace PckStudio.Core.IO.TGA
{
internal static class TGASerializer
{
private static TGAWriter _writer = new TGAWriter();
public static void SerializeToStream(Stream stream, Image image)
{
_writer.WriteToStream(stream, image);
}
}
}

View File

@@ -0,0 +1,140 @@
/* Copyright (c) 2022-present miku-666
* This software is provided 'as-is', without any express or implied
* warranty. In no event will the authors be held liable for any damages
* arising from the use of this software.
*
* Permission is granted to anyone to use this software for any purpose,
* including commercial applications, and to alter it and redistribute it
* freely, subject to the following restrictions:
*
* 1.The origin of this software must not be misrepresented; you must not
* claim that you wrote the original software. If you use this software
* in a product, an acknowledgment in the product documentation would be
* appreciated but is not required.
* 2. Altered source versions must be plainly marked as such, and must not be
* misrepresented as being the original software.
* 3. This notice may not be removed or altered from any source distribution.
**/
using System;
using System.IO;
using System.Drawing;
using System.Drawing.Imaging;
using System.Runtime.InteropServices;
using System.Text;
using OMI;
namespace PckStudio.Core.IO.TGA
{
internal class TGAWriter
{
private Bitmap _bitmap;
private int extensionDataOffset = 0;
public TGAWriter()
{
}
private void WriteHeader(EndiannessAwareBinaryWriter writer)
{
writer.Write(new byte[]
{
0, // header.Id.Length
0, // colormap type
(byte)TGADataTypeCode.RGB
});
writer.Write(0); // Colormap.Origin
writer.Write(0); // Colormap.Length
writer.Write(0); // Colormap.Depth
writer.Write(0); // Origin.X
writer.Write(0); // Origin.Y
writer.Write(_bitmap.Width);
writer.Write(_bitmap.Height);
writer.Write(32); // BitsPerPixel
writer.Write(8); // ImageDescriptor
}
private void WriteImage(EndiannessAwareBinaryWriter writer)
{
_bitmap.RotateFlip(RotateFlipType.RotateNoneFlipY);
BitmapData bitmapData = _bitmap.LockBits(
new Rectangle(0, 0, _bitmap.Width, _bitmap.Height),
ImageLockMode.ReadOnly,
PixelFormat.Format32bppArgb);
byte[] buffer = new byte[_bitmap.Width * _bitmap.Height * 4];
Marshal.Copy(bitmapData.Scan0, buffer, 0, _bitmap.Width * _bitmap.Height * 4);
writer.Write(buffer);
}
private void WriteFooter(EndiannessAwareBinaryWriter writer)
{
writer.Write(extensionDataOffset); // extensionDataOffset
writer.Write(0); // developerAreaDataOffset
writer.WriteString(TGAFooter.Signature);
writer.Write((byte)0x2E);
writer.Write((byte)0x00);
}
private void WriteExtensionData(EndiannessAwareBinaryWriter writer)
{
extensionDataOffset = Convert.ToInt32(writer.BaseStream.Position);
TGAExtentionData extentionData = TGAExtentionData.Create();
writer.Write(TGAExtentionData.ExtensionSize);
// Author Name
writer.WriteString(extentionData.AuthorName, 41);
// Author Comment
writer.WriteString(extentionData.AuthorComment, 324);
// Timestamp
writer.Write((short)extentionData.TimeStamp.Month);
writer.Write((short)extentionData.TimeStamp.Day);
writer.Write((short)extentionData.TimeStamp.Year);
writer.Write((short)extentionData.TimeStamp.Hour);
writer.Write((short)extentionData.TimeStamp.Minute);
writer.Write((short)extentionData.TimeStamp.Second);
// Job id
writer.WriteString(extentionData.JobID, 41);
// Job time
writer.Write((short)extentionData.JobTime.Hours);
writer.Write((short)extentionData.JobTime.Minutes);
writer.Write((short)extentionData.JobTime.Seconds);
// Software Id
writer.WriteString(extentionData.SoftwareID, 41);
// Software version
writer.Write(extentionData.SoftwareVersion, 0, 3);
// Key color
writer.Write(extentionData.KeyColor);
// Pixel aspect ratio
writer.Write(extentionData.PixelAspectRatio);
// Gamma value
writer.Write(extentionData.GammaValue);
// Color correction offset
writer.Write(extentionData.ColorCorrectionOffset);
// Postage stamp offset
writer.Write(extentionData.PostageStampOffset);
// Scan line offset
writer.Write(extentionData.ScanLineOffset);
// Attributes type
writer.Write(extentionData.AttributesType);
}
public void WriteToStream(Stream stream, Image image)
{
_bitmap = new Bitmap(image);
using (var writer = new EndiannessAwareBinaryWriter(stream, Encoding.ASCII, leaveOpen: true, Endianness.LittleEndian))
{
WriteHeader(writer);
WriteImage(writer);
WriteExtensionData(writer);
WriteFooter(writer);
}
}
public void WriteToFile(string filename, Image image)
{
using (FileStream fs = File.OpenWrite(filename))
{
WriteToStream(fs, image);
}
}
}
}

View File

@@ -0,0 +1,24 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using OMI.Formats.Pck;
namespace PckStudio.Interfaces
{
public interface IEditor<T> where T : class
{
T EditorValue { get; }
ISaveContext<T> SaveContext { get; }
void Save();
void SaveAs();
void Close();
void UpdateView();
}
}

View File

@@ -0,0 +1,22 @@
using System;
using System.IO;
using PckStudio.Core;
namespace PckStudio.Interfaces
{
public interface IModelImportProvider<T> where T : class
{
public string Name { get; }
public FileDialogFilter DialogFilter { get; }
public bool SupportImport { get; }
public bool SupportExport { get; }
public T Import(string filename);
public T Import(Stream stream);
public void Export(string filename, T model);
public void Export(ref Stream stream, T model);
}
}

View File

@@ -0,0 +1,9 @@
using OMI.Formats.Pck;
namespace PckStudio.Interfaces
{
public interface IPckAssetDeserializer<T>
{
public T Deserialize(PckAsset asset);
}
}

View File

@@ -0,0 +1,9 @@
using OMI.Formats.Pck;
namespace PckStudio.Interfaces
{
public interface IPckAssetSerializer<T>
{
public void Serialize(T obj, ref PckAsset asset);
}
}

View File

@@ -0,0 +1,9 @@
namespace PckStudio.Interfaces
{
public interface ISaveContext<T>
{
public bool AutoSave { get; }
public void Save(T value);
}
}

View File

@@ -0,0 +1,65 @@
namespace PckStudio.Interfaces
{
public delegate bool TryGetDelegate<in TKey, TValue>(TKey key, out TValue value);
public delegate bool TrySetDelegate<in TKey, TValue>(TKey key, TValue value);
public sealed class TryGet<TKey, TValue> : ITryGet<TKey, TValue>
{
private TryGetDelegate<TKey, TValue> _tryGetDelegate;
public static ITryGet<TKey, TValue> FromDelegate(TryGetDelegate<TKey, TValue> tryGetDelegate) => new TryGet<TKey, TValue>(tryGetDelegate);
bool ITryGet<TKey, TValue>.TryGet(TKey key, out TValue value) => _tryGetDelegate(key, out value);
private TryGet(TryGetDelegate<TKey, TValue> tryGetDelegate)
{
_tryGetDelegate = tryGetDelegate;
}
}
public sealed class TrySet<TKey, TValue> : ITrySet<TKey, TValue>
{
private TrySetDelegate<TKey, TValue> _trySetDelegate;
public static ITrySet<TKey, TValue> FromDelegate(TrySetDelegate<TKey, TValue> trySetDelegate) => new TrySet<TKey, TValue>(trySetDelegate);
bool ITrySet<TKey, TValue>.TrySet(TKey key, TValue value) => _trySetDelegate(key, value);
private TrySet(TrySetDelegate<TKey, TValue> trySetDelegate)
{
_trySetDelegate = trySetDelegate;
}
}
public sealed class TryGetSet<TKey, TValue> : ITryGetSet<TKey, TValue>
{
public static ITryGetSet<TKey, TValue> FromDelegates(TryGetDelegate<TKey, TValue> tryGetDelegate, TrySetDelegate<TKey, TValue> trySetDelegate) => new TryGetSet<TKey, TValue>(tryGetDelegate, trySetDelegate);
public bool TryGet(TKey key, out TValue value) => _tryGetDelegate(key, out value);
public bool TrySet(TKey key, TValue value) => _trySetDelegate(key, value);
private TryGetDelegate<TKey, TValue> _tryGetDelegate;
private TrySetDelegate<TKey, TValue> _trySetDelegate;
private TryGetSet(TryGetDelegate<TKey, TValue> tryGetDelegate, TrySetDelegate<TKey, TValue> trySetDelegate)
{
_tryGetDelegate = tryGetDelegate;
_trySetDelegate = trySetDelegate;
}
}
public interface ITryGet<in TKey, TValue>
{
bool TryGet(TKey key, out TValue value);
}
public interface ITrySet<in TKey, TValue>
{
bool TrySet(TKey key, TValue value);
}
public interface ITryGetSet<in TKey, TValue> : ITryGet<TKey, TValue>, ITrySet<TKey, TValue>
{
}
}

View File

@@ -0,0 +1,90 @@
namespace PckStudio.Core.Additional_Popups
{
partial class ItemSelectionPopUp
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(ItemSelectionPopUp));
this.label2 = new System.Windows.Forms.Label();
this.okBtn = new System.Windows.Forms.Button();
this.cancelButton = new System.Windows.Forms.Button();
this.ComboBox = new System.Windows.Forms.ComboBox();
this.SuspendLayout();
//
// label2
//
resources.ApplyResources(this.label2, "label2");
this.label2.ForeColor = System.Drawing.Color.White;
this.label2.Name = "label2";
//
// okBtn
//
resources.ApplyResources(this.okBtn, "okBtn");
this.okBtn.ForeColor = System.Drawing.Color.White;
this.okBtn.Name = "okBtn";
this.okBtn.UseVisualStyleBackColor = true;
this.okBtn.Click += new System.EventHandler(this.okBtn_Click);
//
// cancelButton
//
resources.ApplyResources(this.cancelButton, "cancelButton");
this.cancelButton.ForeColor = System.Drawing.Color.White;
this.cancelButton.Name = "cancelButton";
this.cancelButton.UseVisualStyleBackColor = true;
this.cancelButton.Click += new System.EventHandler(this.cancelButton_Click);
//
// ComboBox
//
this.ComboBox.FormattingEnabled = true;
resources.ApplyResources(this.ComboBox, "ComboBox");
this.ComboBox.Name = "ComboBox";
//
// ItemSelectionPopUp
//
resources.ApplyResources(this, "$this");
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ControlBox = false;
this.Controls.Add(this.ComboBox);
this.Controls.Add(this.cancelButton);
this.Controls.Add(this.okBtn);
this.Controls.Add(this.label2);
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedToolWindow;
this.MaximizeBox = false;
this.MinimizeBox = false;
this.Name = "ItemSelectionPopUp";
this.ResumeLayout(false);
this.PerformLayout();
}
#endregion
private System.Windows.Forms.Button cancelButton;
private System.Windows.Forms.ComboBox ComboBox;
private System.Windows.Forms.Label label2;
private System.Windows.Forms.Button okBtn;
}
}

View File

@@ -0,0 +1,44 @@
using System;
using System.Windows.Forms;
namespace PckStudio.Core.Additional_Popups
{
public partial class ItemSelectionPopUp : Form
{
public string SelectedItem => DialogResult == DialogResult.OK ? ComboBox.Text : string.Empty;
public int SelectedIndex => DialogResult == DialogResult.OK ? ComboBox.SelectedIndex : -1;
public string LabelText
{
get => label2.Text;
set => label2.Text = value;
}
public string ButtonText
{
get => okBtn.Text;
set => okBtn.Text = value;
}
public ItemSelectionPopUp(params string[] items)
{
InitializeComponent();
ComboBox.Items.AddRange(items);
}
private void okBtn_Click(object sender, EventArgs e)
{
if(ComboBox.SelectedIndex <= -1)
{
cancelButton_Click(sender, e);
return;
}
DialogResult = DialogResult.OK;
}
private void cancelButton_Click(object sender, EventArgs e)
{
DialogResult = DialogResult.Cancel;
Close();
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,21 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Newtonsoft.Json;
namespace PckStudio.Core.Json
{
public class JsonColorEntry
{
[JsonProperty("defaultName", Required = Required.Always)]
public string DefaultName { get; set; }
[JsonProperty("isWaterColour", DefaultValueHandling = DefaultValueHandling.Populate)]
public bool IsWaterColour { get; set; }
[JsonProperty("variants", DefaultValueHandling = DefaultValueHandling.Populate)]
public string[] Variants { get; set; }
}
}

View File

@@ -0,0 +1,20 @@
using System;
using Newtonsoft.Json;
namespace PckStudio.Core.Json
{
public class EntityInfo
{
[JsonProperty("displayName")]
public string DisplayName { get; set; }
[JsonProperty("internalName")]
public string InternalName { get; set; }
public EntityInfo(string displayName, string internalName)
{
DisplayName = displayName;
InternalName = internalName;
}
}
}

View File

@@ -0,0 +1,39 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Newtonsoft.Json;
namespace PckStudio.Core.Json
{
public sealed class JsonTileInfo
{
[JsonProperty("displayName")]
public string DisplayName { get; set; }
[JsonProperty("internalName")]
public string InternalName { get; set; }
[JsonProperty("width")]
public int TileWidth { get; set; } = 1;
[JsonProperty("height")]
public int TileHeight { get; set; } = 1;
[JsonProperty("hasColourEntry", DefaultValueHandling = DefaultValueHandling.Populate)]
public bool HasColourEntry { get; set; }
[JsonProperty("colourEntry", DefaultValueHandling = DefaultValueHandling.Populate)]
public JsonColorEntry ColourEntry { get; set; }
[JsonProperty("allowCustomColour", DefaultValueHandling = DefaultValueHandling.Populate)]
public bool AllowCustomColour { get; set; }
public JsonTileInfo(string displayName, string internalName)
{
DisplayName = displayName;
InternalName = internalName;
}
}
}

View File

@@ -0,0 +1,24 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Newtonsoft.Json;
namespace PckStudio.Core.Json
{
internal class UpdateInformation
{
[JsonProperty("version")]
public string Version { get; set; }
[JsonProperty("url")]
public string Url { get; set; }
[JsonProperty("changelog")]
public string Changelog { get; set; }
[JsonProperty("mandatory")]
public bool Mandatory { get; set; }
}
}

View File

@@ -0,0 +1,66 @@
/* Copyright (c) 2023-present miku-666
* This software is provided 'as-is', without any express or implied
* warranty. In no event will the authors be held liable for any damages
* arising from the use of this software.
*
* Permission is granted to anyone to use this software for any purpose,
* including commercial applications, and to alter it and redistribute it
* freely, subject to the following restrictions:
*
* 1. The origin of this software must not be misrepresented; you must not
* claim that you wrote the original software. If you use this software
* in a product, an acknowledgment in the product documentation would be
* appreciated but is not required.
* 2. Altered source versions must be plainly marked as such, and must not be
* misrepresented as being the original software.
* 3. This notice may not be removed or altered from any source distribution.
**/
using System;
using System.IO;
namespace PckStudio.Core.Misc
{
public class FileCacher
{
private string _cacheDirectory;
public string CacheDirectory => _cacheDirectory;
public FileCacher(string cacheDirectory)
{
_cacheDirectory = cacheDirectory;
}
public bool HasFileCached(string filename)
{
string destinationFilepath = Path.Combine(_cacheDirectory, filename);
return File.Exists(destinationFilepath);
}
public string GetCachedFilepath(string filename)
{
if (HasFileCached(filename))
{
return Path.Combine(_cacheDirectory, filename);
}
return null;
}
/// <summary>
/// Caches data if the <paramref name="filename"/> doesn't already exists in the <see cref="CacheDirectory"/>
/// </summary>
/// <param name="data">Data to cache</param>
/// <param name="filename">Filename with extension</param>
/// <exception cref="ArgumentNullException"></exception>
public void Cache(byte[] data, string filename)
{
_ = filename ?? throw new ArgumentNullException("filename");
string destinationFilepath = Path.Combine(_cacheDirectory, filename);
if (!File.Exists(destinationFilepath))
{
using FileStream fsDst = File.OpenWrite(destinationFilepath);
fsDst.Write(data, 0, data.Length);
}
}
}
}

View File

@@ -0,0 +1,202 @@
/*
* Source by: Simon Mourier(https://stackoverflow.com/users/403671/simon-mourier)
*/
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.ComTypes;
namespace PckStudio.Core.Misc
{
public class OpenFolderDialog
{
public virtual string ResultPath { get; protected set; }
public virtual string ResultName { get; protected set; }
public virtual string InputPath { get; set; }
public virtual bool ForceFileSystem { get; set; }
public virtual string Title { get; set; }
public virtual string OkButtonLabel { get; set; }
public virtual string FileNameLabel { get; set; }
protected virtual int SetOptions(int options)
{
if (ForceFileSystem)
{
options |= (int)FOS.FOS_FORCEFILESYSTEM;
}
return options;
}
// for all .NET
public virtual bool? ShowDialog(IntPtr owner, bool throwOnError = false)
{
var dialog = (IFileOpenDialog)new FileOpenDialog();
if (!string.IsNullOrEmpty(InputPath))
{
if (CheckHr(SHCreateItemFromParsingName(InputPath, null, typeof(IShellItem).GUID, out IShellItem item), throwOnError) != 0)
return null;
dialog.SetFolder(item);
}
FOS options = FOS.FOS_PICKFOLDERS;
options = (FOS)SetOptions((int)options);
dialog.SetOptions(options);
if (Title != null)
{
dialog.SetTitle(Title);
}
if (OkButtonLabel != null)
{
dialog.SetOkButtonLabel(OkButtonLabel);
}
if (FileNameLabel != null)
{
dialog.SetFileName(FileNameLabel);
}
if (owner == IntPtr.Zero)
{
owner = Process.GetCurrentProcess().MainWindowHandle;
if (owner == IntPtr.Zero)
{
owner = GetDesktopWindow();
}
}
var hr = dialog.Show(owner);
if (hr == ERROR_CANCELLED)
return null;
if (CheckHr(hr, throwOnError) != 0)
return null;
if (CheckHr(dialog.GetResult(out IShellItem result), throwOnError) != 0)
return null;
if (CheckHr(result.GetDisplayName(SIGDN.SIGDN_DESKTOPABSOLUTEPARSING, out var path), throwOnError) != 0)
return null;
ResultPath = path;
if (CheckHr(result.GetDisplayName(SIGDN.SIGDN_DESKTOPABSOLUTEEDITING, out path), false) == 0)
{
ResultName = path;
}
return true;
}
private static int CheckHr(int hr, bool throwOnError)
{
if (hr != 0)
{
if (throwOnError)
Marshal.ThrowExceptionForHR(hr);
}
return hr;
}
[DllImport("shell32")]
private static extern int SHCreateItemFromParsingName([MarshalAs(UnmanagedType.LPWStr)] string pszPath, IBindCtx pbc, [MarshalAs(UnmanagedType.LPStruct)] Guid riid, out IShellItem ppv);
[DllImport("user32")]
private static extern IntPtr GetDesktopWindow();
#pragma warning disable IDE1006 // Naming Styles
private const int ERROR_CANCELLED = unchecked((int)0x800704C7);
#pragma warning restore IDE1006 // Naming Styles
[ComImport, Guid("DC1C5A9C-E88A-4dde-A5A1-60F82A20AEF7")] // CLSID_FileOpenDialog
private class FileOpenDialog
{
}
[ComImport, Guid("42f85136-db7e-439c-85f1-e4075d135fc8"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
private interface IFileOpenDialog
{
[PreserveSig] int Show(IntPtr parent); // IModalWindow
[PreserveSig] int SetFileTypes(); // not fully defined
[PreserveSig] int SetFileTypeIndex(int iFileType);
[PreserveSig] int GetFileTypeIndex(out int piFileType);
[PreserveSig] int Advise(); // not fully defined
[PreserveSig] int Unadvise();
[PreserveSig] int SetOptions(FOS fos);
[PreserveSig] int GetOptions(out FOS pfos);
[PreserveSig] int SetDefaultFolder(IShellItem psi);
[PreserveSig] int SetFolder(IShellItem psi);
[PreserveSig] int GetFolder(out IShellItem ppsi);
[PreserveSig] int GetCurrentSelection(out IShellItem ppsi);
[PreserveSig] int SetFileName([MarshalAs(UnmanagedType.LPWStr)] string pszName);
[PreserveSig] int GetFileName([MarshalAs(UnmanagedType.LPWStr)] out string pszName);
[PreserveSig] int SetTitle([MarshalAs(UnmanagedType.LPWStr)] string pszTitle);
[PreserveSig] int SetOkButtonLabel([MarshalAs(UnmanagedType.LPWStr)] string pszText);
[PreserveSig] int SetFileNameLabel([MarshalAs(UnmanagedType.LPWStr)] string pszLabel);
[PreserveSig] int GetResult(out IShellItem ppsi);
[PreserveSig] int AddPlace(IShellItem psi, int alignment);
[PreserveSig] int SetDefaultExtension([MarshalAs(UnmanagedType.LPWStr)] string pszDefaultExtension);
[PreserveSig] int Close(int hr);
[PreserveSig] int SetClientGuid(); // not fully defined
[PreserveSig] int ClearClientData();
[PreserveSig] int SetFilter([MarshalAs(UnmanagedType.IUnknown)] object pFilter);
[PreserveSig] int GetResults([MarshalAs(UnmanagedType.IUnknown)] out object ppenum);
[PreserveSig] int GetSelectedItems([MarshalAs(UnmanagedType.IUnknown)] out object ppsai);
}
[ComImport, Guid("43826D1E-E718-42EE-BC55-A1E261C37BFE"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
private interface IShellItem
{
[PreserveSig] int BindToHandler(); // not fully defined
[PreserveSig] int GetParent(); // not fully defined
[PreserveSig] int GetDisplayName(SIGDN sigdnName, [MarshalAs(UnmanagedType.LPWStr)] out string ppszName);
[PreserveSig] int GetAttributes(); // not fully defined
[PreserveSig] int Compare(); // not fully defined
}
#pragma warning disable CA1712 // Do not prefix enum values with type name
private enum SIGDN : uint
{
SIGDN_DESKTOPABSOLUTEEDITING = 0x8004c000,
SIGDN_DESKTOPABSOLUTEPARSING = 0x80028000,
SIGDN_FILESYSPATH = 0x80058000,
SIGDN_NORMALDISPLAY = 0,
SIGDN_PARENTRELATIVE = 0x80080001,
SIGDN_PARENTRELATIVEEDITING = 0x80031001,
SIGDN_PARENTRELATIVEFORADDRESSBAR = 0x8007c001,
SIGDN_PARENTRELATIVEPARSING = 0x80018001,
SIGDN_URL = 0x80068000
}
[Flags]
private enum FOS
{
FOS_OVERWRITEPROMPT = 0x2,
FOS_STRICTFILETYPES = 0x4,
FOS_NOCHANGEDIR = 0x8,
FOS_PICKFOLDERS = 0x20,
FOS_FORCEFILESYSTEM = 0x40,
FOS_ALLNONSTORAGEITEMS = 0x80,
FOS_NOVALIDATE = 0x100,
FOS_ALLOWMULTISELECT = 0x200,
FOS_PATHMUSTEXIST = 0x800,
FOS_FILEMUSTEXIST = 0x1000,
FOS_CREATEPROMPT = 0x2000,
FOS_SHAREAWARE = 0x4000,
FOS_NOREADONLYRETURN = 0x8000,
FOS_NOTESTFILECREATE = 0x10000,
FOS_HIDEMRUPLACES = 0x20000,
FOS_HIDEPINNEDPLACES = 0x40000,
FOS_NODEREFERENCELINKS = 0x100000,
FOS_OKBUTTONNEEDSINTERACTION = 0x200000,
FOS_DONTADDTORECENT = 0x2000000,
FOS_FORCESHOWHIDDEN = 0x10000000,
FOS_DEFAULTNOMINIMODE = 0x20000000,
FOS_FORCEPREVIEWPANEON = 0x40000000,
FOS_SUPPORTSTREAMABLEITEMS = unchecked((int)0x80000000)
}
#pragma warning restore CA1712 // Do not prefix enum values with type name
}
}

View File

@@ -0,0 +1,45 @@
using System;
using OMI.Formats.Pck;
namespace PckStudio.Core
{
public sealed class PackInfo
{
public static readonly PackInfo Empty = new PackInfo(default, default, default);
public bool IsValid { get; }
public PckFile File { get; }
public OMI.Endianness Endianness { get; }
//public enum PackType
//{
// Unknown = -1,
// SkinPack,
// TexturePack,
// MashUpPack
//}
//public PackType Type { get; }
public bool AllowEndianSwap { get; }
public static PackInfo Create(PckFile file, OMI.Endianness endianness, bool allowEndianSwap)
{
return new PackInfo(file, endianness, allowEndianSwap);
}
private PackInfo(PckFile file, OMI.Endianness endianness, bool allowEndianSwap)
{
File = file;
Endianness = endianness;
AllowEndianSwap = allowEndianSwap;
//Type = GetPackType();
IsValid = file is not null && Enum.IsDefined(typeof(OMI.Endianness), endianness); // && Type != PackType.Unknown;
}
//private PackType GetPackType()
//{
// return PackType.SkinPack;
//}
}
}

View File

@@ -0,0 +1,148 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" Sdk="Microsoft.NET.Sdk" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{345EABED-F0D1-4D04-B409-BABDEF747352}</ProjectGuid>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>PckStudio.Core</RootNamespace>
<AssemblyName>PckStudio.Core</AssemblyName>
<TargetFrameworkVersion>v4.8</TargetFrameworkVersion>
<TargetFramework>net48</TargetFramework>
<UseWindowsForms>true</UseWindowsForms>
<EnableDefaultCompileItems>false</EnableDefaultCompileItems>
<LangVersion>12</LangVersion>
<FileAlignment>512</FileAlignment>
<Deterministic>true</Deterministic>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<ItemGroup>
<Reference Include="System.Core" />
<Reference Include="System.Numerics" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Data" />
<Reference Include="System.Net.Http" />
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<Compile Include="Animation.cs" />
<Compile Include="App\SettingsManager.cs" />
<Compile Include="App\Updater.cs" />
<Compile Include="BoundingBox.cs" />
<Compile Include="DelegatedFileSaveContext.cs" />
<Compile Include="DelegatedSaveContext.cs" />
<Compile Include="Deserializer\AnimationDeserializer.cs" />
<Compile Include="Deserializer\ImageDeserializer.cs" />
<Compile Include="Extensions\AnimationExtensions.cs" />
<Compile Include="Extensions\BlendMode.cs" />
<Compile Include="Extensions\BoundingBoxExtensions.cs" />
<Compile Include="Extensions\ColorExtensions.cs" />
<Compile Include="Extensions\CursorExtensions.cs" />
<Compile Include="Extensions\EnumerableExtensions.cs" />
<Compile Include="Extensions\GraphicsExtensions.cs" />
<Compile Include="Extensions\ImageExtensions.cs" />
<Compile Include="Extensions\ImageLayoutDirection.cs" />
<Compile Include="Extensions\ImageSection.cs" />
<Compile Include="Extensions\ListExtensions.cs" />
<Compile Include="Extensions\LocFileExtensions.cs" />
<Compile Include="Extensions\MaterialContainerExtensions.cs" />
<Compile Include="Extensions\MaterialExtensions.cs" />
<Compile Include="Extensions\MathExtensions.cs" />
<Compile Include="Extensions\ModelBoxExtension.cs" />
<Compile Include="Extensions\OpenTKExtensions.cs" />
<Compile Include="Extensions\PckAssetExtensions.cs" />
<Compile Include="Extensions\PictureBoxExtensions.cs" />
<Compile Include="Extensions\SkinBOXExtensions.cs" />
<Compile Include="Extensions\SkinExtensions.cs" />
<Compile Include="Extensions\System.Numerics.cs" />
<Compile Include="Extensions\TreeNodeExtensions.cs" />
<Compile Include="Extensions\TreeViewExtensions.cs" />
<Compile Include="FileDialogFilter.cs" />
<Compile Include="FileFormats\PckAudioFile.cs" />
<Compile Include="GameConstants.cs" />
<Compile Include="Interfaces\IEditor.cs" />
<Compile Include="Interfaces\IModelImportProvider.cs" />
<Compile Include="Interfaces\IPckAssetDeserializer.cs" />
<Compile Include="Interfaces\IPckAssetSerializer.cs" />
<Compile Include="Interfaces\ISaveContext.cs" />
<Compile Include="Interfaces\ITryGetSet.cs" />
<Compile Include="IO\3DST\3DSTextureReader.cs" />
<Compile Include="IO\3DST\3DSTextureWriter.cs" />
<Compile Include="IO\3DST\TextureCodec.cs" />
<Compile Include="IO\PckAudio\PckAudioFileReader.cs" />
<Compile Include="IO\PckAudio\PckAudioFileWriter.cs" />
<Compile Include="IO\TGA\TGADataTypeCode.cs" />
<Compile Include="IO\TGA\TGADeserializer.cs" />
<Compile Include="IO\TGA\TGAException.cs" />
<Compile Include="IO\TGA\TGAExtentionData.cs" />
<Compile Include="IO\TGA\TGAFileData.cs" />
<Compile Include="IO\TGA\TGAFooter.cs" />
<Compile Include="IO\TGA\TGAHeader.cs" />
<Compile Include="IO\TGA\TGAReader.cs" />
<Compile Include="IO\TGA\TGASerializer.cs" />
<Compile Include="IO\TGA\TGAWriter.cs" />
<Compile Include="ItemSelectionPopUp.cs">
<SubType>Form</SubType>
</Compile>
<Compile Include="ItemSelectionPopUp.Designer.cs" />
<Compile Include="Json\ColorEntry.cs" />
<Compile Include="Json\EntityInfo.cs" />
<Compile Include="Json\TileInfo.cs" />
<Compile Include="Json\UpdateInformation.cs" />
<Compile Include="Misc\FileCacher.cs" />
<Compile Include="Misc\OpenFolderDialog.cs" />
<Compile Include="PackInfo.cs" />
<Compile Include="ResourceCategory.cs" />
<Compile Include="ResourceLocation.cs" />
<Compile Include="Serializer\AnimationSerializer.cs" />
<Compile Include="Serializer\ImageSerializer.cs" />
<Compile Include="Skin\Skin.cs" />
<Compile Include="Skin\SkinANIM.cs" />
<Compile Include="Skin\SkinAnimFlag.cs" />
<Compile Include="Skin\SkinAnimMask.cs" />
<Compile Include="Skin\SkinBOX.cs" />
<Compile Include="Skin\SkinIdentifier.cs" />
<Compile Include="Skin\SkinMetaData.cs" />
<Compile Include="Skin\SkinModel.cs" />
<Compile Include="Skin\SkinPartOffset.cs" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="AnimatedGif" Version="1.0.5" />
<PackageReference Include="DiscordRichPresence" Version="1.6.1.70" />
<PackageReference Include="Newtonsoft.Json">
<Version>13.0.3</Version>
</PackageReference>
<PackageReference Include="Autoupdater.NET.Official">
<Version>1.9.2</Version>
</PackageReference>
<PackageReference Include="OpenTK" Version="3.3.3" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Vendor\OMI-Lib\OMI Filetypes Library\OMI Filetype Library.csproj">
<Project>{693aebc1-293d-4df0-bcae-26a1099fe7bb}</Project>
<Name>OMI Filetype Library</Name>
</ProjectReference>
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>

View File

@@ -0,0 +1,39 @@
/* Copyright (c) 2023-present miku-666
* This software is provided 'as-is', without any express or implied
* warranty. In no event will the authors be held liable for any damages
* arising from the use of this software.
*
* Permission is granted to anyone to use this software for any purpose,
* including commercial applications, and to alter it and redistribute it
* freely, subject to the following restrictions:
*
* 1. The origin of this software must not be misrepresented; you must not
* claim that you wrote the original software. If you use this software
* in a product, an acknowledgment in the product documentation would be
* appreciated but is not required.
* 2. Altered source versions must be plainly marked as such, and must not be
* misrepresented as being the original software.
* 3. This notice may not be removed or altered from any source distribution.
**/
namespace PckStudio.Core
{
public enum ResourceCategory
{
Unknown = -1,
ItemAnimation,
BlockAnimation,
MobEntityTextures,
ItemEntityTextures,
ItemAtlas,
BlockAtlas,
ParticleAtlas,
BannerAtlas,
PaintingAtlas,
ExplosionAtlas,
ExperienceOrbAtlas,
MoonPhaseAtlas,
MapIconAtlas,
AdditionalMapIconsAtlas,
}
}

View File

@@ -0,0 +1,129 @@
/* Copyright (c) 2024-present miku-666
* This software is provided 'as-is', without any express or implied
* warranty. In no event will the authors be held liable for any damages
* arising from the use of this software.
*
* Permission is granted to anyone to use this software for any purpose,
* including commercial applications, and to alter it and redistribute it
* freely, subject to the following restrictions:
*
* 1.The origin of this software must not be misrepresented; you must not
* claim that you wrote the original software. If you use this software
* in a product, an acknowledgment in the product documentation would be
* appreciated but is not required.
* 2. Altered source versions must be plainly marked as such, and must not be
* misrepresented as being the original software.
* 3. This notice may not be removed or altered from any source distribution.
**/
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
namespace PckStudio.Core
{
public sealed class ResourceLocation
{
private static List<ResourceLocation> ResourceGroups = new List<ResourceLocation>();
private static readonly ResourceLocation Unknown = new ResourceLocation(string.Empty, ResourceCategory.Unknown, -1);
private static readonly Dictionary<string, ResourceLocation> _categoryLookUp = new Dictionary<string, ResourceLocation>()
{
["textures/items"] = new ResourceLocation("textures/items", ResourceCategory.ItemAnimation, 16, isGroup: true),
["textures/blocks"] = new ResourceLocation("textures/blocks", ResourceCategory.BlockAnimation, 16, isGroup: true),
["mob"] = new ResourceLocation("mob", ResourceCategory.MobEntityTextures, 1, isGroup: true),
["item"] = new ResourceLocation("item", ResourceCategory.ItemEntityTextures, 1, isGroup: true),
["terrain.png"] = new ResourceLocation("terrain.png", ResourceCategory.BlockAtlas, 16),
["items.png"] = new ResourceLocation("items.png", ResourceCategory.ItemAtlas, 16),
["particles.png"] = new ResourceLocation("particles.png", ResourceCategory.ParticleAtlas, 16),
["item/banner/Banner_Atlas.png"] = new ResourceLocation("item/banner/Banner_Atlas.png", ResourceCategory.BannerAtlas, new Size(6, 7), TillingMode.WidthAndHeight),
["art/kz.png"] = new ResourceLocation("art/kz.png", ResourceCategory.PaintingAtlas, 16),
["misc/explosion.png"] = new ResourceLocation("misc/explosion.png", ResourceCategory.ExplosionAtlas, 4),
["item/xporb.png"] = new ResourceLocation("item/xporb.png", ResourceCategory.ExperienceOrbAtlas, 4),
["terrain/moon_phases.png"] = new ResourceLocation("terrain/moon_phases.png", ResourceCategory.MoonPhaseAtlas, 4),
["misc/mapicons.png"] = new ResourceLocation("misc/mapicons.png", ResourceCategory.MapIconAtlas, 4),
["misc/additionalmapicons.png"] = new ResourceLocation("misc/additionalmapicons.png", ResourceCategory.AdditionalMapIconsAtlas, 4),
};
public static string GetPathFromCategory(ResourceCategory category)
{
return category switch
{
ResourceCategory.ItemAnimation => _categoryLookUp["textures/items"].ToString(),
ResourceCategory.BlockAnimation => _categoryLookUp["textures/blocks"].ToString(),
ResourceCategory.MobEntityTextures => _categoryLookUp["mob"].ToString(),
ResourceCategory.ItemEntityTextures => _categoryLookUp["item"].ToString(),
ResourceCategory.BlockAtlas => _categoryLookUp["terrain.png"].ToString(),
ResourceCategory.ItemAtlas => _categoryLookUp["items.png"].ToString(),
ResourceCategory.ParticleAtlas => _categoryLookUp["particles.png"].ToString(),
ResourceCategory.BannerAtlas => _categoryLookUp["item/banner/Banner_Atlas.png"].ToString(),
ResourceCategory.PaintingAtlas => _categoryLookUp["art/kz.png"].ToString(),
ResourceCategory.ExplosionAtlas => _categoryLookUp["misc/explosion.png"].ToString(),
ResourceCategory.ExperienceOrbAtlas => _categoryLookUp["item/xporb.png"].ToString(),
ResourceCategory.MoonPhaseAtlas => _categoryLookUp["terrain/moon_phases.png"].ToString(),
ResourceCategory.MapIconAtlas => _categoryLookUp["misc/mapicons.png"].ToString(),
ResourceCategory.AdditionalMapIconsAtlas => _categoryLookUp["misc/additionalmapicons.png"].ToString(),
_ => string.Empty
};
}
public static ResourceCategory GetCategoryFromPath(string path) => GetFromPath(path).Category;
public static ResourceLocation GetFromPath(string path)
{
if (string.IsNullOrWhiteSpace(path) || !path.StartsWith("res/"))
return Unknown;
string categoryPath = path.Substring("res/".Length);
if (_categoryLookUp.ContainsKey(categoryPath))
return _categoryLookUp[categoryPath];
return ResourceGroups.Where(group => categoryPath.StartsWith(group.Path)).FirstOrDefault() ?? Unknown;
}
public enum TillingMode
{
Width,
Height,
WidthAndHeight
}
public readonly string Path;
public readonly ResourceCategory Category;
public readonly Size TillingFactor;
public readonly TillingMode Tilling;
public readonly bool IsGroup;
public Size GetTileArea(Size imgSize)
{
int tileFactorWidth = Math.Max(1, TillingFactor.Width);
int tileFactorHeight = Math.Max(1, TillingFactor.Height);
return Tilling switch
{
TillingMode.Width => new Size(imgSize.Width / tileFactorWidth, imgSize.Width / tileFactorHeight),
TillingMode.Height => new Size(imgSize.Height / tileFactorWidth, imgSize.Height / tileFactorHeight),
TillingMode.WidthAndHeight => new Size(imgSize.Width / tileFactorWidth, imgSize.Height / tileFactorHeight),
_ => Size.Empty,
};
}
private ResourceLocation(string path, ResourceCategory category, int tillingFactor, TillingMode tilling = TillingMode.Width, bool isGroup = false)
: this(path, category, new Size(tillingFactor, tillingFactor), tilling, isGroup)
{
}
private ResourceLocation(string path, ResourceCategory category, Size tillingFactor, TillingMode tilling = TillingMode.Width, bool isGroup = false)
{
Path = path;
Category = category;
TillingFactor = tillingFactor;
Tilling = tilling;
IsGroup = isGroup;
if (isGroup)
ResourceGroups.Add(this);
}
public override string ToString()
{
return "res/" + Path;
}
}
}

View File

@@ -0,0 +1,81 @@
/* Copyright (c) 2024-present miku-666
* This software is provided 'as-is', without any express or implied
* warranty. In no event will the authors be held liable for any damages
* arising from the use of this software.
*
* Permission is granted to anyone to use this software for any purpose,
* including commercial applications, and to alter it and redistribute it
* freely, subject to the following restrictions:
*
* 1.The origin of this software must not be misrepresented; you must not
* claim that you wrote the original software. If you use this software
* in a product, an acknowledgment in the product documentation would be
* appreciated but is not required.
* 2. Altered source versions must be plainly marked as such, and must not be
* misrepresented as being the original software.
* 3. This notice may not be removed or altered from any source distribution.
**/
using System.Collections.Generic;
using System;
using System.Drawing;
using System.Text;
using OMI.Formats.Pck;
using PckStudio.Core.Extensions;
using PckStudio.Interfaces;
using PckStudio.Core;
using System.Linq;
using Newtonsoft.Json.Linq;
using System.Windows.Forms;
namespace PckStudio.Core.Serializer
{
public sealed class AnimationSerializer : IPckAssetSerializer<Animation>
{
public static readonly AnimationSerializer DefaultSerializer = new AnimationSerializer();
public void Serialize(Animation animation, ref PckAsset asset)
{
string anim = SerializeAnim(animation);
asset.SetProperty("ANIM", anim);
Image texture = SerializeTexture(animation);
asset.SetTexture(texture);
}
private static string SerializeAnim(Animation animation)
{
StringBuilder stringBuilder = new StringBuilder(animation.Interpolate ? "#" : string.Empty);
foreach (Animation.Frame frame in animation.GetFrames())
stringBuilder.Append($"{animation.GetTextureIndex(frame.Texture)}*{frame.Ticks},");
return stringBuilder.ToString(0, stringBuilder.Length - 1);
}
public static Image SerializeTexture(Animation animation)
{
IReadOnlyCollection<Image> textures = animation.GetTextures();
Size size = textures.First().Size;
if (size.Width != size.Height)
throw new Exception("Invalid size");
return textures.Combine(ImageLayoutDirection.Vertical);
}
public static JObject SerializeJavaAnimation(Animation animation)
{
JObject janimation = new JObject();
JObject mcmeta = new JObject();
mcmeta["comment"] = $"Animation converted with {Application.ProductName}";
mcmeta["animation"] = janimation;
JArray jframes = new JArray();
foreach (Animation.Frame frame in animation.GetFrames())
{
JObject jframe = new JObject();
jframe["index"] = animation.GetTextureIndex(frame.Texture);
jframe["time"] = frame.Ticks;
jframes.Add(jframe);
}
janimation["interpolation"] = animation.Interpolate;
janimation["frames"] = jframes;
return mcmeta;
}
}
}

View File

@@ -0,0 +1,51 @@
/* Copyright (c) 2024-present miku-666
* This software is provided 'as-is', without any express or implied
* warranty. In no event will the authors be held liable for any damages
* arising from the use of this software.
*
* Permission is granted to anyone to use this software for any purpose,
* including commercial applications, and to alter it and redistribute it
* freely, subject to the following restrictions:
*
* 1.The origin of this software must not be misrepresented; you must not
* claim that you wrote the original software. If you use this software
* in a product, an acknowledgment in the product documentation would be
* appreciated but is not required.
* 2. Altered source versions must be plainly marked as such, and must not be
* misrepresented as being the original software.
* 3. This notice may not be removed or altered from any source distribution.
**/
using System;
using System.Diagnostics;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using OMI.Formats.Pck;
using PckStudio.Core.IO.TGA;
using PckStudio.Interfaces;
namespace PckStudio.Core.Serializer
{
internal sealed class ImageSerializer : IPckAssetSerializer<Image>
{
public static readonly ImageSerializer DefaultSerializer = new ImageSerializer();
public void Serialize(Image obj, ref PckAsset asset)
{
var stream = new MemoryStream();
try
{
if (Path.GetExtension(asset.Filename) == ".tga")
TGASerializer.SerializeToStream(stream, obj);
else
obj.Save(stream, ImageFormat.Png);
asset.SetData(stream.ToArray());
}
catch (Exception ex)
{
Trace.TraceError($"Failed to serialize image to pck file data({asset.Filename}).");
Debug.WriteLine(ex.Message);
}
}
}
}

View File

@@ -0,0 +1,53 @@
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace PckStudio.Core.Skin
{
public sealed class Skin
{
public SkinMetaData MetaData { get; set; }
public SkinIdentifier Identifier { get; set; }
public SkinANIM Anim { get; set; }
public SkinModel Model { get; set; }
public Image Texture { get; set; }
public Image CapeTexture { get; set; }
public bool HasCape => CapeTexture is not null;
public Skin(string name, Image texture)
{
MetaData = new SkinMetaData(name, string.Empty);
Texture = texture;
Model = new SkinModel();
}
public Skin(string name, Image texture, Image capeTexture)
: this(name, texture)
{
CapeTexture = capeTexture;
}
public Skin(string name, SkinANIM anim, Image texture, IEnumerable<SkinBOX> additionalBoxes, IEnumerable<SkinPartOffset> partOffsets)
: this(name, texture)
{
Model.AdditionalBoxes.AddRange(additionalBoxes);
Model.PartOffsets.AddRange(partOffsets);
Anim = anim;
}
public Skin(string name, int id, Image texture, SkinANIM anim, IEnumerable<SkinBOX> additionalBoxes, IEnumerable<SkinPartOffset> partOffsets)
: this(name, anim, texture, additionalBoxes, partOffsets)
{
Identifier = new(id);
}
}
}

View File

@@ -0,0 +1,115 @@
/* Copyright (c) 2022-present miku-666, MattNL
* This software is provided 'as-is', without any express or implied
* warranty. In no event will the authors be held liable for any damages
* arising from the use of this software.
*
* Permission is granted to anyone to use this software for any purpose,
* including commercial applications, and to alter it and redistribute it
* freely, subject to the following restrictions:
*
* 1. The origin of this software must not be misrepresented; you must not
* claim that you wrote the original software. If you use this software
* in a product, an acknowledgment in the product documentation would be
* appreciated but is not required.
* 2. Altered source versions must be plainly marked as such, and must not be
* misrepresented as being the original software.
* 3. This notice may not be removed or altered from any source distribution.
**/
using System;
using System.Collections.Specialized;
using System.Text.RegularExpressions;
namespace PckStudio.Core.Skin
{
/// <summary>
/// Represents a Skin Anim value where flags can be set
/// </summary>
public class SkinANIM : ICloneable, IEquatable<SkinANIM>, IEquatable<SkinAnimMask>
{
public static readonly SkinANIM Empty = new SkinANIM(0);
private BitVector32 _flags;
private static readonly Regex _validator = new Regex(@"^0x[0-9a-f]{1,8}\b", RegexOptions.IgnoreCase);
public SkinANIM(SkinAnimMask mask)
: this((int)mask)
{
}
private SkinANIM(int mask)
{
_flags = new BitVector32(mask);
}
public override string ToString() => "0x" + _flags.Data.ToString("x8");
public static bool IsValidANIM(string anim)
{
return !string.IsNullOrWhiteSpace(anim) && _validator.IsMatch(anim);
}
public static SkinANIM FromString(string value)
=> IsValidANIM(value)
? new SkinANIM(Convert.ToInt32(value.TrimEnd(' ', '\n', '\r'), 16))
: Empty;
public static SkinANIM operator |(SkinANIM @this, SkinANIM other) => new SkinANIM(@this._flags.Data | other._flags.Data);
public static SkinANIM operator |(SkinANIM @this, SkinAnimMask mask) => new SkinANIM(@this._flags.Data | (int)mask);
public static SkinANIM operator &(SkinANIM @this, SkinAnimMask mask) => new SkinANIM(@this._flags.Data & (int)mask);
public static SkinANIM FromValue(int value) => new SkinANIM(value);
public int ToValue() => _flags.Data;
public static implicit operator SkinANIM(SkinAnimMask mask) => new SkinANIM(mask);
public static bool operator ==(SkinANIM @this, SkinAnimMask mask) => @this.Equals(mask);
public static bool operator !=(SkinANIM @this, SkinAnimMask mask) => !@this.Equals(mask);
public static bool operator ==(SkinANIM @this, SkinANIM other) => @this.Equals(other);
public static bool operator !=(SkinANIM @this, SkinANIM other) => !@this.Equals(other);
public bool Equals(SkinANIM other)
{
return _flags.Data == other._flags.Data;
}
public bool Equals(SkinAnimMask other)
{
return _flags.Data == (int)other;
}
public override bool Equals(object obj) => obj is SkinANIM a && Equals(a);
public override int GetHashCode() => _flags.Data;
/// <summary>
/// Sets the desired flag in the bitfield
/// </summary>
/// <param name="flag">ANIM Flag to set</param>
/// <param name="state">State of the flag</param>
public SkinANIM SetFlag(SkinAnimFlag flag, bool state)
{
if (!Enum.IsDefined(typeof(SkinAnimFlag), flag))
throw new ArgumentOutOfRangeException(nameof(flag));
return new SkinANIM(state ? _flags.Data | 1 << (int)flag : _flags.Data & ~(1 << (int)flag));
}
/// <summary>
/// Gets flag state
/// </summary>
/// <param name="flag">Flag to check</param>
/// <returns>True if flag is set, otherwise false</returns>
public bool GetFlag(SkinAnimFlag flag)
{
if (!Enum.IsDefined(typeof(SkinAnimFlag), flag))
throw new ArgumentOutOfRangeException(nameof(flag));
return _flags[1 << (int)flag];
}
public object Clone()
{
return MemberwiseClone();
}
}
}

View File

@@ -0,0 +1,66 @@
/* Copyright (c) 2022-present miku-666, MattNL
* This software is provided 'as-is', without any express or implied
* warranty. In no event will the authors be held liable for any damages
* arising from the use of this software.
*
* Permission is granted to anyone to use this software for any purpose,
* including commercial applications, and to alter it and redistribute it
* freely, subject to the following restrictions:
*
* 1. The origin of this software must not be misrepresented; you must not
* claim that you wrote the original software. If you use this software
* in a product, an acknowledgment in the product documentation would be
* appreciated but is not required.
* 2. Altered source versions must be plainly marked as such, and must not be
* misrepresented as being the original software.
* 3. This notice may not be removed or altered from any source distribution.
**/
namespace PckStudio.Core.Skin
{
/// <summary>
/// For usage see <see cref="SkinANIM"/>
/// </summary>
public enum SkinAnimFlag : int
{
STATIC_ARMS = 0, // 0x01
ZOMBIE_ARMS = 1, // 0x02
STATIC_LEGS = 2, // 0x04
BAD_SANTA = 3, // 0x08
//
__BIT_4 = 4, // 0x10 - Unused??
SYNCED_LEGS = 5, // 0x20
SYNCED_ARMS = 6, // 0x40
STATUE_OF_LIBERTY = 7, // 0x80
ALL_ARMOR_DISABLED = 8, // 0x100
HEAD_BOBBING_DISABLED = 9, // 0x200
HEAD_DISABLED = 10, // 0x400
RIGHT_ARM_DISABLED = 11, // 0x800
LEFT_ARM_DISABLED = 12, // 0x1000
BODY_DISABLED = 13, // 0x2000
RIGHT_LEG_DISABLED = 14, // 0x4000
LEFT_LEG_DISABLED = 15, // 0x8000
HEAD_OVERLAY_DISABLED = 16, // 0x10000
DO_BACKWARDS_CROUCH = 17, // 0x20000
RESOLUTION_64x64 = 18, // 0x40000
SLIM_MODEL = 19, // 0x80000
LEFT_ARM_OVERLAY_DISABLED = 20, // 0x100000
RIGHT_ARM_OVERLAY_DISABLED = 21, // 0x200000
LEFT_LEG_OVERLAY_DISABLED = 22, // 0x400000
RIGHT_LEG_OVERLAY_DISABLED = 23, // 0x800000
BODY_OVERLAY_DISABLED = 24, // 0x1000000
FORCE_HEAD_ARMOR = 25, // 0x2000000
FORCE_RIGHT_ARM_ARMOR = 26, // 0x4000000
FORCE_LEFT_ARM_ARMOR = 27, // 0x8000000
FORCE_BODY_ARMOR = 28, // 0x10000000
FORCE_RIGHT_LEG_ARMOR = 29, // 0x20000000
FORCE_LEFT_LEG_ARMOR = 30, // 0x40000000
DINNERBONE = 31, // 0x80000000
}
}

View File

@@ -0,0 +1,53 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace PckStudio.Core.Skin
{
[Flags]
public enum SkinAnimMask : int
{
NONE = 0, // 0x00
STATIC_ARMS = 1 << 0, // 0x01
ZOMBIE_ARMS = 1 << 1, // 0x02
STATIC_LEGS = 1 << 2, // 0x04
BAD_SANTA = 1 << 3, // 0x08
__BIT_4 = 1 << 4, // 0x10 - Unused??
SYNCED_LEGS = 1 << 5, // 0x20
SYNCED_ARMS = 1 << 6, // 0x40
STATUE_OF_LIBERTY = 1 << 7, // 0x80
ALL_ARMOR_DISABLED = 1 << 8, // 0x100
HEAD_BOBBING_DISABLED = 1 << 9, // 0x200
HEAD_DISABLED = 1 << 10, // 0x400
RIGHT_ARM_DISABLED = 1 << 11, // 0x800
LEFT_ARM_DISABLED = 1 << 12, // 0x1000
BODY_DISABLED = 1 << 13, // 0x2000
RIGHT_LEG_DISABLED = 1 << 14, // 0x4000
LEFT_LEG_DISABLED = 1 << 15, // 0x8000
HEAD_OVERLAY_DISABLED = 1 << 16, // 0x10000
DO_BACKWARDS_CROUCH = 1 << 17, // 0x20000
RESOLUTION_64x64 = 1 << 18, // 0x40000
SLIM_MODEL = 1 << 19, // 0x80000
LEFT_ARM_OVERLAY_DISABLED = 1 << 20, // 0x100000
RIGHT_ARM_OVERLAY_DISABLED = 1 << 21, // 0x200000
LEFT_LEG_OVERLAY_DISABLED = 1 << 22, // 0x400000
RIGHT_LEG_OVERLAY_DISABLED = 1 << 23, // 0x800000
BODY_OVERLAY_DISABLED = 1 << 24, // 0x1000000
FORCE_HEAD_ARMOR = 1 << 25, // 0x2000000
FORCE_RIGHT_ARM_ARMOR = 1 << 26, // 0x4000000
FORCE_LEFT_ARM_ARMOR = 1 << 27, // 0x8000000
FORCE_BODY_ARMOR = 1 << 28, // 0x10000000
FORCE_RIGHT_LEG_ARMOR = 1 << 29, // 0x20000000
FORCE_LEFT_LEG_ARMOR = 1 << 30, // 0x40000000
DINNERBONE = 1 << 31, // 0x80000000
}
}

View File

@@ -0,0 +1,180 @@
/* Copyright (c) 2023-present miku-666, MattNL
* This software is provided 'as-is', without any express or implied
* warranty. In no event will the authors be held liable for any damages
* arising from the use of this software.
*
* Permission is granted to anyone to use this software for any purpose,
* including commercial applications, and to alter it and redistribute it
* freely, subject to the following restrictions:
*
* 1.The origin of this software must not be misrepresented; you must not
* claim that you wrote the original software. If you use this software
* in a product, an acknowledgment in the product documentation would be
* appreciated but is not required.
* 2. Altered source versions must be plainly marked as such, and must not be
* misrepresented as being the original software.
* 3. This notice may not be removed or altered from any source distribution.
**/
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Numerics;
using PckStudio.Core.Extensions;
namespace PckStudio.Core.Skin
{
public record SkinBOX : IEquatable<SkinBOX>
{
public static readonly SkinBOX DefaultHead = new SkinBOX("HEAD", new Vector3(-4, -8, -4), new Vector3(8), Vector2.Zero);
public static readonly string[] BaseTypes = new string[]
{
"HEAD",
"BODY",
"ARM0",
"ARM1",
"LEG0",
"LEG1",
};
public static readonly string[] OverlayTypes = new string[]
{
"HEADWEAR",
"JACKET",
"SLEEVE0",
"SLEEVE1",
"PANTS0",
"PANTS1",
};
public static Dictionary<int, SkinAnimFlag> KnownHashes = new Dictionary<int, SkinAnimFlag>()
{
[unchecked((int)0x9560320c)] = SkinAnimFlag.HEAD_DISABLED, // HEAD -4 -8 -4 8 8 8 0 0 0 0 0
[unchecked((int)0x1f13e4a3)] = SkinAnimFlag.BODY_DISABLED, // BODY -4 0 -2 8 12 4 16 16 0 0 0
[unchecked((int)0x407c9b27)] = SkinAnimFlag.RIGHT_ARM_DISABLED, // ARM0 -3 -2 -2 4 12 4 40 16 0 0 0 // standard (64x64)
[unchecked((int)0x867c9b27)] = SkinAnimFlag.RIGHT_ARM_DISABLED, // ARM0 -2 -2 -2 3 12 4 40 16 0 0 0 // slim
[unchecked((int)0xca3cf050)] = SkinAnimFlag.LEFT_ARM_DISABLED, // ARM1 -1 -2 -2 4 12 4 40 16 0 1 0 // classic (64x32)
[unchecked((int)0x879b27)] = SkinAnimFlag.LEFT_ARM_DISABLED, // ARM1 -1 -2 -2 4 12 4 32 48 0 0 0 // standard (64x64)
[unchecked((int)0xe8c79b27)] = SkinAnimFlag.LEFT_ARM_DISABLED, // ARM1 -1 -2 -2 3 12 4 32 48 0 0 0 // slim
[unchecked((int)0x1910e24a)] = SkinAnimFlag.LEFT_LEG_DISABLED, // LEG1 -2 0 -2 4 12 4 16 48 0 0 0 // 64x64
[unchecked((int)0xce263773)] = SkinAnimFlag.LEFT_LEG_DISABLED, // LEG1 -2 0 -2 4 12 4 0 16 0 1 0 // 64x32
[unchecked((int)0x5da5e24a)] = SkinAnimFlag.RIGHT_LEG_DISABLED, // LEG0 -2 0 -2 4 12 4 0 16 0 0 0
[unchecked((int)0x4bfe0142)] = SkinAnimFlag.HEAD_OVERLAY_DISABLED, // HEADWEAR -4 -8 -4 8 8 8 32 0 0 0 0
// ------------------------------------------------------------------------------------------------------------------------------------
[unchecked((int)0xe693e4a3)] = SkinAnimFlag.BODY_OVERLAY_DISABLED, // BODY -4 0 -2 8 12 4 16 32 0 0 0
[unchecked((int)0x8e322609)] = SkinAnimFlag.BODY_OVERLAY_DISABLED, // JACKET -4 0 -2 8 12 4 16 32 0 0 0
[unchecked((int)0x860c4433)] = SkinAnimFlag.RIGHT_ARM_OVERLAY_DISABLED, // SLEEVE0 -3 -2 -2 4 12 4 40 32 0 0 0 // classic
[unchecked((int)0xcc0c4433)] = SkinAnimFlag.RIGHT_ARM_OVERLAY_DISABLED, // SLEEVE0 -2 -2 -2 3 12 4 40 32 0 0 0 // slim
[unchecked((int)0x91407908)] = SkinAnimFlag.LEFT_ARM_OVERLAY_DISABLED, // SLEEVE1 -1 -2 -2 4 12 4 48 48 0 0 0 // classic
[unchecked((int)0x79807908)] = SkinAnimFlag.LEFT_ARM_OVERLAY_DISABLED, // SLEEVE1 -1 -2 -2 3 12 4 48 48 0 0 0 // slim
[unchecked((int)0x4de0238a)] = SkinAnimFlag.RIGHT_LEG_OVERLAY_DISABLED, // PANTS0 -2 0 -2 4 12 4 0 32 0 0 0
[unchecked((int)0x176f238a)] = SkinAnimFlag.LEFT_LEG_OVERLAY_DISABLED, // PANTS1 -2 0 -2 4 12 4 0 48 0 0 0
};
public static readonly string[] ValidBoxTypes = BaseTypes.Concat(OverlayTypes).ToArray();
public string Type { get; }
public Vector3 Pos { get; }
public Vector3 Size { get; }
public Vector2 UV { get; }
public bool HideWithArmor { get; }
public bool Mirror { get; }
public float Scale { get; }
public SkinBOX(string type, Vector3 pos, Vector3 size, Vector2 uv,
bool hideWithArmor = false, bool mirror = false, float scale = 0.0f)
{
Type = type;
Pos = pos;
Size = size;
UV = uv;
HideWithArmor = hideWithArmor;
Mirror = mirror;
Scale = scale;
}
public static SkinBOX FromString(string value)
{
var arguments = value.TrimEnd('\n', '\r', ' ').Split(' ');
if (arguments.Length < 9)
{
throw new ArgumentException("Arguments must have at least a length of 9");
}
var type = arguments[0];
Vector3 pos = TryGetVector3(arguments, 1);
Vector3 size = TryGetVector3(arguments, 4);
Vector2 uv = TryGetVector2(arguments, 7);
bool hideWithArmor = arguments.IndexInRange(9) && arguments[9] == "1";
bool mirror = arguments.IndexInRange(10) && arguments[10] == "1";
float scale = default;
if (arguments.IndexInRange(11))
float.TryParse(arguments[11], out scale);
return new SkinBOX(type, pos, size, uv, hideWithArmor, mirror, scale);
}
public bool IsValidType() => IsValidType(Type);
public static bool IsValidType(string type) => ValidBoxTypes.Contains(type);
public bool IsBasePart() => IsBasePart(Type);
public static bool IsBasePart(string type) => BaseTypes.Contains(type);
public bool IsOverlayPart() => IsOverlayPart(Type);
public static bool IsOverlayPart(string type) => OverlayTypes.Contains(type);
public KeyValuePair<string, string> ToProperty()
{
return new KeyValuePair<string, string>("BOX", ToString());
}
public override string ToString()
{
return
$"{Type} {Pos.X} {Pos.Y} {Pos.Z} {Size.X} {Size.Y} {Size.Z} {UV.X} {UV.Y} {Convert.ToInt32(HideWithArmor)} {Convert.ToInt32(Mirror)} {Scale}"
.Replace(',', '.');
}
private static Vector2 TryGetVector2(string[] arguments, int startIndex)
{
float.TryParse(arguments[startIndex], out float x);
float.TryParse(arguments[startIndex + 1], out float y);
return new Vector2(x, y);
}
private static Vector3 TryGetVector3(string[] arguments, int startIndex)
{
Vector2 xy = TryGetVector2(arguments, startIndex);
float.TryParse(arguments[startIndex + 2], out float z);
return new Vector3(xy, z);
}
public override int GetHashCode()
{
int hashCode = -1311939065;
hashCode = hashCode * -1521134295 + EqualityComparer<string>.Default.GetHashCode(Type);
hashCode = hashCode * -1521134295 + Pos.GetHashCode();
hashCode = hashCode * -1521134295 + Size.GetHashCode();
hashCode = hashCode * -1521134295 + UV.GetHashCode();
hashCode = hashCode * -1521134295 + HideWithArmor.GetHashCode();
hashCode = hashCode * -1521134295 + Mirror.GetHashCode();
hashCode = hashCode * -1521134295 + Scale.GetHashCode();
return hashCode;
}
}
}

View File

@@ -0,0 +1,23 @@
using System;
using System.Globalization;
namespace PckStudio.Core.Skin
{
public sealed class SkinIdentifier : IFormattable
{
public int Id { get; }
public SkinIdentifier(int id)
{
Id = id;
}
public static implicit operator int(SkinIdentifier _this) => _this.Id;
public string ToString(string format, IFormatProvider formatProvider) => Id.ToString(format, formatProvider);
public string ToString(string format) => Id.ToString(format, NumberFormatInfo.CurrentInfo);
public override string ToString() => Id.ToString(NumberFormatInfo.CurrentInfo);
}
}

View File

@@ -0,0 +1,14 @@
namespace PckStudio.Core.Skin
{
public sealed class SkinMetaData
{
public string Name { get; }
public string Theme { get; }
public SkinMetaData(string name, string theme)
{
Name = name;
Theme = theme;
}
}
}

View File

@@ -0,0 +1,27 @@
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Text;
using OMI.Formats.Pck;
namespace PckStudio.Core.Skin
{
public sealed class SkinModel
{
public readonly List<SkinBOX> AdditionalBoxes;
public readonly List<SkinPartOffset> PartOffsets;
public SkinModel()
{
AdditionalBoxes = new List<SkinBOX>();
PartOffsets = new List<SkinPartOffset>(5);
}
public SkinModel(IEnumerable<SkinBOX> additionalBoxes, IEnumerable<SkinPartOffset> partOffsets)
{
AdditionalBoxes = new List<SkinBOX>(additionalBoxes);
PartOffsets = new List<SkinPartOffset>(partOffsets);
}
}
}

View File

@@ -0,0 +1,81 @@
using System;
using System.Linq;
using System.IO;
using PckStudio.Core.Extensions;
using System.Diagnostics;
using System.Text.RegularExpressions;
using System.Collections.Generic;
namespace PckStudio.Core.Skin
{
public readonly struct SkinPartOffset
{
private static readonly Regex sWhitespace = new Regex(@"\s+");
public static string ReplaceWhitespace(string input, string replacement)
{
return sWhitespace.Replace(input, replacement);
}
public static readonly string[] ValidModelOffsetTypes = new string[]
{
//! See: 0x02af8a20 - 0x02af8ed8 (Wii U Editon)
"HEAD",
"BODY",
"ARM0",
"ARM1",
"LEG0",
"LEG1",
"TOOL0",
"TOOL1",
"HELMET",
"SHOULDER0",
"SHOULDER1",
"CHEST",
"WAIST",
"PANTS0",
"PANTS1",
"BOOT0",
"BOOT1",
};
public string Type { get; }
public float Value { get; }
public SkinPartOffset(string type, float value)
{
Type = type;
Value = value;
}
public static SkinPartOffset FromString(string offsetFormatString)
{
string[] offset = ReplaceWhitespace(offsetFormatString.TrimEnd('\n', '\r', ' '), ",").Split(',');
if (offset.Length < 3)
throw new InvalidDataException("Format string does not contain enough data.");
string type = offset[0];
if (!ValidModelOffsetTypes.Contains(type))
{
Debug.WriteLine($"'{type}' is an invalid offset type.", category: nameof(SkinPartOffset));
}
// Ignore => Y assumed
//if (offset[1] != "Y")
if (!float.TryParse(offset[2], out float value))
{
Debug.WriteLine($"Failed to parse y offset for: '{type}'", category: nameof(SkinPartOffset));
}
return new SkinPartOffset(type, value);
}
public KeyValuePair<string, string> ToProperty()
{
string value = $"{Type} Y {Value}";
return new KeyValuePair<string, string>("OFFSET", value.Replace(',', '.'));
}
}
}