C&C Remastered Map Editor
Initial commit of C&C Remastered Map Editor code
This commit is contained in:
parent
1f6350fe6e
commit
e37e174be1
289 changed files with 80922 additions and 7 deletions
60
CnCTDRAMapEditor/Utility/CRC.cs
Normal file
60
CnCTDRAMapEditor/Utility/CRC.cs
Normal file
|
@ -0,0 +1,60 @@
|
|||
//
|
||||
// Copyright 2020 Electronic Arts Inc.
|
||||
//
|
||||
// The Command & Conquer Map Editor and corresponding source code is free
|
||||
// software: you can redistribute it and/or modify it under the terms of
|
||||
// the GNU General Public License as published by the Free Software Foundation,
|
||||
// either version 3 of the License, or (at your option) any later version.
|
||||
|
||||
// The Command & Conquer Map Editor and corresponding source code is distributed
|
||||
// in the hope that it will be useful, but with permitted additional restrictions
|
||||
// under Section 7 of the GPL. See the GNU General Public License in LICENSE.TXT
|
||||
// distributed with this program. You should have received a copy of the
|
||||
// GNU General Public License along with permitted additional restrictions
|
||||
// with this program. If not, see https://github.com/electronicarts/CnC_Remastered_Collection
|
||||
using System;
|
||||
|
||||
namespace MobiusEditor.Utility
|
||||
{
|
||||
public class CRC
|
||||
{
|
||||
static CRC()
|
||||
{
|
||||
for (var i = 0U; i < 256U; ++i)
|
||||
{
|
||||
uint crc = i;
|
||||
for (var j = 0U; j < 8U; ++j)
|
||||
{
|
||||
if ((crc & 1U) != 0U)
|
||||
{
|
||||
crc = (crc >> 1) ^ polynomial;
|
||||
}
|
||||
else
|
||||
{
|
||||
crc >>= 1;
|
||||
}
|
||||
}
|
||||
crcTable[i] = crc;
|
||||
}
|
||||
}
|
||||
|
||||
public static uint Calculate(byte[] bytes)
|
||||
{
|
||||
if (bytes == null)
|
||||
{
|
||||
throw new ArgumentNullException("bytes");
|
||||
}
|
||||
|
||||
uint remainder = 0xFFFFFFFFU;
|
||||
for (var i = 0; i < bytes.Length; ++i)
|
||||
{
|
||||
uint index = (remainder & 0xFF) ^ bytes[i];
|
||||
remainder = (remainder >> 8) ^ crcTable[index];
|
||||
}
|
||||
return ~remainder;
|
||||
}
|
||||
|
||||
private static readonly uint[] crcTable = new uint[256];
|
||||
private const uint polynomial = 0xEDB88320;
|
||||
}
|
||||
}
|
216
CnCTDRAMapEditor/Utility/ExtensionMethods.cs
Normal file
216
CnCTDRAMapEditor/Utility/ExtensionMethods.cs
Normal file
|
@ -0,0 +1,216 @@
|
|||
//
|
||||
// Copyright 2020 Electronic Arts Inc.
|
||||
//
|
||||
// The Command & Conquer Map Editor and corresponding source code is free
|
||||
// software: you can redistribute it and/or modify it under the terms of
|
||||
// the GNU General Public License as published by the Free Software Foundation,
|
||||
// either version 3 of the License, or (at your option) any later version.
|
||||
|
||||
// The Command & Conquer Map Editor and corresponding source code is distributed
|
||||
// in the hope that it will be useful, but with permitted additional restrictions
|
||||
// under Section 7 of the GPL. See the GNU General Public License in LICENSE.TXT
|
||||
// distributed with this program. You should have received a copy of the
|
||||
// GNU General Public License along with permitted additional restrictions
|
||||
// with this program. If not, see https://github.com/electronicarts/CnC_Remastered_Collection
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Drawing;
|
||||
using System.Drawing.Imaging;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace MobiusEditor.Utility
|
||||
{
|
||||
public static class ExtensionMethods
|
||||
{
|
||||
public static float ToLinear(this float v)
|
||||
{
|
||||
return (v < 0.04045f) ? (v * 25.0f / 323.0f) : (float)Math.Pow(((200.0f * v) + 11.0f) / 211.0f, 12.0f / 5.0f);
|
||||
}
|
||||
|
||||
public static float ToLinear(this byte v)
|
||||
{
|
||||
return (v / 255.0f).ToLinear();
|
||||
}
|
||||
|
||||
public static float ToSRGB(this float v)
|
||||
{
|
||||
return (v < 0.0031308) ? (v * 323.0f / 25.0f) : ((((float)Math.Pow(v, 5.0f / 12.0f) * 211.0f) - 11.0f) / 200.0f);
|
||||
}
|
||||
|
||||
public static void SetDefault<T>(this T data)
|
||||
{
|
||||
var properties = data.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance).Where(p => p.GetSetMethod() != null);
|
||||
foreach (var property in properties)
|
||||
{
|
||||
if (property.GetCustomAttribute(typeof(DefaultValueAttribute)) is DefaultValueAttribute defaultValueAttr)
|
||||
{
|
||||
property.SetValue(data, defaultValueAttr.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void CopyTo<T>(this T data, T other)
|
||||
{
|
||||
var properties = data.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance).Where(p => (p.GetSetMethod() != null) && (p.GetGetMethod() != null));
|
||||
foreach (var property in properties)
|
||||
{
|
||||
var defaultValueAttr = property.GetCustomAttribute(typeof(DefaultValueAttribute)) as DefaultValueAttribute;
|
||||
property.SetValue(other, property.GetValue(data));
|
||||
}
|
||||
}
|
||||
|
||||
public static IEnumerable<Point> Points(this Rectangle rectangle)
|
||||
{
|
||||
for (var y = rectangle.Top; y < rectangle.Bottom; ++y)
|
||||
{
|
||||
for (var x = rectangle.Left; x < rectangle.Right; ++x)
|
||||
{
|
||||
yield return new Point(x, y);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static IEnumerable<T> Yield<T>(this T item)
|
||||
{
|
||||
yield return item;
|
||||
}
|
||||
|
||||
public static HashSet<T> ToHashSet<T>(this IEnumerable<T> source, IEqualityComparer<T> comparer = null)
|
||||
{
|
||||
return new HashSet<T>(source, comparer);
|
||||
}
|
||||
|
||||
public static IEnumerable<byte[]> Split(this byte[] bytes, int length)
|
||||
{
|
||||
for (int i = 0; i < bytes.Length; i += length)
|
||||
{
|
||||
yield return bytes.Skip(i).Take(Math.Min(length, bytes.Length - i)).ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
public static IEnumerable<string> Split(this string str, int length)
|
||||
{
|
||||
for (int i = 0; i < str.Length; i += length)
|
||||
{
|
||||
yield return str.Substring(i, Math.Min(length, str.Length - i));
|
||||
}
|
||||
}
|
||||
|
||||
public static Font GetAdjustedFont(this Graphics graphics, string text, Font originalFont, int width, int minSize, int maxSize, bool smallestOnFail)
|
||||
{
|
||||
if (minSize > maxSize)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException("minSize");
|
||||
}
|
||||
|
||||
for (var size = maxSize; size >= minSize; --size)
|
||||
{
|
||||
var font = new Font(originalFont.Name, size, originalFont.Style);
|
||||
var textSize = graphics.MeasureString(text, font);
|
||||
|
||||
if (width > Convert.ToInt32(textSize.Width))
|
||||
{
|
||||
return font;
|
||||
}
|
||||
}
|
||||
|
||||
return smallestOnFail ? new Font(originalFont.Name, minSize, originalFont.Style) : originalFont;
|
||||
}
|
||||
|
||||
public static Bitmap Sharpen(this Bitmap bitmap, double strength)
|
||||
{
|
||||
var sharpenImage = bitmap.Clone() as Bitmap;
|
||||
|
||||
int width = bitmap.Width;
|
||||
int height = bitmap.Height;
|
||||
|
||||
// Create sharpening filter.
|
||||
const int filterSize = 5;
|
||||
|
||||
var filter = new double[,]
|
||||
{
|
||||
{-1, -1, -1, -1, -1},
|
||||
{-1, 2, 2, 2, -1},
|
||||
{-1, 2, 16, 2, -1},
|
||||
{-1, 2, 2, 2, -1},
|
||||
{-1, -1, -1, -1, -1}
|
||||
};
|
||||
|
||||
double bias = 1.0 - strength;
|
||||
double factor = strength / 16.0;
|
||||
|
||||
const int s = filterSize / 2;
|
||||
|
||||
var result = new Color[bitmap.Width, bitmap.Height];
|
||||
|
||||
// Lock image bits for read/write.
|
||||
if (sharpenImage != null)
|
||||
{
|
||||
BitmapData pbits = sharpenImage.LockBits(new Rectangle(0, 0, width, height), ImageLockMode.ReadWrite, PixelFormat.Format24bppRgb);
|
||||
|
||||
// Declare an array to hold the bytes of the bitmap.
|
||||
int bytes = pbits.Stride * height;
|
||||
var rgbValues = new byte[bytes];
|
||||
|
||||
// Copy the RGB values into the array.
|
||||
Marshal.Copy(pbits.Scan0, rgbValues, 0, bytes);
|
||||
|
||||
int rgb;
|
||||
// Fill the color array with the new sharpened color values.
|
||||
for (int x = s; x < width - s; x++)
|
||||
{
|
||||
for (int y = s; y < height - s; y++)
|
||||
{
|
||||
double red = 0.0, green = 0.0, blue = 0.0;
|
||||
|
||||
for (int filterX = 0; filterX < filterSize; filterX++)
|
||||
{
|
||||
for (int filterY = 0; filterY < filterSize; filterY++)
|
||||
{
|
||||
int imageX = (x - s + filterX + width) % width;
|
||||
int imageY = (y - s + filterY + height) % height;
|
||||
|
||||
rgb = imageY * pbits.Stride + 3 * imageX;
|
||||
|
||||
red += rgbValues[rgb + 2] * filter[filterX, filterY];
|
||||
green += rgbValues[rgb + 1] * filter[filterX, filterY];
|
||||
blue += rgbValues[rgb + 0] * filter[filterX, filterY];
|
||||
}
|
||||
|
||||
rgb = y * pbits.Stride + 3 * x;
|
||||
|
||||
int r = Math.Min(Math.Max((int)(factor * red + (bias * rgbValues[rgb + 2])), 0), 255);
|
||||
int g = Math.Min(Math.Max((int)(factor * green + (bias * rgbValues[rgb + 1])), 0), 255);
|
||||
int b = Math.Min(Math.Max((int)(factor * blue + (bias * rgbValues[rgb + 0])), 0), 255);
|
||||
|
||||
result[x, y] = Color.FromArgb(r, g, b);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update the image with the sharpened pixels.
|
||||
for (int x = s; x < width - s; x++)
|
||||
{
|
||||
for (int y = s; y < height - s; y++)
|
||||
{
|
||||
rgb = y * pbits.Stride + 3 * x;
|
||||
|
||||
rgbValues[rgb + 2] = result[x, y].R;
|
||||
rgbValues[rgb + 1] = result[x, y].G;
|
||||
rgbValues[rgb + 0] = result[x, y].B;
|
||||
}
|
||||
}
|
||||
|
||||
// Copy the RGB values back to the bitmap.
|
||||
Marshal.Copy(rgbValues, 0, pbits.Scan0, bytes);
|
||||
// Release image bits.
|
||||
sharpenImage.UnlockBits(pbits);
|
||||
}
|
||||
|
||||
return sharpenImage;
|
||||
}
|
||||
}
|
||||
}
|
57
CnCTDRAMapEditor/Utility/GameTextManager.cs
Normal file
57
CnCTDRAMapEditor/Utility/GameTextManager.cs
Normal file
|
@ -0,0 +1,57 @@
|
|||
//
|
||||
// Copyright 2020 Electronic Arts Inc.
|
||||
//
|
||||
// The Command & Conquer Map Editor and corresponding source code is free
|
||||
// software: you can redistribute it and/or modify it under the terms of
|
||||
// the GNU General Public License as published by the Free Software Foundation,
|
||||
// either version 3 of the License, or (at your option) any later version.
|
||||
|
||||
// The Command & Conquer Map Editor and corresponding source code is distributed
|
||||
// in the hope that it will be useful, but with permitted additional restrictions
|
||||
// under Section 7 of the GPL. See the GNU General Public License in LICENSE.TXT
|
||||
// distributed with this program. You should have received a copy of the
|
||||
// GNU General Public License along with permitted additional restrictions
|
||||
// with this program. If not, see https://github.com/electronicarts/CnC_Remastered_Collection
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
|
||||
namespace MobiusEditor.Utility
|
||||
{
|
||||
public class GameTextManager
|
||||
{
|
||||
private readonly Dictionary<string, string> gameText = new Dictionary<string, string>();
|
||||
|
||||
public string this[string textId] => gameText.TryGetValue(textId, out string text) ? text : textId;
|
||||
|
||||
public GameTextManager(MegafileManager megafileManager, string gameTextFile)
|
||||
{
|
||||
using (var stream = megafileManager.Open(gameTextFile))
|
||||
using (var reader = new BinaryReader(stream))
|
||||
using (var unicodeReader = new BinaryReader(stream, Encoding.Unicode))
|
||||
using (var asciiReader = new BinaryReader(stream, Encoding.ASCII))
|
||||
{
|
||||
var numStrings = reader.ReadUInt32();
|
||||
var stringSizes = new (uint textSize, uint idSize)[numStrings];
|
||||
var strings = new string[numStrings];
|
||||
|
||||
for (var i = 0; i < numStrings; ++i)
|
||||
{
|
||||
reader.ReadUInt32();
|
||||
stringSizes[i] = (reader.ReadUInt32(), reader.ReadUInt32());
|
||||
}
|
||||
|
||||
for (var i = 0; i < numStrings; ++i)
|
||||
{
|
||||
strings[i] = new string(unicodeReader.ReadChars((int)stringSizes[i].textSize));
|
||||
}
|
||||
|
||||
for (var i = 0; i < numStrings; ++i)
|
||||
{
|
||||
var textId = new string(asciiReader.ReadChars((int)stringSizes[i].idSize));
|
||||
gameText[textId] = strings[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
569
CnCTDRAMapEditor/Utility/INI.cs
Normal file
569
CnCTDRAMapEditor/Utility/INI.cs
Normal file
|
@ -0,0 +1,569 @@
|
|||
//
|
||||
// Copyright 2020 Electronic Arts Inc.
|
||||
//
|
||||
// The Command & Conquer Map Editor and corresponding source code is free
|
||||
// software: you can redistribute it and/or modify it under the terms of
|
||||
// the GNU General Public License as published by the Free Software Foundation,
|
||||
// either version 3 of the License, or (at your option) any later version.
|
||||
|
||||
// The Command & Conquer Map Editor and corresponding source code is distributed
|
||||
// in the hope that it will be useful, but with permitted additional restrictions
|
||||
// under Section 7 of the GPL. See the GNU General Public License in LICENSE.TXT
|
||||
// distributed with this program. You should have received a copy of the
|
||||
// GNU General Public License along with permitted additional restrictions
|
||||
// with this program. If not, see https://github.com/electronicarts/CnC_Remastered_Collection
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
using System.ComponentModel;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace MobiusEditor.Utility
|
||||
{
|
||||
static class INIHelpers
|
||||
{
|
||||
public static readonly Regex SectionRegex = new Regex(@"^\s*\[([^\]]*)\]", RegexOptions.Compiled);
|
||||
public static readonly Regex KeyValueRegex = new Regex(@"^\s*(.*?)\s*=([^;]*)", RegexOptions.Compiled);
|
||||
public static readonly Regex CommentRegex = new Regex(@"^\s*(#|;)", RegexOptions.Compiled);
|
||||
|
||||
public static readonly Func<INIDiffType, string> DiffPrefix = t =>
|
||||
{
|
||||
switch (t)
|
||||
{
|
||||
case INIDiffType.Added:
|
||||
return "+";
|
||||
case INIDiffType.Removed:
|
||||
return "-";
|
||||
case INIDiffType.Updated:
|
||||
return "@";
|
||||
}
|
||||
return string.Empty;
|
||||
};
|
||||
}
|
||||
|
||||
public class INIKeyValueCollection : IEnumerable<(string Key, string Value)>, IEnumerable
|
||||
{
|
||||
private readonly OrderedDictionary KeyValues;
|
||||
|
||||
public string this[string key]
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!KeyValues.Contains(key))
|
||||
{
|
||||
throw new KeyNotFoundException(key);
|
||||
}
|
||||
return KeyValues[key] as string;
|
||||
}
|
||||
set
|
||||
{
|
||||
if (key == null)
|
||||
{
|
||||
throw new ArgumentNullException("key");
|
||||
}
|
||||
KeyValues[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
public INIKeyValueCollection()
|
||||
{
|
||||
KeyValues = new OrderedDictionary(StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
public int Count => KeyValues.Count;
|
||||
|
||||
public bool Contains(string key) => KeyValues.Contains(key);
|
||||
|
||||
public T Get<T>(string key) where T : struct
|
||||
{
|
||||
var converter = TypeDescriptor.GetConverter(typeof(T));
|
||||
return (T)converter.ConvertFromString(this[key]);
|
||||
}
|
||||
|
||||
public void Set<T>(string key, T value) where T : struct
|
||||
{
|
||||
var converter = TypeDescriptor.GetConverter(typeof(T));
|
||||
this[key] = converter.ConvertToString(value);
|
||||
}
|
||||
|
||||
public bool Remove(string key)
|
||||
{
|
||||
if (!KeyValues.Contains(key))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
KeyValues.Remove(key);
|
||||
return true;
|
||||
}
|
||||
|
||||
public IEnumerator<(string Key, string Value)> GetEnumerator()
|
||||
{
|
||||
foreach (DictionaryEntry entry in KeyValues)
|
||||
{
|
||||
yield return (entry.Key as string, entry.Value as string);
|
||||
}
|
||||
}
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator()
|
||||
{
|
||||
return GetEnumerator();
|
||||
}
|
||||
}
|
||||
|
||||
public class INISection : IEnumerable<(string Key, string Value)>, IEnumerable
|
||||
{
|
||||
public readonly INIKeyValueCollection Keys;
|
||||
|
||||
public string Name { get; private set; }
|
||||
|
||||
public string this[string key] { get => Keys[key]; set => Keys[key] = value; }
|
||||
|
||||
public bool Empty => Keys.Count == 0;
|
||||
|
||||
public INISection(string name)
|
||||
{
|
||||
Keys = new INIKeyValueCollection();
|
||||
Name = name;
|
||||
}
|
||||
|
||||
public void Parse(TextReader reader)
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
var line = reader.ReadLine();
|
||||
if (line == null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
var m = INIHelpers.KeyValueRegex.Match(line);
|
||||
if (m.Success)
|
||||
{
|
||||
Keys[m.Groups[1].Value] = m.Groups[2].Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Parse(string iniText)
|
||||
{
|
||||
using (var reader = new StringReader(iniText))
|
||||
{
|
||||
Parse(reader);
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerator<(string Key, string Value)> GetEnumerator()
|
||||
{
|
||||
return Keys.GetEnumerator();
|
||||
}
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator()
|
||||
{
|
||||
return GetEnumerator();
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
var lines = new List<string>(Keys.Count);
|
||||
foreach (var item in Keys)
|
||||
{
|
||||
lines.Add(string.Format("{0}={1}", item.Key, item.Value));
|
||||
}
|
||||
return string.Join(Environment.NewLine, lines);
|
||||
}
|
||||
}
|
||||
|
||||
public class INISectionCollection : IEnumerable<INISection>, IEnumerable
|
||||
{
|
||||
private readonly OrderedDictionary Sections;
|
||||
|
||||
public INISection this[string name] => Sections.Contains(name) ? (Sections[name] as INISection) : null;
|
||||
|
||||
public INISectionCollection()
|
||||
{
|
||||
Sections = new OrderedDictionary(StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
public int Count => Sections.Count;
|
||||
|
||||
public bool Contains(string section) => Sections.Contains(section);
|
||||
|
||||
public INISection Add(string name)
|
||||
{
|
||||
if (!Sections.Contains(name))
|
||||
{
|
||||
var section = new INISection(name);
|
||||
Sections[name] = section;
|
||||
}
|
||||
return this[name];
|
||||
}
|
||||
|
||||
public bool Add(INISection section)
|
||||
{
|
||||
if ((section == null) || Sections.Contains(section.Name))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
Sections[section.Name] = section;
|
||||
return true;
|
||||
}
|
||||
|
||||
public void AddRange(IEnumerable<INISection> sections)
|
||||
{
|
||||
foreach (var section in sections)
|
||||
{
|
||||
Add(section);
|
||||
}
|
||||
}
|
||||
|
||||
public bool Remove(string name)
|
||||
{
|
||||
if (!Sections.Contains(name))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
Sections.Remove(name);
|
||||
return true;
|
||||
}
|
||||
|
||||
public INISection Extract(string name)
|
||||
{
|
||||
if (!Sections.Contains(name))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
var section = this[name];
|
||||
Sections.Remove(name);
|
||||
return section;
|
||||
}
|
||||
|
||||
public IEnumerator<INISection> GetEnumerator()
|
||||
{
|
||||
foreach (DictionaryEntry entry in Sections)
|
||||
{
|
||||
yield return entry.Value as INISection;
|
||||
}
|
||||
}
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator()
|
||||
{
|
||||
return GetEnumerator();
|
||||
}
|
||||
}
|
||||
|
||||
public partial class INI : IEnumerable<INISection>, IEnumerable
|
||||
{
|
||||
public readonly INISectionCollection Sections;
|
||||
|
||||
public INISection this[string name] { get => Sections[name]; }
|
||||
|
||||
public INI()
|
||||
{
|
||||
Sections = new INISectionCollection();
|
||||
}
|
||||
|
||||
public void Parse(TextReader reader)
|
||||
{
|
||||
INISection currentSection = null;
|
||||
|
||||
while (true)
|
||||
{
|
||||
var line = reader.ReadLine();
|
||||
if (line == null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
var m = INIHelpers.SectionRegex.Match(line);
|
||||
if (m.Success)
|
||||
{
|
||||
currentSection = Sections.Add(m.Groups[1].Value);
|
||||
}
|
||||
|
||||
if (currentSection != null)
|
||||
{
|
||||
if (INIHelpers.CommentRegex.Match(line).Success)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
currentSection.Parse(line);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Parse(string iniText)
|
||||
{
|
||||
using (var reader = new StringReader(iniText))
|
||||
{
|
||||
Parse(reader);
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerator<INISection> GetEnumerator()
|
||||
{
|
||||
foreach (var section in Sections)
|
||||
{
|
||||
yield return section;
|
||||
}
|
||||
}
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator()
|
||||
{
|
||||
return GetEnumerator();
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
var sections = new List<string>(Sections.Count);
|
||||
foreach (var item in Sections)
|
||||
{
|
||||
var lines = new List<string>
|
||||
{
|
||||
string.Format("[{0}]", item.Name)
|
||||
};
|
||||
if (!item.Empty)
|
||||
{
|
||||
lines.Add(item.ToString());
|
||||
}
|
||||
sections.Add(string.Join(Environment.NewLine, lines));
|
||||
}
|
||||
return string.Join(Environment.NewLine + Environment.NewLine, sections) + Environment.NewLine;
|
||||
}
|
||||
}
|
||||
|
||||
[Flags]
|
||||
public enum INIDiffType
|
||||
{
|
||||
None = 0,
|
||||
Added = 1,
|
||||
Removed = 2,
|
||||
Updated = 4,
|
||||
AddedOrUpdated = 5
|
||||
}
|
||||
|
||||
public class INISectionDiff : IEnumerable<string>, IEnumerable
|
||||
{
|
||||
public readonly INIDiffType Type;
|
||||
|
||||
private readonly Dictionary<string, INIDiffType> keyDiff;
|
||||
|
||||
public INIDiffType this[string key]
|
||||
{
|
||||
get
|
||||
{
|
||||
INIDiffType diffType;
|
||||
if (!keyDiff.TryGetValue(key, out diffType))
|
||||
{
|
||||
return INIDiffType.None;
|
||||
}
|
||||
return diffType;
|
||||
}
|
||||
}
|
||||
|
||||
private INISectionDiff()
|
||||
{
|
||||
keyDiff = new Dictionary<string, INIDiffType>();
|
||||
Type = INIDiffType.None;
|
||||
}
|
||||
|
||||
internal INISectionDiff(INIDiffType type, INISection section)
|
||||
: this()
|
||||
{
|
||||
foreach (var keyValue in section.Keys)
|
||||
{
|
||||
keyDiff[keyValue.Key] = type;
|
||||
}
|
||||
|
||||
Type = type;
|
||||
}
|
||||
|
||||
internal INISectionDiff(INISection leftSection, INISection rightSection)
|
||||
: this(INIDiffType.Removed, leftSection)
|
||||
{
|
||||
foreach (var keyValue in rightSection.Keys)
|
||||
{
|
||||
var key = keyValue.Key;
|
||||
if (keyDiff.ContainsKey(key))
|
||||
{
|
||||
if (leftSection[key] == rightSection[key])
|
||||
{
|
||||
keyDiff.Remove(key);
|
||||
}
|
||||
else
|
||||
{
|
||||
keyDiff[key] = INIDiffType.Updated;
|
||||
Type = INIDiffType.Updated;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
keyDiff[key] = INIDiffType.Added;
|
||||
Type = INIDiffType.Updated;
|
||||
}
|
||||
}
|
||||
|
||||
Type = (keyDiff.Count > 0) ? INIDiffType.Updated : INIDiffType.None;
|
||||
}
|
||||
|
||||
public IEnumerator<string> GetEnumerator()
|
||||
{
|
||||
return keyDiff.Keys.GetEnumerator();
|
||||
}
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator()
|
||||
{
|
||||
return GetEnumerator();
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
foreach (var item in keyDiff)
|
||||
{
|
||||
sb.AppendLine(string.Format("{0} {1}", INIHelpers.DiffPrefix(item.Value), item.Key));
|
||||
}
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
public class INIDiff : IEnumerable<string>, IEnumerable
|
||||
{
|
||||
private readonly Dictionary<string, INISectionDiff> sectionDiffs;
|
||||
|
||||
public INISectionDiff this[string key]
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!sectionDiffs.TryGetValue(key, out INISectionDiff sectionDiff))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
return sectionDiff;
|
||||
}
|
||||
}
|
||||
|
||||
private INIDiff()
|
||||
{
|
||||
sectionDiffs = new Dictionary<string, INISectionDiff>(StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
public INIDiff(INI leftIni, INI rightIni)
|
||||
: this()
|
||||
{
|
||||
foreach (var leftSection in leftIni)
|
||||
{
|
||||
sectionDiffs[leftSection.Name] = rightIni.Sections.Contains(leftSection.Name) ?
|
||||
new INISectionDiff(leftSection, rightIni[leftSection.Name]) :
|
||||
new INISectionDiff(INIDiffType.Removed, leftSection);
|
||||
}
|
||||
|
||||
foreach (var rightSection in rightIni)
|
||||
{
|
||||
if (!leftIni.Sections.Contains(rightSection.Name))
|
||||
{
|
||||
sectionDiffs[rightSection.Name] = new INISectionDiff(INIDiffType.Added, rightSection);
|
||||
}
|
||||
}
|
||||
|
||||
sectionDiffs = sectionDiffs.Where(x => x.Value.Type != INIDiffType.None).ToDictionary(x => x.Key, x => x.Value);
|
||||
}
|
||||
|
||||
public bool Contains(string key) => sectionDiffs.ContainsKey(key);
|
||||
|
||||
public IEnumerator<string> GetEnumerator()
|
||||
{
|
||||
return sectionDiffs.Keys.GetEnumerator();
|
||||
}
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator()
|
||||
{
|
||||
return GetEnumerator();
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
foreach (var item in sectionDiffs)
|
||||
{
|
||||
sb.AppendLine(string.Format("{0} {1}", INIHelpers.DiffPrefix(item.Value.Type), item.Key));
|
||||
using (var reader = new StringReader(item.Value.ToString()))
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
var line = reader.ReadLine();
|
||||
if (line == null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
sb.AppendLine(string.Format("\t{0}", line));
|
||||
}
|
||||
}
|
||||
}
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
[AttributeUsage(AttributeTargets.Property)]
|
||||
public class NonSerializedINIKeyAttribute : Attribute
|
||||
{
|
||||
}
|
||||
|
||||
public partial class INI
|
||||
{
|
||||
public static void ParseSection<T>(ITypeDescriptorContext context, INISection section, T data)
|
||||
{
|
||||
var propertyDescriptors = TypeDescriptor.GetProperties(data);
|
||||
var properties = data.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance).Where(p => p.GetSetMethod() != null);
|
||||
foreach (var property in properties)
|
||||
{
|
||||
if (property.GetCustomAttribute<NonSerializedINIKeyAttribute>() != null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (section.Keys.Contains(property.Name))
|
||||
{
|
||||
var converter = propertyDescriptors.Find(property.Name, false)?.Converter ?? TypeDescriptor.GetConverter(property.PropertyType);
|
||||
if (converter.CanConvertFrom(context, typeof(string)))
|
||||
{
|
||||
property.SetValue(data, converter.ConvertFromString(context, section[property.Name]));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void WriteSection<T>(ITypeDescriptorContext context, INISection section, T data)
|
||||
{
|
||||
var propertyDescriptors = TypeDescriptor.GetProperties(data);
|
||||
var properties = data.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance).Where(p => p.GetGetMethod() != null);
|
||||
foreach (var property in properties)
|
||||
{
|
||||
if (property.GetCustomAttribute<NonSerializedINIKeyAttribute>() != null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var value = property.GetValue(data);
|
||||
if (property.PropertyType.IsValueType || (value != null))
|
||||
{
|
||||
var converter = propertyDescriptors.Find(property.Name, false)?.Converter ?? TypeDescriptor.GetConverter(property.PropertyType);
|
||||
if (converter.CanConvertTo(context, typeof(string)))
|
||||
{
|
||||
section[property.Name] = converter.ConvertToString(context, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void ParseSection<T>(INISection section, T data) => ParseSection(null, section, data);
|
||||
|
||||
public static void WriteSection<T>(INISection section, T data) => WriteSection(null, section, data);
|
||||
}
|
||||
}
|
135
CnCTDRAMapEditor/Utility/MRU.cs
Normal file
135
CnCTDRAMapEditor/Utility/MRU.cs
Normal file
|
@ -0,0 +1,135 @@
|
|||
//
|
||||
// Copyright 2020 Electronic Arts Inc.
|
||||
//
|
||||
// The Command & Conquer Map Editor and corresponding source code is free
|
||||
// software: you can redistribute it and/or modify it under the terms of
|
||||
// the GNU General Public License as published by the Free Software Foundation,
|
||||
// either version 3 of the License, or (at your option) any later version.
|
||||
|
||||
// The Command & Conquer Map Editor and corresponding source code is distributed
|
||||
// in the hope that it will be useful, but with permitted additional restrictions
|
||||
// under Section 7 of the GPL. See the GNU General Public License in LICENSE.TXT
|
||||
// distributed with this program. You should have received a copy of the
|
||||
// GNU General Public License along with permitted additional restrictions
|
||||
// with this program. If not, see https://github.com/electronicarts/CnC_Remastered_Collection
|
||||
using Microsoft.Win32;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace MobiusEditor.Utility
|
||||
{
|
||||
public class MRU
|
||||
{
|
||||
private readonly RegistryKey registryKey;
|
||||
private readonly int maxFiles;
|
||||
|
||||
private readonly List<FileInfo> files = new List<FileInfo>();
|
||||
|
||||
private readonly ToolStripMenuItem menu;
|
||||
private readonly ToolStripMenuItem[] fileItems;
|
||||
|
||||
public event EventHandler<FileInfo> FileSelected;
|
||||
|
||||
public MRU(string registryPath, int maxFiles, ToolStripMenuItem menu)
|
||||
{
|
||||
var subKey = Registry.CurrentUser;
|
||||
foreach (var key in registryPath.Split('\\'))
|
||||
{
|
||||
subKey = subKey.CreateSubKey(key, true);
|
||||
}
|
||||
registryKey = subKey.CreateSubKey("MRU");
|
||||
|
||||
this.maxFiles = maxFiles;
|
||||
this.menu = menu;
|
||||
this.menu.DropDownItems.Clear();
|
||||
|
||||
fileItems = new ToolStripMenuItem[maxFiles];
|
||||
for (var i = 0; i < fileItems.Length; ++i)
|
||||
{
|
||||
var fileItem = fileItems[i] = new ToolStripMenuItem();
|
||||
fileItem.Visible = false;
|
||||
menu.DropDownItems.Add(fileItem);
|
||||
}
|
||||
|
||||
LoadMRU();
|
||||
ShowMRU();
|
||||
}
|
||||
|
||||
public void Add(FileInfo file)
|
||||
{
|
||||
files.RemoveAll(f => f.FullName == file.FullName);
|
||||
files.Insert(0, file);
|
||||
|
||||
if (files.Count > maxFiles)
|
||||
{
|
||||
files.RemoveAt(files.Count - 1);
|
||||
}
|
||||
|
||||
SaveMRU();
|
||||
ShowMRU();
|
||||
}
|
||||
|
||||
public void Remove(FileInfo file)
|
||||
{
|
||||
files.RemoveAll(f => f.FullName == file.FullName);
|
||||
|
||||
SaveMRU();
|
||||
ShowMRU();
|
||||
}
|
||||
|
||||
private void LoadMRU()
|
||||
{
|
||||
files.Clear();
|
||||
for (var i = 0; i < maxFiles; ++i)
|
||||
{
|
||||
string fileName = registryKey.GetValue(i.ToString()) as string;
|
||||
if (!string.IsNullOrEmpty(fileName))
|
||||
{
|
||||
files.Add(new FileInfo(fileName));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void SaveMRU()
|
||||
{
|
||||
for (var i = 0; i < files.Count; ++i)
|
||||
{
|
||||
registryKey.SetValue(i.ToString(), files[i].FullName);
|
||||
}
|
||||
for (var i = files.Count; i < maxFiles; ++i)
|
||||
{
|
||||
registryKey.DeleteValue(i.ToString(), false);
|
||||
}
|
||||
}
|
||||
|
||||
private void ShowMRU()
|
||||
{
|
||||
for (var i = 0; i < files.Count; ++i)
|
||||
{
|
||||
var file = files[i];
|
||||
var fileItem = fileItems[i];
|
||||
|
||||
fileItem.Text = string.Format("&{0} {1}", i + 1, file.FullName);
|
||||
fileItem.Tag = file;
|
||||
fileItem.Click -= FileItem_Click;
|
||||
fileItem.Click += FileItem_Click;
|
||||
fileItem.Visible = true;
|
||||
}
|
||||
for (var i = files.Count; i < maxFiles; ++i)
|
||||
{
|
||||
var fileItem = fileItems[i];
|
||||
|
||||
fileItem.Visible = false;
|
||||
fileItem.Click -= FileItem_Click;
|
||||
}
|
||||
menu.Enabled = files.Count > 0;
|
||||
}
|
||||
|
||||
private void FileItem_Click(object sender, EventArgs e)
|
||||
{
|
||||
FileSelected?.Invoke(this, (sender as ToolStripMenuItem).Tag as FileInfo);
|
||||
}
|
||||
}
|
||||
}
|
144
CnCTDRAMapEditor/Utility/Megafile.cs
Normal file
144
CnCTDRAMapEditor/Utility/Megafile.cs
Normal file
|
@ -0,0 +1,144 @@
|
|||
//
|
||||
// Copyright 2020 Electronic Arts Inc.
|
||||
//
|
||||
// The Command & Conquer Map Editor and corresponding source code is free
|
||||
// software: you can redistribute it and/or modify it under the terms of
|
||||
// the GNU General Public License as published by the Free Software Foundation,
|
||||
// either version 3 of the License, or (at your option) any later version.
|
||||
|
||||
// The Command & Conquer Map Editor and corresponding source code is distributed
|
||||
// in the hope that it will be useful, but with permitted additional restrictions
|
||||
// under Section 7 of the GPL. See the GNU General Public License in LICENSE.TXT
|
||||
// distributed with this program. You should have received a copy of the
|
||||
// GNU General Public License along with permitted additional restrictions
|
||||
// with this program. If not, see https://github.com/electronicarts/CnC_Remastered_Collection
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.IO.MemoryMappedFiles;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace MobiusEditor.Utility
|
||||
{
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 2)]
|
||||
struct SubFileData
|
||||
{
|
||||
public ushort Flags;
|
||||
public uint CRCValue;
|
||||
public int SubfileIndex;
|
||||
public uint SubfileSize;
|
||||
public uint SubfileImageDataOffset;
|
||||
public ushort SubfileNameIndex;
|
||||
|
||||
public static readonly uint Size = (uint)Marshal.SizeOf(typeof(SubFileData));
|
||||
}
|
||||
|
||||
public class Megafile : IEnumerable<string>, IEnumerable, IDisposable
|
||||
{
|
||||
private readonly MemoryMappedFile megafileMap;
|
||||
|
||||
private readonly string[] stringTable;
|
||||
|
||||
private readonly Dictionary<string, SubFileData> fileTable = new Dictionary<string, SubFileData>();
|
||||
|
||||
public Megafile(string megafilePath)
|
||||
{
|
||||
megafileMap = MemoryMappedFile.CreateFromFile(
|
||||
new FileStream(megafilePath, FileMode.Open, FileAccess.Read, FileShare.Read) , null, 0, MemoryMappedFileAccess.Read, HandleInheritability.None, false
|
||||
);
|
||||
|
||||
var numFiles = 0U;
|
||||
var numStrings = 0U;
|
||||
var stringTableSize = 0U;
|
||||
var fileTableSize = 0U;
|
||||
|
||||
var readOffset = 0U;
|
||||
using (var magicNumberReader = new BinaryReader(megafileMap.CreateViewStream(readOffset, 4, MemoryMappedFileAccess.Read)))
|
||||
{
|
||||
var magicNumber = magicNumberReader.ReadUInt32();
|
||||
if ((magicNumber == 0xFFFFFFFF) || (magicNumber == 0x8FFFFFFF))
|
||||
{
|
||||
// Skip header size and version
|
||||
readOffset += 8;
|
||||
}
|
||||
}
|
||||
|
||||
readOffset += 4U;
|
||||
using (var headerReader = new BinaryReader(megafileMap.CreateViewStream(readOffset, 12, MemoryMappedFileAccess.Read)))
|
||||
{
|
||||
numFiles = headerReader.ReadUInt32();
|
||||
numStrings = headerReader.ReadUInt32();
|
||||
stringTableSize = headerReader.ReadUInt32();
|
||||
fileTableSize = numFiles * SubFileData.Size;
|
||||
}
|
||||
|
||||
readOffset += 12U;
|
||||
using (var stringReader = new BinaryReader(megafileMap.CreateViewStream(readOffset, stringTableSize, MemoryMappedFileAccess.Read)))
|
||||
{
|
||||
stringTable = new string[numStrings];
|
||||
|
||||
for (var i = 0U; i < numStrings; ++i)
|
||||
{
|
||||
var stringSize = stringReader.ReadUInt16();
|
||||
stringTable[i] = new string(stringReader.ReadChars(stringSize));
|
||||
}
|
||||
}
|
||||
|
||||
readOffset += stringTableSize;
|
||||
using (var subFileAccessor = megafileMap.CreateViewAccessor(readOffset, fileTableSize, MemoryMappedFileAccess.Read))
|
||||
{
|
||||
for (var i = 0U; i < numFiles; ++i)
|
||||
{
|
||||
subFileAccessor.Read(i * SubFileData.Size, out SubFileData subFile);
|
||||
var fullName = stringTable[subFile.SubfileNameIndex];
|
||||
fileTable[fullName] = subFile;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Stream Open(string path)
|
||||
{
|
||||
if (!fileTable.TryGetValue(path, out SubFileData subFile))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return megafileMap.CreateViewStream(subFile.SubfileImageDataOffset, subFile.SubfileSize, MemoryMappedFileAccess.Read);
|
||||
}
|
||||
|
||||
public IEnumerator<string> GetEnumerator()
|
||||
{
|
||||
foreach (var file in stringTable)
|
||||
{
|
||||
yield return file;
|
||||
}
|
||||
}
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator()
|
||||
{
|
||||
return GetEnumerator();
|
||||
}
|
||||
|
||||
#region IDisposable Support
|
||||
private bool disposedValue = false;
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (!disposedValue)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
megafileMap.Dispose();
|
||||
}
|
||||
disposedValue = true;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
196
CnCTDRAMapEditor/Utility/MegafileBuilder.cs
Normal file
196
CnCTDRAMapEditor/Utility/MegafileBuilder.cs
Normal file
|
@ -0,0 +1,196 @@
|
|||
//
|
||||
// Copyright 2020 Electronic Arts Inc.
|
||||
//
|
||||
// The Command & Conquer Map Editor and corresponding source code is free
|
||||
// software: you can redistribute it and/or modify it under the terms of
|
||||
// the GNU General Public License as published by the Free Software Foundation,
|
||||
// either version 3 of the License, or (at your option) any later version.
|
||||
|
||||
// The Command & Conquer Map Editor and corresponding source code is distributed
|
||||
// in the hope that it will be useful, but with permitted additional restrictions
|
||||
// under Section 7 of the GPL. See the GNU General Public License in LICENSE.TXT
|
||||
// distributed with this program. You should have received a copy of the
|
||||
// GNU General Public License along with permitted additional restrictions
|
||||
// with this program. If not, see https://github.com/electronicarts/CnC_Remastered_Collection
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
|
||||
namespace MobiusEditor.Utility
|
||||
{
|
||||
public class MegafileBuilder : IDisposable
|
||||
{
|
||||
#region IDisposable Support
|
||||
private bool disposedValue = false;
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (!disposedValue)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
Out.Dispose();
|
||||
}
|
||||
|
||||
disposedValue = true;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
}
|
||||
#endregion
|
||||
|
||||
private const float Version = 0.99f;
|
||||
|
||||
public string RootPath { get; private set; }
|
||||
|
||||
private Stream Out { get; set; }
|
||||
|
||||
private List<(string, object)> Files = new List<(string, object)>();
|
||||
|
||||
public MegafileBuilder(string rootPath, string outFile)
|
||||
{
|
||||
RootPath = rootPath.ToUpper();
|
||||
Out = new FileStream(outFile, FileMode.Create);
|
||||
}
|
||||
|
||||
public void AddFile(string path)
|
||||
{
|
||||
if (File.Exists(path))
|
||||
{
|
||||
Files.Add((Path.GetFileName(path), path));
|
||||
}
|
||||
}
|
||||
|
||||
public void AddFile(string path, Stream stream)
|
||||
{
|
||||
Files.Add((Path.GetFileName(path), stream));
|
||||
}
|
||||
|
||||
public void AddDirectory(string path)
|
||||
{
|
||||
AddDirectory(path, "*.*");
|
||||
}
|
||||
|
||||
public void AddDirectory(string path, string searchPattern)
|
||||
{
|
||||
var uriPath = new Uri(path);
|
||||
foreach (var file in Directory.GetFiles(path, searchPattern, SearchOption.AllDirectories))
|
||||
{
|
||||
var relativePath = Uri.UnescapeDataString(uriPath.MakeRelativeUri(new Uri(file)).ToString()).Replace('/', Path.DirectorySeparatorChar);
|
||||
Files.Add((relativePath, file));
|
||||
}
|
||||
}
|
||||
|
||||
public void Write()
|
||||
{
|
||||
var headerSize = sizeof(uint) * 6U;
|
||||
headerSize += SubFileData.Size * (uint)Files.Count;
|
||||
|
||||
var strings = new List<string>();
|
||||
Func<string, ushort> stringIndex = (string value) =>
|
||||
{
|
||||
var index = strings.IndexOf(value);
|
||||
if (index < 0)
|
||||
{
|
||||
index = strings.Count;
|
||||
if (index > ushort.MaxValue)
|
||||
{
|
||||
throw new IndexOutOfRangeException();
|
||||
}
|
||||
strings.Add(value);
|
||||
}
|
||||
return (ushort)index;
|
||||
};
|
||||
|
||||
var files = new List<(ushort index, uint crc, Stream stream, bool dispose)>();
|
||||
foreach (var (filename, source) in Files)
|
||||
{
|
||||
var name = Encoding.ASCII.GetBytes(filename);
|
||||
var crc = CRC.Calculate(name);
|
||||
|
||||
if (source is string)
|
||||
{
|
||||
var file = source as string;
|
||||
if (File.Exists(file))
|
||||
{
|
||||
files.Add((stringIndex(Path.Combine(RootPath, filename).ToUpper()), crc, new FileStream(file, FileMode.Open, FileAccess.Read), true));
|
||||
}
|
||||
}
|
||||
else if (source is Stream)
|
||||
{
|
||||
files.Add((stringIndex(Path.Combine(RootPath, filename).ToUpper()), crc, source as Stream, false));
|
||||
}
|
||||
}
|
||||
files = files.OrderBy(x => x.crc).ToList();
|
||||
|
||||
var stringsSize = sizeof(ushort) * (uint)strings.Count;
|
||||
stringsSize += (uint)strings.Sum(x => x.Length);
|
||||
headerSize += stringsSize;
|
||||
|
||||
var subfileImageOffset = headerSize;
|
||||
using (var writer = new BinaryWriter(Out))
|
||||
{
|
||||
writer.Write(0xFFFFFFFF);
|
||||
writer.Write(Version);
|
||||
writer.Write(headerSize);
|
||||
|
||||
writer.Write((uint)Files.Count);
|
||||
writer.Write((uint)strings.Count);
|
||||
writer.Write(stringsSize);
|
||||
|
||||
foreach (var item in strings)
|
||||
{
|
||||
writer.Write((ushort)item.Length);
|
||||
writer.Write(item.ToCharArray());
|
||||
}
|
||||
|
||||
using (var fileStream = new MemoryStream())
|
||||
{
|
||||
for (var i = 0; i < files.Count; ++i)
|
||||
{
|
||||
var (index, crc, stream, dispose) = files[i];
|
||||
|
||||
var fileSize = (uint)(stream.Length - stream.Position);
|
||||
var fileBytes = new byte[fileSize];
|
||||
stream.Read(fileBytes, 0, fileBytes.Length);
|
||||
fileStream.Write(fileBytes, 0, fileBytes.Length);
|
||||
|
||||
if (dispose)
|
||||
{
|
||||
stream.Dispose();
|
||||
}
|
||||
|
||||
SubFileData data = new SubFileData
|
||||
{
|
||||
Flags = 0,
|
||||
CRCValue = crc,
|
||||
SubfileIndex = i,
|
||||
SubfileSize = fileSize,
|
||||
SubfileImageDataOffset = subfileImageOffset,
|
||||
SubfileNameIndex = index
|
||||
};
|
||||
|
||||
var ptr = Marshal.AllocHGlobal((int)SubFileData.Size);
|
||||
Marshal.StructureToPtr(data, ptr, false);
|
||||
var bytes = new byte[SubFileData.Size];
|
||||
Marshal.Copy(ptr, bytes, 0, bytes.Length);
|
||||
Marshal.FreeHGlobal(ptr);
|
||||
|
||||
writer.Write(bytes);
|
||||
|
||||
subfileImageOffset += data.SubfileSize;
|
||||
}
|
||||
|
||||
fileStream.Seek(0, SeekOrigin.Begin);
|
||||
fileStream.CopyTo(writer.BaseStream);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
104
CnCTDRAMapEditor/Utility/MegafileManager.cs
Normal file
104
CnCTDRAMapEditor/Utility/MegafileManager.cs
Normal file
|
@ -0,0 +1,104 @@
|
|||
//
|
||||
// Copyright 2020 Electronic Arts Inc.
|
||||
//
|
||||
// The Command & Conquer Map Editor and corresponding source code is free
|
||||
// software: you can redistribute it and/or modify it under the terms of
|
||||
// the GNU General Public License as published by the Free Software Foundation,
|
||||
// either version 3 of the License, or (at your option) any later version.
|
||||
|
||||
// The Command & Conquer Map Editor and corresponding source code is distributed
|
||||
// in the hope that it will be useful, but with permitted additional restrictions
|
||||
// under Section 7 of the GPL. See the GNU General Public License in LICENSE.TXT
|
||||
// distributed with this program. You should have received a copy of the
|
||||
// GNU General Public License along with permitted additional restrictions
|
||||
// with this program. If not, see https://github.com/electronicarts/CnC_Remastered_Collection
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
|
||||
namespace MobiusEditor.Utility
|
||||
{
|
||||
public class MegafileManager : IEnumerable<string>, IEnumerable, IDisposable
|
||||
{
|
||||
private readonly string looseFilePath;
|
||||
|
||||
private readonly List<Megafile> megafiles = new List<Megafile>();
|
||||
|
||||
private readonly HashSet<string> filenames = new HashSet<string>();
|
||||
|
||||
public MegafileManager(string looseFilePath)
|
||||
{
|
||||
this.looseFilePath = looseFilePath;
|
||||
}
|
||||
|
||||
public bool Load(string megafilePath)
|
||||
{
|
||||
if (!File.Exists(megafilePath))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var megafile = new Megafile(megafilePath);
|
||||
filenames.UnionWith(megafile);
|
||||
megafiles.Add(megafile);
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool Exists(string path)
|
||||
{
|
||||
return File.Exists(Path.Combine(looseFilePath, path)) || filenames.Contains(path.ToUpper());
|
||||
}
|
||||
|
||||
public Stream Open(string path)
|
||||
{
|
||||
string loosePath = Path.Combine(looseFilePath, path);
|
||||
if (File.Exists(loosePath))
|
||||
{
|
||||
return File.Open(loosePath, FileMode.Open, FileAccess.Read);
|
||||
}
|
||||
|
||||
foreach (var megafile in megafiles)
|
||||
{
|
||||
var stream = megafile.Open(path.ToUpper());
|
||||
if (stream != null)
|
||||
{
|
||||
return stream;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public IEnumerator<string> GetEnumerator()
|
||||
{
|
||||
return filenames.GetEnumerator();
|
||||
}
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator()
|
||||
{
|
||||
return GetEnumerator();
|
||||
}
|
||||
|
||||
#region IDisposable Support
|
||||
private bool disposedValue = false;
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (!disposedValue)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
megafiles.ForEach(m => m.Dispose());
|
||||
}
|
||||
disposedValue = true;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
210
CnCTDRAMapEditor/Utility/PropertyTracker.cs
Normal file
210
CnCTDRAMapEditor/Utility/PropertyTracker.cs
Normal file
|
@ -0,0 +1,210 @@
|
|||
//
|
||||
// Copyright 2020 Electronic Arts Inc.
|
||||
//
|
||||
// The Command & Conquer Map Editor and corresponding source code is free
|
||||
// software: you can redistribute it and/or modify it under the terms of
|
||||
// the GNU General Public License as published by the Free Software Foundation,
|
||||
// either version 3 of the License, or (at your option) any later version.
|
||||
|
||||
// The Command & Conquer Map Editor and corresponding source code is distributed
|
||||
// in the hope that it will be useful, but with permitted additional restrictions
|
||||
// under Section 7 of the GPL. See the GNU General Public License in LICENSE.TXT
|
||||
// distributed with this program. You should have received a copy of the
|
||||
// GNU General Public License along with permitted additional restrictions
|
||||
// with this program. If not, see https://github.com/electronicarts/CnC_Remastered_Collection
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Dynamic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
|
||||
namespace MobiusEditor.Utility
|
||||
{
|
||||
public class TrackablePropertyDescriptor<T> : PropertyDescriptor
|
||||
{
|
||||
private readonly T obj;
|
||||
private readonly PropertyInfo propertyInfo;
|
||||
private readonly Dictionary<string, object> propertyValues;
|
||||
|
||||
public override Type ComponentType => obj.GetType();
|
||||
|
||||
public override bool IsReadOnly => false;
|
||||
|
||||
public override Type PropertyType => propertyInfo.PropertyType;
|
||||
|
||||
public TrackablePropertyDescriptor(string name, T obj, PropertyInfo propertyInfo, Dictionary<string, object> propertyValues)
|
||||
: base(name, null)
|
||||
{
|
||||
this.obj = obj;
|
||||
this.propertyInfo = propertyInfo;
|
||||
this.propertyValues = propertyValues;
|
||||
}
|
||||
|
||||
public override bool CanResetValue(object component)
|
||||
{
|
||||
return propertyValues.ContainsKey(Name);
|
||||
}
|
||||
|
||||
public override object GetValue(object component)
|
||||
{
|
||||
if (propertyValues.TryGetValue(Name, out object result))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
return propertyInfo.GetValue(obj);
|
||||
}
|
||||
|
||||
public override void ResetValue(object component)
|
||||
{
|
||||
propertyValues.Remove(Name);
|
||||
}
|
||||
|
||||
public override void SetValue(object component, object value)
|
||||
{
|
||||
if (Equals(propertyInfo.GetValue(obj), value))
|
||||
{
|
||||
propertyValues.Remove(Name);
|
||||
}
|
||||
else
|
||||
{
|
||||
propertyValues[Name] = value;
|
||||
}
|
||||
}
|
||||
|
||||
public override bool ShouldSerializeValue(object component)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public class PropertyTracker<T> : DynamicObject, ICustomTypeDescriptor
|
||||
{
|
||||
private readonly Dictionary<string, PropertyInfo> trackableProperties;
|
||||
private readonly Dictionary<string, object> propertyValues = new Dictionary<string, object>();
|
||||
|
||||
public T Object { get; private set; }
|
||||
|
||||
public PropertyTracker(T obj)
|
||||
{
|
||||
Object = obj;
|
||||
|
||||
trackableProperties = Object.GetType()
|
||||
.GetProperties(BindingFlags.Public | BindingFlags.Instance)
|
||||
.Where(p => (p.GetGetMethod() != null) && (p.GetSetMethod() != null))
|
||||
.ToDictionary(k => k.Name, v => v);
|
||||
}
|
||||
|
||||
public void Revert() => propertyValues.Clear();
|
||||
|
||||
public void Commit()
|
||||
{
|
||||
foreach (var propertyValue in propertyValues)
|
||||
{
|
||||
trackableProperties[propertyValue.Key].SetValue(Object, propertyValue.Value);
|
||||
}
|
||||
propertyValues.Clear();
|
||||
}
|
||||
|
||||
public IDictionary<string, object> GetUndoValues() => propertyValues.ToDictionary(kv => kv.Key, kv => trackableProperties[kv.Key].GetValue(Object));
|
||||
|
||||
public IDictionary<string, object> GetRedoValues() => new Dictionary<string, object>(propertyValues);
|
||||
|
||||
public override bool TryGetMember(GetMemberBinder binder, out object result)
|
||||
{
|
||||
if (!trackableProperties.TryGetValue(binder.Name, out PropertyInfo property))
|
||||
{
|
||||
result = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!propertyValues.TryGetValue(binder.Name, out result))
|
||||
{
|
||||
result = property.GetValue(Object);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public override bool TrySetMember(SetMemberBinder binder, object value)
|
||||
{
|
||||
if (!trackableProperties.TryGetValue(binder.Name, out PropertyInfo property))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Equals(property.GetValue(Object), value))
|
||||
{
|
||||
propertyValues.Remove(binder.Name);
|
||||
}
|
||||
else
|
||||
{
|
||||
propertyValues[binder.Name] = value;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public AttributeCollection GetAttributes()
|
||||
{
|
||||
return TypeDescriptor.GetAttributes(Object.GetType());
|
||||
}
|
||||
|
||||
public string GetClassName()
|
||||
{
|
||||
return TypeDescriptor.GetClassName(Object.GetType());
|
||||
}
|
||||
|
||||
public string GetComponentName()
|
||||
{
|
||||
return TypeDescriptor.GetComponentName(Object.GetType());
|
||||
}
|
||||
|
||||
public TypeConverter GetConverter()
|
||||
{
|
||||
return TypeDescriptor.GetConverter(Object.GetType());
|
||||
}
|
||||
|
||||
public EventDescriptor GetDefaultEvent()
|
||||
{
|
||||
return TypeDescriptor.GetDefaultEvent(Object.GetType());
|
||||
}
|
||||
|
||||
public PropertyDescriptor GetDefaultProperty()
|
||||
{
|
||||
return TypeDescriptor.GetDefaultProperty(Object.GetType());
|
||||
}
|
||||
|
||||
public object GetEditor(Type editorBaseType)
|
||||
{
|
||||
return TypeDescriptor.GetEditor(Object.GetType(), editorBaseType);
|
||||
}
|
||||
|
||||
public EventDescriptorCollection GetEvents()
|
||||
{
|
||||
return TypeDescriptor.GetEvents(Object.GetType());
|
||||
}
|
||||
|
||||
public EventDescriptorCollection GetEvents(Attribute[] attributes)
|
||||
{
|
||||
return TypeDescriptor.GetEvents(Object.GetType(), attributes);
|
||||
}
|
||||
|
||||
public PropertyDescriptorCollection GetProperties()
|
||||
{
|
||||
var propertyDescriptors = trackableProperties.Select(kv =>
|
||||
{
|
||||
return new TrackablePropertyDescriptor<T>(kv.Key, Object, kv.Value, propertyValues);
|
||||
}).ToArray();
|
||||
return new PropertyDescriptorCollection(propertyDescriptors);
|
||||
}
|
||||
|
||||
public PropertyDescriptorCollection GetProperties(Attribute[] attributes)
|
||||
{
|
||||
return GetProperties();
|
||||
}
|
||||
|
||||
public object GetPropertyOwner(PropertyDescriptor pd)
|
||||
{
|
||||
return Object;
|
||||
}
|
||||
}
|
||||
}
|
328
CnCTDRAMapEditor/Utility/SteamworksUGC.cs
Normal file
328
CnCTDRAMapEditor/Utility/SteamworksUGC.cs
Normal file
|
@ -0,0 +1,328 @@
|
|||
//
|
||||
// Copyright 2020 Electronic Arts Inc.
|
||||
//
|
||||
// The Command & Conquer Map Editor and corresponding source code is free
|
||||
// software: you can redistribute it and/or modify it under the terms of
|
||||
// the GNU General Public License as published by the Free Software Foundation,
|
||||
// either version 3 of the License, or (at your option) any later version.
|
||||
|
||||
// The Command & Conquer Map Editor and corresponding source code is distributed
|
||||
// in the hope that it will be useful, but with permitted additional restrictions
|
||||
// under Section 7 of the GPL. See the GNU General Public License in LICENSE.TXT
|
||||
// distributed with this program. You should have received a copy of the
|
||||
// GNU General Public License along with permitted additional restrictions
|
||||
// with this program. If not, see https://github.com/electronicarts/CnC_Remastered_Collection
|
||||
using MobiusEditor.Model;
|
||||
using Steamworks;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace MobiusEditor.Utility
|
||||
{
|
||||
public interface ISteamworksOperation : IDisposable
|
||||
{
|
||||
bool Done { get; }
|
||||
|
||||
bool Failed { get; }
|
||||
|
||||
string Status { get; }
|
||||
|
||||
void OnSuccess();
|
||||
|
||||
void OnFailed();
|
||||
}
|
||||
|
||||
public class SteamworksUGCPublishOperation : ISteamworksOperation
|
||||
{
|
||||
private readonly string ugcPath;
|
||||
private readonly IList<string> tags;
|
||||
private readonly Action onSuccess;
|
||||
private readonly Action<string> onFailed;
|
||||
|
||||
private CallResult<CreateItemResult_t> createItemResult;
|
||||
private CallResult<SubmitItemUpdateResult_t> submitItemUpdateResult;
|
||||
private SteamSection steamSection = new SteamSection();
|
||||
|
||||
public bool Done => !(createItemResult?.IsActive() ?? false) && !(submitItemUpdateResult?.IsActive() ?? false);
|
||||
|
||||
public bool Failed { get; private set; }
|
||||
|
||||
public string Status { get; private set; }
|
||||
|
||||
public SteamworksUGCPublishOperation(string ugcPath, SteamSection steamSection, IList<string> tags, Action onSuccess, Action<string> onFailed)
|
||||
{
|
||||
this.ugcPath = ugcPath;
|
||||
this.steamSection = steamSection;
|
||||
this.tags = tags;
|
||||
this.onSuccess = onSuccess;
|
||||
this.onFailed = onFailed;
|
||||
|
||||
if (steamSection.PublishedFileId == PublishedFileId_t.Invalid.m_PublishedFileId)
|
||||
{
|
||||
CreateUGCItem();
|
||||
}
|
||||
else
|
||||
{
|
||||
UpdateUGCItem();
|
||||
}
|
||||
|
||||
Status = "Publishing UGC...";
|
||||
}
|
||||
|
||||
public void OnSuccess() => onSuccess();
|
||||
|
||||
public void OnFailed() => onFailed(Status);
|
||||
|
||||
private void CreateUGCItem()
|
||||
{
|
||||
var steamAPICall = SteamUGC.CreateItem(SteamUtils.GetAppID(), EWorkshopFileType.k_EWorkshopFileTypeCommunity);
|
||||
if (steamAPICall == SteamAPICall_t.Invalid)
|
||||
{
|
||||
Failed = true;
|
||||
Status = "Publishing failed.";
|
||||
return;
|
||||
}
|
||||
|
||||
createItemResult = CallResult<CreateItemResult_t>.Create(OnCreateItemResult);
|
||||
createItemResult.Set(steamAPICall);
|
||||
}
|
||||
|
||||
private void UpdateUGCItem()
|
||||
{
|
||||
var updateHandle = SteamUGC.StartItemUpdate(SteamUtils.GetAppID(), new PublishedFileId_t(steamSection.PublishedFileId));
|
||||
if (updateHandle == UGCUpdateHandle_t.Invalid)
|
||||
{
|
||||
Failed = true;
|
||||
Status = "Publishing failed.";
|
||||
return;
|
||||
}
|
||||
|
||||
bool success = true;
|
||||
success = success && SteamUGC.SetItemContent(updateHandle, ugcPath);
|
||||
success = success && SteamUGC.SetItemPreview(updateHandle, steamSection.PreviewFile);
|
||||
success = success && SteamUGC.SetItemUpdateLanguage(updateHandle, "English");
|
||||
success = success && SteamUGC.SetItemTitle(updateHandle, steamSection.Title);
|
||||
success = success && SteamUGC.SetItemDescription(updateHandle, steamSection.Description);
|
||||
success = success && SteamUGC.SetItemVisibility(updateHandle, steamSection.Visibility);
|
||||
success = success && SteamUGC.SetItemTags(updateHandle, tags);
|
||||
if (!success)
|
||||
{
|
||||
Failed = true;
|
||||
Status = "Publishing failed.";
|
||||
return;
|
||||
}
|
||||
|
||||
var steamAPICall = SteamUGC.SubmitItemUpdate(updateHandle, "");
|
||||
if (steamAPICall == SteamAPICall_t.Invalid)
|
||||
{
|
||||
Failed = true;
|
||||
Status = "Publishing failed.";
|
||||
return;
|
||||
}
|
||||
|
||||
submitItemUpdateResult = CallResult<SubmitItemUpdateResult_t>.Create(OnSubmitItemUpdateResult);
|
||||
submitItemUpdateResult.Set(steamAPICall);
|
||||
}
|
||||
|
||||
private void OnCreateItemResult(CreateItemResult_t callback, bool ioFailure)
|
||||
{
|
||||
if (ioFailure)
|
||||
{
|
||||
Failed = true;
|
||||
Status = "Publishing failed.";
|
||||
return;
|
||||
}
|
||||
|
||||
switch (callback.m_eResult)
|
||||
{
|
||||
case EResult.k_EResultOK:
|
||||
steamSection.PublishedFileId = callback.m_nPublishedFileId.m_PublishedFileId;
|
||||
UpdateUGCItem();
|
||||
break;
|
||||
case EResult.k_EResultFileNotFound:
|
||||
Failed = true;
|
||||
Status = "UGC not found.";
|
||||
break;
|
||||
case EResult.k_EResultNotLoggedOn:
|
||||
Failed = true;
|
||||
Status = "Not logged on.";
|
||||
break;
|
||||
default:
|
||||
Failed = true;
|
||||
Status = "Publishing failed.";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnSubmitItemUpdateResult(SubmitItemUpdateResult_t callback, bool ioFailure)
|
||||
{
|
||||
if (ioFailure)
|
||||
{
|
||||
Failed = true;
|
||||
Status = "Publishing failed.";
|
||||
return;
|
||||
}
|
||||
|
||||
switch (callback.m_eResult)
|
||||
{
|
||||
case EResult.k_EResultOK:
|
||||
Status = "Done.";
|
||||
steamSection.PublishedFileId = callback.m_nPublishedFileId.m_PublishedFileId;
|
||||
break;
|
||||
case EResult.k_EResultFileNotFound:
|
||||
Failed = true;
|
||||
Status = "UGC not found.";
|
||||
break;
|
||||
case EResult.k_EResultLimitExceeded:
|
||||
Failed = true;
|
||||
Status = "Size limit exceeded.";
|
||||
break;
|
||||
default:
|
||||
Failed = true;
|
||||
Status = string.Format("Publishing failed. ({0})", callback.m_eResult);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
#region IDisposable Support
|
||||
private bool disposedValue = false;
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (!disposedValue)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
createItemResult?.Dispose();
|
||||
submitItemUpdateResult?.Dispose();
|
||||
}
|
||||
disposedValue = true;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
|
||||
public static class SteamworksUGC
|
||||
{
|
||||
public static bool IsInit { get; private set; }
|
||||
|
||||
public static ISteamworksOperation CurrentOperation { get; private set; }
|
||||
|
||||
public static string WorkshopURL
|
||||
{
|
||||
get
|
||||
{
|
||||
var app_id = IsInit ? SteamUtils.GetAppID() : AppId_t.Invalid;
|
||||
if (app_id == AppId_t.Invalid)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
return string.Format("http://steamcommunity.com/app/{0}/workshop/", app_id.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
public static bool IsSteamBuild => File.Exists("steam_appid.txt");
|
||||
|
||||
private static Callback<GameLobbyJoinRequested_t> GameLobbyJoinRequested;
|
||||
|
||||
public static bool Init()
|
||||
{
|
||||
if (IsInit)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!IsSteamBuild)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!Packsize.Test())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!DllCheck.Test())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!SteamAPI.Init())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
SteamClient.SetWarningMessageHook(new SteamAPIWarningMessageHook_t(SteamAPIDebugTextHook));
|
||||
|
||||
GameLobbyJoinRequested = Callback<GameLobbyJoinRequested_t>.Create(OnGameLobbyJoinRequested);
|
||||
|
||||
IsInit = true;
|
||||
return IsInit;
|
||||
}
|
||||
|
||||
public static void Shutdown()
|
||||
{
|
||||
if (IsInit)
|
||||
{
|
||||
GameLobbyJoinRequested?.Dispose();
|
||||
GameLobbyJoinRequested = null;
|
||||
|
||||
CurrentOperation?.Dispose();
|
||||
CurrentOperation = null;
|
||||
|
||||
SteamAPI.Shutdown();
|
||||
IsInit = false;
|
||||
}
|
||||
}
|
||||
|
||||
public static void Service()
|
||||
{
|
||||
SteamAPI.RunCallbacks();
|
||||
|
||||
if (CurrentOperation?.Done ?? false)
|
||||
{
|
||||
if (CurrentOperation.Failed)
|
||||
{
|
||||
CurrentOperation.OnFailed();
|
||||
}
|
||||
else
|
||||
{
|
||||
CurrentOperation.OnSuccess();
|
||||
}
|
||||
CurrentOperation.Dispose();
|
||||
CurrentOperation = null;
|
||||
}
|
||||
}
|
||||
|
||||
public static bool PublishUGC(string ugcPath, SteamSection steamSection, IList<string> tags, Action onSuccess, Action<string> onFailed)
|
||||
{
|
||||
if (CurrentOperation != null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
CurrentOperation = new SteamworksUGCPublishOperation(ugcPath, steamSection, tags, onSuccess, onFailed);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void SteamAPIDebugTextHook(int nSeverity, StringBuilder pchDebugText)
|
||||
{
|
||||
Debug.WriteLine(pchDebugText);
|
||||
}
|
||||
|
||||
private static void OnGameLobbyJoinRequested(GameLobbyJoinRequested_t data)
|
||||
{
|
||||
MessageBox.Show("You cannot accept an invitation to a multiplayer game while using the map editor.", "Steam", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||||
}
|
||||
}
|
||||
}
|
5977
CnCTDRAMapEditor/Utility/TGASharpLib.cs
Normal file
5977
CnCTDRAMapEditor/Utility/TGASharpLib.cs
Normal file
File diff suppressed because it is too large
Load diff
170
CnCTDRAMapEditor/Utility/TeamColor.cs
Normal file
170
CnCTDRAMapEditor/Utility/TeamColor.cs
Normal file
|
@ -0,0 +1,170 @@
|
|||
//
|
||||
// Copyright 2020 Electronic Arts Inc.
|
||||
//
|
||||
// The Command & Conquer Map Editor and corresponding source code is free
|
||||
// software: you can redistribute it and/or modify it under the terms of
|
||||
// the GNU General Public License as published by the Free Software Foundation,
|
||||
// either version 3 of the License, or (at your option) any later version.
|
||||
|
||||
// The Command & Conquer Map Editor and corresponding source code is distributed
|
||||
// in the hope that it will be useful, but with permitted additional restrictions
|
||||
// under Section 7 of the GPL. See the GNU General Public License in LICENSE.TXT
|
||||
// distributed with this program. You should have received a copy of the
|
||||
// GNU General Public License along with permitted additional restrictions
|
||||
// with this program. If not, see https://github.com/electronicarts/CnC_Remastered_Collection
|
||||
using System.Drawing;
|
||||
using System.Numerics;
|
||||
using System.Xml;
|
||||
|
||||
namespace MobiusEditor.Utility
|
||||
{
|
||||
public class TeamColor
|
||||
{
|
||||
private readonly TeamColorManager teamColorManager;
|
||||
private readonly MegafileManager megafileManager;
|
||||
|
||||
public string Variant { get; private set; }
|
||||
|
||||
public string Name { get; private set; }
|
||||
|
||||
private Color? lowerBounds;
|
||||
public Color LowerBounds => lowerBounds.HasValue ? lowerBounds.Value : ((Variant != null) ? teamColorManager[Variant].LowerBounds : default);
|
||||
|
||||
private Color? upperBounds;
|
||||
public Color UpperBounds => upperBounds.HasValue ? upperBounds.Value : ((Variant != null) ? teamColorManager[Variant].UpperBounds : default);
|
||||
|
||||
private float? fudge;
|
||||
public float Fudge => fudge.HasValue ? fudge.Value : ((Variant != null) ? teamColorManager[Variant].Fudge : default);
|
||||
|
||||
private Vector3? hsvShift;
|
||||
public Vector3 HSVShift => hsvShift.HasValue ? hsvShift.Value : ((Variant != null) ? teamColorManager[Variant].HSVShift : default);
|
||||
|
||||
private Vector3? inputLevels;
|
||||
public Vector3 InputLevels => inputLevels.HasValue ? inputLevels.Value : ((Variant != null) ? teamColorManager[Variant].InputLevels : default);
|
||||
|
||||
private Vector2? outputLevels;
|
||||
public Vector2 OutputLevels => outputLevels.HasValue ? outputLevels.Value : ((Variant != null) ? teamColorManager[Variant].OutputLevels : default);
|
||||
|
||||
private Vector3? overallInputLevels;
|
||||
public Vector3 OverallInputLevels => overallInputLevels.HasValue ? overallInputLevels.Value : ((Variant != null) ? teamColorManager[Variant].OverallInputLevels : default);
|
||||
|
||||
private Vector2? overallOutputLevels;
|
||||
public Vector2 OverallOutputLevels => overallOutputLevels.HasValue ? overallOutputLevels.Value : ((Variant != null) ? teamColorManager[Variant].OverallOutputLevels : default);
|
||||
|
||||
private Color? radarMapColor;
|
||||
public Color RadarMapColor => radarMapColor.HasValue ? radarMapColor.Value : ((Variant != null) ? teamColorManager[Variant].RadarMapColor : default);
|
||||
|
||||
public TeamColor(TeamColorManager teamColorManager, MegafileManager megafileManager)
|
||||
{
|
||||
this.teamColorManager = teamColorManager;
|
||||
this.megafileManager = megafileManager;
|
||||
}
|
||||
|
||||
public void Load(string xml)
|
||||
{
|
||||
XmlDocument xmlDoc = new XmlDocument();
|
||||
xmlDoc.LoadXml(xml);
|
||||
|
||||
var node = xmlDoc.FirstChild;
|
||||
Name = node.Attributes["Name"].Value;
|
||||
Variant = node.Attributes["Variant"]?.Value;
|
||||
|
||||
var lowerBoundsNode = node.SelectSingleNode("LowerBounds");
|
||||
if (lowerBoundsNode != null)
|
||||
{
|
||||
lowerBounds = Color.FromArgb(
|
||||
(int)(float.Parse(lowerBoundsNode.SelectSingleNode("R").InnerText) * 255),
|
||||
(int)(float.Parse(lowerBoundsNode.SelectSingleNode("G").InnerText) * 255),
|
||||
(int)(float.Parse(lowerBoundsNode.SelectSingleNode("B").InnerText) * 255)
|
||||
);
|
||||
}
|
||||
|
||||
var upperBoundsNode = node.SelectSingleNode("UpperBounds");
|
||||
if (upperBoundsNode != null)
|
||||
{
|
||||
upperBounds = Color.FromArgb(
|
||||
(int)(float.Parse(upperBoundsNode.SelectSingleNode("R").InnerText) * 255),
|
||||
(int)(float.Parse(upperBoundsNode.SelectSingleNode("G").InnerText) * 255),
|
||||
(int)(float.Parse(upperBoundsNode.SelectSingleNode("B").InnerText) * 255)
|
||||
);
|
||||
}
|
||||
|
||||
var fudgeNode = node.SelectSingleNode("Fudge");
|
||||
if (fudgeNode != null)
|
||||
{
|
||||
fudge = float.Parse(fudgeNode.InnerText);
|
||||
}
|
||||
|
||||
var hsvShiftNode = node.SelectSingleNode("HSVShift");
|
||||
if (hsvShiftNode != null)
|
||||
{
|
||||
hsvShift = new Vector3(
|
||||
float.Parse(hsvShiftNode.SelectSingleNode("X").InnerText),
|
||||
float.Parse(hsvShiftNode.SelectSingleNode("Y").InnerText),
|
||||
float.Parse(hsvShiftNode.SelectSingleNode("Z").InnerText)
|
||||
);
|
||||
}
|
||||
|
||||
var inputLevelsNode = node.SelectSingleNode("InputLevels");
|
||||
if (inputLevelsNode != null)
|
||||
{
|
||||
inputLevels = new Vector3(
|
||||
float.Parse(inputLevelsNode.SelectSingleNode("X").InnerText),
|
||||
float.Parse(inputLevelsNode.SelectSingleNode("Y").InnerText),
|
||||
float.Parse(inputLevelsNode.SelectSingleNode("Z").InnerText)
|
||||
);
|
||||
}
|
||||
|
||||
var outputLevelsNode = node.SelectSingleNode("OutputLevels");
|
||||
if (outputLevelsNode != null)
|
||||
{
|
||||
outputLevels = new Vector2(
|
||||
float.Parse(outputLevelsNode.SelectSingleNode("X").InnerText),
|
||||
float.Parse(outputLevelsNode.SelectSingleNode("Y").InnerText)
|
||||
);
|
||||
}
|
||||
|
||||
var overallInputLevelsNode = node.SelectSingleNode("OverallInputLevels");
|
||||
if (overallInputLevelsNode != null)
|
||||
{
|
||||
overallInputLevels = new Vector3(
|
||||
float.Parse(overallInputLevelsNode.SelectSingleNode("X").InnerText),
|
||||
float.Parse(overallInputLevelsNode.SelectSingleNode("Y").InnerText),
|
||||
float.Parse(overallInputLevelsNode.SelectSingleNode("Z").InnerText)
|
||||
);
|
||||
}
|
||||
|
||||
var overallOutputLevelsNode = node.SelectSingleNode("OverallOutputLevels");
|
||||
if (outputLevelsNode != null)
|
||||
{
|
||||
overallOutputLevels = new Vector2(
|
||||
float.Parse(overallOutputLevelsNode.SelectSingleNode("X").InnerText),
|
||||
float.Parse(overallOutputLevelsNode.SelectSingleNode("Y").InnerText)
|
||||
);
|
||||
}
|
||||
|
||||
var radarMapColorNode = node.SelectSingleNode("RadarMapColor");
|
||||
if (radarMapColorNode != null)
|
||||
{
|
||||
radarMapColor = Color.FromArgb(
|
||||
(int)(float.Parse(radarMapColorNode.SelectSingleNode("R").InnerText) * 255),
|
||||
(int)(float.Parse(radarMapColorNode.SelectSingleNode("G").InnerText) * 255),
|
||||
(int)(float.Parse(radarMapColorNode.SelectSingleNode("B").InnerText) * 255)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public void Flatten()
|
||||
{
|
||||
lowerBounds = LowerBounds;
|
||||
upperBounds = UpperBounds;
|
||||
fudge = Fudge;
|
||||
hsvShift = HSVShift;
|
||||
inputLevels = InputLevels;
|
||||
outputLevels = OutputLevels;
|
||||
overallInputLevels = OverallInputLevels;
|
||||
overallOutputLevels = OverallOutputLevels;
|
||||
radarMapColor = RadarMapColor;
|
||||
}
|
||||
}
|
||||
}
|
93
CnCTDRAMapEditor/Utility/TeamColorManager.cs
Normal file
93
CnCTDRAMapEditor/Utility/TeamColorManager.cs
Normal file
|
@ -0,0 +1,93 @@
|
|||
//
|
||||
// Copyright 2020 Electronic Arts Inc.
|
||||
//
|
||||
// The Command & Conquer Map Editor and corresponding source code is free
|
||||
// software: you can redistribute it and/or modify it under the terms of
|
||||
// the GNU General Public License as published by the Free Software Foundation,
|
||||
// either version 3 of the License, or (at your option) any later version.
|
||||
|
||||
// The Command & Conquer Map Editor and corresponding source code is distributed
|
||||
// in the hope that it will be useful, but with permitted additional restrictions
|
||||
// under Section 7 of the GPL. See the GNU General Public License in LICENSE.TXT
|
||||
// distributed with this program. You should have received a copy of the
|
||||
// GNU General Public License along with permitted additional restrictions
|
||||
// with this program. If not, see https://github.com/electronicarts/CnC_Remastered_Collection
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Xml;
|
||||
|
||||
namespace MobiusEditor.Utility
|
||||
{
|
||||
public class TeamColorManager
|
||||
{
|
||||
private readonly Dictionary<string, TeamColor> teamColors = new Dictionary<string, TeamColor>();
|
||||
|
||||
private readonly MegafileManager megafileManager;
|
||||
|
||||
public TeamColor this[string key] => !string.IsNullOrEmpty(key) ? teamColors[key] : null;
|
||||
|
||||
public TeamColorManager(MegafileManager megafileManager)
|
||||
{
|
||||
this.megafileManager = megafileManager;
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
teamColors.Clear();
|
||||
}
|
||||
|
||||
public void Load(string xmlPath)
|
||||
{
|
||||
XmlDocument xmlDoc = new XmlDocument();
|
||||
xmlDoc.Load(megafileManager.Open(xmlPath));
|
||||
|
||||
foreach (XmlNode teamColorNode in xmlDoc.SelectNodes("/*/TeamColorTypeClass"))
|
||||
{
|
||||
var teamColor = new TeamColor(this, megafileManager);
|
||||
teamColor.Load(teamColorNode.OuterXml);
|
||||
|
||||
teamColors[teamColorNode.Attributes["Name"].Value] = teamColor;
|
||||
}
|
||||
|
||||
foreach (var teamColor in TopologicalSortTeamColors())
|
||||
{
|
||||
teamColor.Flatten();
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerable<TeamColor> TopologicalSortTeamColors()
|
||||
{
|
||||
var nodes = teamColors.Values.ToList();
|
||||
HashSet<(TeamColor, TeamColor)> edges = new HashSet<(TeamColor, TeamColor)>();
|
||||
foreach (var node in nodes)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(node.Variant))
|
||||
{
|
||||
edges.Add((this[node.Variant], node));
|
||||
}
|
||||
}
|
||||
|
||||
var sorted = new List<TeamColor>();
|
||||
var openSet = new HashSet<TeamColor>(nodes.Where(n => edges.All(e => !e.Item2.Equals(n))));
|
||||
while (openSet.Count > 0)
|
||||
{
|
||||
var node = openSet.First();
|
||||
openSet.Remove(node);
|
||||
sorted.Add(node);
|
||||
|
||||
foreach (var edge in edges.Where(e => e.Item1.Equals(node)).ToArray())
|
||||
{
|
||||
var node2 = edge.Item2;
|
||||
edges.Remove(edge);
|
||||
|
||||
if (edges.All(edge2 => !edge2.Item2.Equals(node2)))
|
||||
{
|
||||
openSet.Add(node2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (edges.Count == 0) ? sorted : null;
|
||||
}
|
||||
}
|
||||
}
|
466
CnCTDRAMapEditor/Utility/TextureManager.cs
Normal file
466
CnCTDRAMapEditor/Utility/TextureManager.cs
Normal file
|
@ -0,0 +1,466 @@
|
|||
//
|
||||
// Copyright 2020 Electronic Arts Inc.
|
||||
//
|
||||
// The Command & Conquer Map Editor and corresponding source code is free
|
||||
// software: you can redistribute it and/or modify it under the terms of
|
||||
// the GNU General Public License as published by the Free Software Foundation,
|
||||
// either version 3 of the License, or (at your option) any later version.
|
||||
|
||||
// The Command & Conquer Map Editor and corresponding source code is distributed
|
||||
// in the hope that it will be useful, but with permitted additional restrictions
|
||||
// under Section 7 of the GPL. See the GNU General Public License in LICENSE.TXT
|
||||
// distributed with this program. You should have received a copy of the
|
||||
// GNU General Public License along with permitted additional restrictions
|
||||
// with this program. If not, see https://github.com/electronicarts/CnC_Remastered_Collection
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Pfim;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
using System.Drawing.Imaging;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using TGASharpLib;
|
||||
|
||||
namespace MobiusEditor.Utility
|
||||
{
|
||||
public class TextureManager
|
||||
{
|
||||
#if false
|
||||
private class ImageData
|
||||
{
|
||||
public TGA TGA;
|
||||
public JObject Metadata;
|
||||
}
|
||||
#endif
|
||||
|
||||
private readonly MegafileManager megafileManager;
|
||||
|
||||
private Dictionary<string, Bitmap> cachedTextures = new Dictionary<string, Bitmap>();
|
||||
private Dictionary<(string, TeamColor), (Bitmap, Rectangle)> teamColorTextures = new Dictionary<(string, TeamColor), (Bitmap, Rectangle)>();
|
||||
|
||||
public TextureManager(MegafileManager megafileManager)
|
||||
{
|
||||
this.megafileManager = megafileManager;
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
cachedTextures.Clear();
|
||||
teamColorTextures.Clear();
|
||||
}
|
||||
|
||||
public (Bitmap, Rectangle) GetTexture(string filename, TeamColor teamColor)
|
||||
{
|
||||
if (teamColorTextures.TryGetValue((filename, teamColor), out (Bitmap bitmap, Rectangle opaqueBounds) result))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
if (!cachedTextures.TryGetValue(filename, out result.bitmap))
|
||||
{
|
||||
if (Path.GetExtension(filename).ToLower() == ".tga")
|
||||
{
|
||||
TGA tga = null;
|
||||
JObject metadata = null;
|
||||
|
||||
// First attempt to find the texture in an archive
|
||||
var name = Path.GetFileNameWithoutExtension(filename);
|
||||
var archiveDir = Path.GetDirectoryName(filename);
|
||||
var archivePath = archiveDir + ".ZIP";
|
||||
using (var fileStream = megafileManager.Open(archivePath))
|
||||
{
|
||||
if (fileStream != null)
|
||||
{
|
||||
using (var archive = new ZipArchive(fileStream, ZipArchiveMode.Read))
|
||||
{
|
||||
foreach (var entry in archive.Entries)
|
||||
{
|
||||
if (name == Path.GetFileNameWithoutExtension(entry.Name))
|
||||
{
|
||||
if ((tga == null) && (Path.GetExtension(entry.Name).ToLower() == ".tga"))
|
||||
{
|
||||
using (var stream = entry.Open())
|
||||
using (var memStream = new MemoryStream())
|
||||
{
|
||||
stream.CopyTo(memStream);
|
||||
tga = new TGA(memStream);
|
||||
}
|
||||
}
|
||||
else if ((metadata == null) && (Path.GetExtension(entry.Name).ToLower() == ".meta"))
|
||||
{
|
||||
using (var stream = entry.Open())
|
||||
using (var reader = new StreamReader(stream))
|
||||
{
|
||||
metadata = JObject.Parse(reader.ReadToEnd());
|
||||
}
|
||||
}
|
||||
|
||||
if ((tga != null) && (metadata != null))
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Next attempt to load a standalone file
|
||||
if (tga == null)
|
||||
{
|
||||
using (var fileStream = megafileManager.Open(filename))
|
||||
{
|
||||
if (fileStream != null)
|
||||
{
|
||||
tga = new TGA(fileStream);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (tga != null)
|
||||
{
|
||||
var bitmap = tga.ToBitmap(true);
|
||||
if (metadata != null)
|
||||
{
|
||||
var size = new Size(metadata["size"][0].ToObject<int>(), metadata["size"][1].ToObject<int>());
|
||||
var crop = Rectangle.FromLTRB(
|
||||
metadata["crop"][0].ToObject<int>(),
|
||||
metadata["crop"][1].ToObject<int>(),
|
||||
metadata["crop"][2].ToObject<int>(),
|
||||
metadata["crop"][3].ToObject<int>()
|
||||
);
|
||||
|
||||
var uncroppedBitmap = new Bitmap(size.Width, size.Height, bitmap.PixelFormat);
|
||||
using (var g = Graphics.FromImage(uncroppedBitmap))
|
||||
{
|
||||
g.DrawImage(bitmap, crop, new Rectangle(Point.Empty, bitmap.Size), GraphicsUnit.Pixel);
|
||||
}
|
||||
cachedTextures[filename] = uncroppedBitmap;
|
||||
}
|
||||
else
|
||||
{
|
||||
cachedTextures[filename] = bitmap;
|
||||
}
|
||||
}
|
||||
|
||||
#if false
|
||||
// Attempt to load parent directory as archive
|
||||
var archiveDir = Path.GetDirectoryName(filename);
|
||||
var archivePath = archiveDir + ".ZIP";
|
||||
using (var fileStream = megafileManager.Open(archivePath))
|
||||
{
|
||||
if (fileStream != null)
|
||||
{
|
||||
using (var archive = new ZipArchive(fileStream, ZipArchiveMode.Read))
|
||||
{
|
||||
var images = new Dictionary<string, ImageData>();
|
||||
|
||||
foreach (var entry in archive.Entries)
|
||||
{
|
||||
var name = Path.GetFileNameWithoutExtension(entry.Name);
|
||||
if (!images.TryGetValue(name, out ImageData imageData))
|
||||
{
|
||||
imageData = images[name] = new ImageData { TGA = null, Metadata = null };
|
||||
}
|
||||
|
||||
if ((imageData.TGA == null) && (Path.GetExtension(entry.Name).ToLower() == ".tga"))
|
||||
{
|
||||
using (var stream = entry.Open())
|
||||
using (var memStream = new MemoryStream())
|
||||
{
|
||||
stream.CopyTo(memStream);
|
||||
imageData.TGA = new TGA(memStream);
|
||||
}
|
||||
}
|
||||
else if ((imageData.Metadata == null) && (Path.GetExtension(entry.Name).ToLower() == ".meta"))
|
||||
{
|
||||
using (var stream = entry.Open())
|
||||
using (var reader = new StreamReader(stream))
|
||||
{
|
||||
imageData.Metadata = JObject.Parse(reader.ReadToEnd());
|
||||
}
|
||||
}
|
||||
|
||||
if ((imageData.TGA != null) && (imageData.Metadata != null))
|
||||
{
|
||||
var bitmap = imageData.TGA.ToBitmap(true);
|
||||
var size = new Size(imageData.Metadata["size"][0].ToObject<int>(), imageData.Metadata["size"][1].ToObject<int>());
|
||||
var crop = Rectangle.FromLTRB(
|
||||
imageData.Metadata["crop"][0].ToObject<int>(),
|
||||
imageData.Metadata["crop"][1].ToObject<int>(),
|
||||
imageData.Metadata["crop"][2].ToObject<int>(),
|
||||
imageData.Metadata["crop"][3].ToObject<int>()
|
||||
);
|
||||
|
||||
var uncroppedBitmap = new Bitmap(size.Width, size.Height, bitmap.PixelFormat);
|
||||
using (var g = Graphics.FromImage(uncroppedBitmap))
|
||||
{
|
||||
g.DrawImage(bitmap, crop, new Rectangle(Point.Empty, bitmap.Size), GraphicsUnit.Pixel);
|
||||
}
|
||||
cachedTextures[Path.Combine(archiveDir, name) + ".tga"] = uncroppedBitmap;
|
||||
|
||||
images.Remove(name);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var item in images.Where(x => x.Value.TGA != null))
|
||||
{
|
||||
cachedTextures[Path.Combine(archiveDir, item.Key) + ".tga"] = item.Value.TGA.ToBitmap(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
if (!cachedTextures.TryGetValue(filename, out result.bitmap))
|
||||
{
|
||||
// Try loading as a DDS
|
||||
var ddsFilename = Path.ChangeExtension(filename, ".DDS");
|
||||
using (var fileStream = megafileManager.Open(ddsFilename))
|
||||
{
|
||||
if (fileStream != null)
|
||||
{
|
||||
var bytes = new byte[fileStream.Length];
|
||||
fileStream.Read(bytes, 0, bytes.Length);
|
||||
|
||||
using (var image = Dds.Create(bytes, new PfimConfig()))
|
||||
{
|
||||
PixelFormat format;
|
||||
switch (image.Format)
|
||||
{
|
||||
case Pfim.ImageFormat.Rgb24:
|
||||
format = PixelFormat.Format24bppRgb;
|
||||
break;
|
||||
|
||||
case Pfim.ImageFormat.Rgba32:
|
||||
format = PixelFormat.Format32bppArgb;
|
||||
break;
|
||||
|
||||
case Pfim.ImageFormat.R5g5b5:
|
||||
format = PixelFormat.Format16bppRgb555;
|
||||
break;
|
||||
|
||||
case Pfim.ImageFormat.R5g6b5:
|
||||
format = PixelFormat.Format16bppRgb565;
|
||||
break;
|
||||
|
||||
case Pfim.ImageFormat.R5g5b5a1:
|
||||
format = PixelFormat.Format16bppArgb1555;
|
||||
break;
|
||||
|
||||
case Pfim.ImageFormat.Rgb8:
|
||||
format = PixelFormat.Format8bppIndexed;
|
||||
break;
|
||||
|
||||
default:
|
||||
format = PixelFormat.DontCare;
|
||||
break;
|
||||
}
|
||||
|
||||
var bitmap = new Bitmap(image.Width, image.Height, format);
|
||||
var bitmapData = bitmap.LockBits(new Rectangle(0, 0, image.Width, image.Height), ImageLockMode.WriteOnly, bitmap.PixelFormat);
|
||||
Marshal.Copy(image.Data, 0, bitmapData.Scan0, image.Stride * image.Height);
|
||||
bitmap.UnlockBits(bitmapData);
|
||||
cachedTextures[filename] = bitmap;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!cachedTextures.TryGetValue(filename, out result.bitmap))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
result.bitmap = new Bitmap(result.bitmap);
|
||||
result.opaqueBounds = new Rectangle(0, 0, result.bitmap.Width, result.bitmap.Height);
|
||||
if (teamColor != null)
|
||||
{
|
||||
float frac(float x) => x - (int)x;
|
||||
float lerp(float x, float y, float t) => (x * (1.0f - t)) + (y * t);
|
||||
float saturate(float x) => Math.Max(0.0f, Math.Min(1.0f, x));
|
||||
|
||||
BitmapData data = null;
|
||||
try
|
||||
{
|
||||
data = result.bitmap.LockBits(new Rectangle(0, 0, result.bitmap.Width, result.bitmap.Height), ImageLockMode.ReadWrite, result.bitmap.PixelFormat);
|
||||
var bpp = Image.GetPixelFormatSize(data.PixelFormat) / 8;
|
||||
var bytes = new byte[data.Stride * data.Height];
|
||||
Marshal.Copy(data.Scan0, bytes, 0, bytes.Length);
|
||||
|
||||
result.opaqueBounds = CalculateOpaqueBounds(bytes, data.Width, data.Height, bpp, data.Stride);
|
||||
|
||||
for (int j = 0; j < bytes.Length; j += bpp)
|
||||
{
|
||||
var pixel = Color.FromArgb(bytes[j + 2], bytes[j + 1], bytes[j + 0]);
|
||||
(float r, float g, float b) = (pixel.R.ToLinear(), pixel.G.ToLinear(), pixel.B.ToLinear());
|
||||
|
||||
(float x, float y, float z, float w) K = (0.0f, -1.0f / 3.0f, 2.0f / 3.0f, -1.0f);
|
||||
(float x, float y, float z, float w) p = (g >= b) ? (g, b, K.x, K.y) : (b, g, K.w, K.z);
|
||||
(float x, float y, float z, float w) q = (r >= p.x) ? (r, p.y, p.z, p.x) : (p.x, p.y, p.w, r);
|
||||
(float d, float e) = (q.x - Math.Min(q.w, q.y), 1e-10f);
|
||||
(float hue, float saturation, float value) = (Math.Abs(q.z + (q.w - q.y) / (6.0f * d + e)), d / (q.x + e), q.x);
|
||||
|
||||
var lowerHue = teamColor.LowerBounds.GetHue() / 360.0f;
|
||||
var upperHue = teamColor.UpperBounds.GetHue() / 360.0f;
|
||||
if ((hue >= lowerHue) && (upperHue >= hue))
|
||||
{
|
||||
hue = (hue / (upperHue - lowerHue)) * ((upperHue + teamColor.Fudge) - (lowerHue - teamColor.Fudge));
|
||||
hue += teamColor.HSVShift.X;
|
||||
saturation += teamColor.HSVShift.Y;
|
||||
value += teamColor.HSVShift.Z;
|
||||
|
||||
(float x, float y, float z, float w) L = (1.0f, 2.0f / 3.0f, 1.0f / 3.0f, 3.0f);
|
||||
(float x, float y, float z) m = (
|
||||
Math.Abs(frac(hue + L.x) * 6.0f - L.w),
|
||||
Math.Abs(frac(hue + L.y) * 6.0f - L.w),
|
||||
Math.Abs(frac(hue + L.z) * 6.0f - L.w)
|
||||
);
|
||||
|
||||
r = value * lerp(L.x, saturate(m.x - L.x), saturation);
|
||||
g = value * lerp(L.x, saturate(m.y - L.x), saturation);
|
||||
b = value * lerp(L.x, saturate(m.z - L.x), saturation);
|
||||
|
||||
(float x, float y, float z) n = (
|
||||
Math.Min(1.0f, Math.Max(0.0f, r - teamColor.InputLevels.X) / (teamColor.InputLevels.Z - teamColor.InputLevels.X)),
|
||||
Math.Min(1.0f, Math.Max(0.0f, g - teamColor.InputLevels.X) / (teamColor.InputLevels.Z - teamColor.InputLevels.X)),
|
||||
Math.Min(1.0f, Math.Max(0.0f, b - teamColor.InputLevels.X) / (teamColor.InputLevels.Z - teamColor.InputLevels.X))
|
||||
);
|
||||
n.x = (float)Math.Pow(n.x, teamColor.InputLevels.Y);
|
||||
n.y = (float)Math.Pow(n.y, teamColor.InputLevels.Y);
|
||||
n.z = (float)Math.Pow(n.z, teamColor.InputLevels.Y);
|
||||
|
||||
r = lerp(teamColor.OutputLevels.X, teamColor.OutputLevels.Y, n.x);
|
||||
g = lerp(teamColor.OutputLevels.X, teamColor.OutputLevels.Y, n.y);
|
||||
b = lerp(teamColor.OutputLevels.X, teamColor.OutputLevels.Y, n.z);
|
||||
}
|
||||
|
||||
(float x, float y, float z) n2 = (
|
||||
Math.Min(1.0f, Math.Max(0.0f, r - teamColor.OverallInputLevels.X) / (teamColor.OverallInputLevels.Z - teamColor.OverallInputLevels.X)),
|
||||
Math.Min(1.0f, Math.Max(0.0f, g - teamColor.OverallInputLevels.X) / (teamColor.OverallInputLevels.Z - teamColor.OverallInputLevels.X)),
|
||||
Math.Min(1.0f, Math.Max(0.0f, b - teamColor.OverallInputLevels.X) / (teamColor.OverallInputLevels.Z - teamColor.OverallInputLevels.X))
|
||||
);
|
||||
n2.x = (float)Math.Pow(n2.x, teamColor.OverallInputLevels.Y);
|
||||
n2.y = (float)Math.Pow(n2.y, teamColor.OverallInputLevels.Y);
|
||||
n2.z = (float)Math.Pow(n2.z, teamColor.OverallInputLevels.Y);
|
||||
|
||||
r = lerp(teamColor.OverallOutputLevels.X, teamColor.OverallOutputLevels.Y, n2.x);
|
||||
g = lerp(teamColor.OverallOutputLevels.X, teamColor.OverallOutputLevels.Y, n2.y);
|
||||
b = lerp(teamColor.OverallOutputLevels.X, teamColor.OverallOutputLevels.Y, n2.z);
|
||||
|
||||
bytes[j + 2] = (byte)(r.ToSRGB() * 255.0f);
|
||||
bytes[j + 1] = (byte)(g.ToSRGB() * 255.0f);
|
||||
bytes[j + 0] = (byte)(b.ToSRGB() * 255.0f);
|
||||
}
|
||||
|
||||
Marshal.Copy(bytes, 0, data.Scan0, bytes.Length);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (data != null)
|
||||
{
|
||||
result.bitmap.UnlockBits(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
result.opaqueBounds = CalculateOpaqueBounds(result.bitmap);
|
||||
}
|
||||
|
||||
teamColorTextures[(filename, teamColor)] = result;
|
||||
return result;
|
||||
}
|
||||
|
||||
private static Rectangle CalculateOpaqueBounds(byte[] data, int width, int height, int bpp, int stride)
|
||||
{
|
||||
bool isTransparentRow(int y)
|
||||
{
|
||||
var start = y * stride;
|
||||
for (var i = bpp - 1; i < stride; i += bpp)
|
||||
{
|
||||
if (data[start + i] != 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
var opaqueBounds = new Rectangle(0, 0, width, height);
|
||||
for (int y = 0; y < height; ++y)
|
||||
{
|
||||
if (!isTransparentRow(y))
|
||||
{
|
||||
opaqueBounds.Offset(0, y);
|
||||
break;
|
||||
}
|
||||
}
|
||||
for (int y = height; y > 0; --y)
|
||||
{
|
||||
if (!isTransparentRow(y - 1))
|
||||
{
|
||||
opaqueBounds.Height = y - opaqueBounds.Top;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
bool isTransparentColumn(int x)
|
||||
{
|
||||
var start = (x * bpp) + (bpp - 1);
|
||||
for (var y = opaqueBounds.Top; y < opaqueBounds.Bottom; ++y)
|
||||
{
|
||||
if (data[start + (y * stride)] != 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
for (int x = 0; x < width; ++x)
|
||||
{
|
||||
if (!isTransparentColumn(x))
|
||||
{
|
||||
opaqueBounds.Offset(x, 0);
|
||||
break;
|
||||
}
|
||||
}
|
||||
for (int x = width; x > 0; --x)
|
||||
{
|
||||
if (!isTransparentColumn(x - 1))
|
||||
{
|
||||
opaqueBounds.Width = x - opaqueBounds.Left;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return opaqueBounds;
|
||||
}
|
||||
|
||||
private static Rectangle CalculateOpaqueBounds(Bitmap bitmap)
|
||||
{
|
||||
BitmapData data = null;
|
||||
try
|
||||
{
|
||||
data = bitmap.LockBits(new Rectangle(0, 0, bitmap.Width, bitmap.Height), ImageLockMode.ReadWrite, bitmap.PixelFormat);
|
||||
var bpp = Image.GetPixelFormatSize(data.PixelFormat) / 8;
|
||||
var bytes = new byte[data.Stride * data.Height];
|
||||
Marshal.Copy(data.Scan0, bytes, 0, bytes.Length);
|
||||
return CalculateOpaqueBounds(bytes, data.Width, data.Height, bpp, data.Stride);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (data != null)
|
||||
{
|
||||
bitmap.UnlockBits(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
169
CnCTDRAMapEditor/Utility/Tileset.cs
Normal file
169
CnCTDRAMapEditor/Utility/Tileset.cs
Normal file
|
@ -0,0 +1,169 @@
|
|||
//
|
||||
// Copyright 2020 Electronic Arts Inc.
|
||||
//
|
||||
// The Command & Conquer Map Editor and corresponding source code is free
|
||||
// software: you can redistribute it and/or modify it under the terms of
|
||||
// the GNU General Public License as published by the Free Software Foundation,
|
||||
// either version 3 of the License, or (at your option) any later version.
|
||||
|
||||
// The Command & Conquer Map Editor and corresponding source code is distributed
|
||||
// in the hope that it will be useful, but with permitted additional restrictions
|
||||
// under Section 7 of the GPL. See the GNU General Public License in LICENSE.TXT
|
||||
// distributed with this program. You should have received a copy of the
|
||||
// GNU General Public License along with permitted additional restrictions
|
||||
// with this program. If not, see https://github.com/electronicarts/CnC_Remastered_Collection
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Xml;
|
||||
|
||||
namespace MobiusEditor.Utility
|
||||
{
|
||||
public class Tile
|
||||
{
|
||||
public Image Image { get; private set; }
|
||||
|
||||
public Rectangle OpaqueBounds { get; private set; }
|
||||
|
||||
public Tile(Image image, Rectangle opaqueBounds)
|
||||
{
|
||||
Image = image;
|
||||
OpaqueBounds = opaqueBounds;
|
||||
}
|
||||
|
||||
public Tile(Image image)
|
||||
: this(image, new Rectangle(0, 0, image.Width, image.Height))
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public class Tileset
|
||||
{
|
||||
private class TileData
|
||||
{
|
||||
public int FPS { get; set; }
|
||||
public string[] Frames { get; set; }
|
||||
|
||||
public Dictionary<string, Tile[]> TeamColorTiles { get; } = new Dictionary<string, Tile[]>();
|
||||
}
|
||||
|
||||
private readonly Dictionary<string, Dictionary<int, TileData>> tiles = new Dictionary<string, Dictionary<int, TileData>>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
private readonly TextureManager textureManager;
|
||||
|
||||
private static readonly Bitmap transparentTileImage;
|
||||
|
||||
static Tileset()
|
||||
{
|
||||
transparentTileImage = new Bitmap(Globals.TileWidth, Globals.TileHeight);
|
||||
transparentTileImage.MakeTransparent();
|
||||
}
|
||||
|
||||
public Tileset(TextureManager textureManager)
|
||||
{
|
||||
this.textureManager = textureManager;
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
foreach (var item in tiles)
|
||||
{
|
||||
foreach (var tileItem in item.Value)
|
||||
{
|
||||
tileItem.Value.TeamColorTiles.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Load(string xml, string texturesPath)
|
||||
{
|
||||
XmlDocument xmlDoc = new XmlDocument();
|
||||
xmlDoc.LoadXml(xml);
|
||||
|
||||
var rootPath = Path.Combine(texturesPath, xmlDoc.SelectSingleNode("TilesetTypeClass/RootTexturePath").InnerText);
|
||||
foreach (XmlNode tileNode in xmlDoc.SelectNodes("TilesetTypeClass/Tiles/Tile"))
|
||||
{
|
||||
TileData tileData = new TileData();
|
||||
|
||||
var name = tileNode.SelectSingleNode("Key/Name").InnerText;
|
||||
var shape = int.Parse(tileNode.SelectSingleNode("Key/Shape").InnerText);
|
||||
var fpsNode = tileNode.SelectSingleNode("Value/AnimationData/FPS");
|
||||
tileData.FPS = (fpsNode != null) ? int.Parse(fpsNode.InnerText) : 0;
|
||||
|
||||
var frameNodes = tileNode.SelectNodes("Value/Frames/Frame");
|
||||
#if false
|
||||
tileData.Frames = new string[frameNodes.Count];
|
||||
#else
|
||||
tileData.Frames = new string[Math.Min(1, frameNodes.Count)];
|
||||
#endif
|
||||
|
||||
for (var i = 0; i < tileData.Frames.Length; ++i)
|
||||
{
|
||||
string filename = null;
|
||||
if (!string.IsNullOrEmpty(frameNodes[i].InnerText))
|
||||
{
|
||||
filename = Path.Combine(rootPath, frameNodes[i].InnerText);
|
||||
}
|
||||
|
||||
tileData.Frames[i] = filename;
|
||||
}
|
||||
|
||||
if (!tiles.TryGetValue(name, out Dictionary<int, TileData> shapes))
|
||||
{
|
||||
shapes = new Dictionary<int, TileData>();
|
||||
tiles[name] = shapes;
|
||||
}
|
||||
|
||||
shapes[shape] = tileData;
|
||||
}
|
||||
}
|
||||
|
||||
public bool GetTileData(string name, int shape, TeamColor teamColor, out int fps, out Tile[] tiles)
|
||||
{
|
||||
fps = 0;
|
||||
tiles = null;
|
||||
|
||||
if (!this.tiles.TryGetValue(name, out Dictionary<int, TileData> shapes))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (shape < 0)
|
||||
{
|
||||
shape = Math.Max(0, shapes.Max(kv => kv.Key) + shape + 1);
|
||||
}
|
||||
if (!shapes.TryGetValue(shape, out TileData tileData))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var key = teamColor?.Name ?? string.Empty;
|
||||
if (!tileData.TeamColorTiles.TryGetValue(key, out Tile[] tileDataTiles))
|
||||
{
|
||||
tileDataTiles = new Tile[tileData.Frames.Length];
|
||||
tileData.TeamColorTiles[key] = tileDataTiles;
|
||||
|
||||
for (int i = 0; i < tileDataTiles.Length; ++i)
|
||||
{
|
||||
var filename = tileData.Frames[i];
|
||||
if (!string.IsNullOrEmpty(filename))
|
||||
{
|
||||
(Bitmap bitmap, Rectangle opaqueBounds) = textureManager.GetTexture(filename, teamColor);
|
||||
tileDataTiles[i] = new Tile(bitmap, opaqueBounds);
|
||||
}
|
||||
else
|
||||
{
|
||||
tileDataTiles[i] = new Tile(transparentTileImage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fps = tileData.FPS;
|
||||
tiles = tileDataTiles;
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
116
CnCTDRAMapEditor/Utility/TilesetManager.cs
Normal file
116
CnCTDRAMapEditor/Utility/TilesetManager.cs
Normal file
|
@ -0,0 +1,116 @@
|
|||
//
|
||||
// Copyright 2020 Electronic Arts Inc.
|
||||
//
|
||||
// The Command & Conquer Map Editor and corresponding source code is free
|
||||
// software: you can redistribute it and/or modify it under the terms of
|
||||
// the GNU General Public License as published by the Free Software Foundation,
|
||||
// either version 3 of the License, or (at your option) any later version.
|
||||
|
||||
// The Command & Conquer Map Editor and corresponding source code is distributed
|
||||
// in the hope that it will be useful, but with permitted additional restrictions
|
||||
// under Section 7 of the GPL. See the GNU General Public License in LICENSE.TXT
|
||||
// distributed with this program. You should have received a copy of the
|
||||
// GNU General Public License along with permitted additional restrictions
|
||||
// with this program. If not, see https://github.com/electronicarts/CnC_Remastered_Collection
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Xml;
|
||||
|
||||
namespace MobiusEditor.Utility
|
||||
{
|
||||
public class TilesetManager
|
||||
{
|
||||
private readonly Dictionary<string, Tileset> tilesets = new Dictionary<string, Tileset>();
|
||||
|
||||
private readonly MegafileManager megafileManager;
|
||||
|
||||
public TilesetManager(MegafileManager megafileManager, TextureManager textureManager, string xmlPath, string texturesPath)
|
||||
{
|
||||
this.megafileManager = megafileManager;
|
||||
|
||||
XmlDocument xmlDoc = new XmlDocument();
|
||||
xmlDoc.Load(megafileManager.Open(xmlPath));
|
||||
|
||||
foreach (XmlNode fileNode in xmlDoc.SelectNodes("TilesetFiles/File"))
|
||||
{
|
||||
var xmlFile = Path.Combine(Path.GetDirectoryName(xmlPath), fileNode.InnerText);
|
||||
XmlDocument fileXmlDoc = new XmlDocument();
|
||||
fileXmlDoc.Load(megafileManager.Open(xmlFile));
|
||||
|
||||
foreach (XmlNode tilesetNode in fileXmlDoc.SelectNodes("Tilesets/TilesetTypeClass"))
|
||||
{
|
||||
var tileset = new Tileset(textureManager);
|
||||
tileset.Load(tilesetNode.OuterXml, texturesPath);
|
||||
|
||||
tilesets[tilesetNode.Attributes["name"].Value] = tileset;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
foreach (var item in tilesets)
|
||||
{
|
||||
item.Value.Reset();
|
||||
}
|
||||
}
|
||||
|
||||
public bool GetTeamColorTileData(IEnumerable<string> searchTilesets, string name, int shape, TeamColor teamColor, out int fps, out Tile[] tiles)
|
||||
{
|
||||
fps = 0;
|
||||
tiles = null;
|
||||
|
||||
foreach (var tileset in tilesets.Join(searchTilesets, x => x.Key, y => y, (x, y) => x.Value))
|
||||
{
|
||||
if (tileset.GetTileData(name, shape, teamColor, out fps, out tiles))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool GetTeamColorTileData(IEnumerable<string> searchTilesets, string name, int shape, TeamColor teamColor, out int fps, out Tile tile)
|
||||
{
|
||||
tile = null;
|
||||
if (!GetTeamColorTileData(searchTilesets, name, shape, teamColor, out fps, out Tile[] tiles))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
tile = tiles[0];
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool GetTeamColorTileData(IEnumerable<string> searchTilesets, string name, int shape, TeamColor teamColor, out Tile[] tiles)
|
||||
{
|
||||
return GetTeamColorTileData(searchTilesets, name, shape, teamColor, out int fps, out tiles);
|
||||
}
|
||||
|
||||
public bool GetTeamColorTileData(IEnumerable<string> searchTilesets, string name, int shape, TeamColor teamColor, out Tile tile)
|
||||
{
|
||||
return GetTeamColorTileData(searchTilesets, name, shape, teamColor, out int fps, out tile);
|
||||
}
|
||||
|
||||
public bool GetTileData(IEnumerable<string> searchTilesets, string name, int shape, out int fps, out Tile[] tiles)
|
||||
{
|
||||
return GetTeamColorTileData(searchTilesets, name, shape, null, out fps, out tiles);
|
||||
}
|
||||
|
||||
public bool GetTileData(IEnumerable<string> searchTilesets, string name, int shape, out int fps, out Tile tile)
|
||||
{
|
||||
return GetTeamColorTileData(searchTilesets, name, shape, null, out fps, out tile);
|
||||
}
|
||||
|
||||
public bool GetTileData(IEnumerable<string> searchTilesets, string name, int shape, out Tile[] tiles)
|
||||
{
|
||||
return GetTeamColorTileData(searchTilesets, name, shape, null, out tiles);
|
||||
}
|
||||
|
||||
public bool GetTileData(IEnumerable<string> searchTilesets, string name, int shape, out Tile tile)
|
||||
{
|
||||
return GetTeamColorTileData(searchTilesets, name, shape, null, out tile);
|
||||
}
|
||||
}
|
||||
}
|
113
CnCTDRAMapEditor/Utility/UndoRedoList.cs
Normal file
113
CnCTDRAMapEditor/Utility/UndoRedoList.cs
Normal file
|
@ -0,0 +1,113 @@
|
|||
//
|
||||
// Copyright 2020 Electronic Arts Inc.
|
||||
//
|
||||
// The Command & Conquer Map Editor and corresponding source code is free
|
||||
// software: you can redistribute it and/or modify it under the terms of
|
||||
// the GNU General Public License as published by the Free Software Foundation,
|
||||
// either version 3 of the License, or (at your option) any later version.
|
||||
|
||||
// The Command & Conquer Map Editor and corresponding source code is distributed
|
||||
// in the hope that it will be useful, but with permitted additional restrictions
|
||||
// under Section 7 of the GPL. See the GNU General Public License in LICENSE.TXT
|
||||
// distributed with this program. You should have received a copy of the
|
||||
// GNU General Public License along with permitted additional restrictions
|
||||
// with this program. If not, see https://github.com/electronicarts/CnC_Remastered_Collection
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MobiusEditor.Utility
|
||||
{
|
||||
public class UndoRedoList<T>
|
||||
{
|
||||
private const int DefaultMaxUndoRedo = 50;
|
||||
|
||||
private readonly List<(Action<T> Undo, Action<T> Redo)> undoRedoActions = new List<(Action<T> Undo, Action<T> Redo)>();
|
||||
private readonly int maxUndoRedo;
|
||||
private int undoRedoPosition = 0;
|
||||
|
||||
public event EventHandler<EventArgs> Tracked;
|
||||
public event EventHandler<EventArgs> Undone;
|
||||
public event EventHandler<EventArgs> Redone;
|
||||
|
||||
public bool CanUndo => undoRedoPosition > 0;
|
||||
|
||||
public bool CanRedo => undoRedoActions.Count > undoRedoPosition;
|
||||
|
||||
public UndoRedoList(int maxUndoRedo)
|
||||
{
|
||||
this.maxUndoRedo = maxUndoRedo;
|
||||
}
|
||||
|
||||
public UndoRedoList()
|
||||
: this(DefaultMaxUndoRedo)
|
||||
{
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
undoRedoActions.Clear();
|
||||
undoRedoPosition = 0;
|
||||
OnTracked();
|
||||
}
|
||||
|
||||
public void Track(Action<T> undo, Action<T> redo)
|
||||
{
|
||||
if (undoRedoActions.Count > undoRedoPosition)
|
||||
{
|
||||
undoRedoActions.RemoveRange(undoRedoPosition, undoRedoActions.Count - undoRedoPosition);
|
||||
}
|
||||
|
||||
undoRedoActions.Add((undo, redo));
|
||||
|
||||
if (undoRedoActions.Count > maxUndoRedo)
|
||||
{
|
||||
undoRedoActions.RemoveRange(0, undoRedoActions.Count - maxUndoRedo);
|
||||
}
|
||||
|
||||
undoRedoPosition = undoRedoActions.Count;
|
||||
OnTracked();
|
||||
}
|
||||
|
||||
public void Undo(T context)
|
||||
{
|
||||
if (!CanUndo)
|
||||
{
|
||||
throw new InvalidOperationException();
|
||||
}
|
||||
|
||||
undoRedoPosition--;
|
||||
undoRedoActions[undoRedoPosition].Undo(context);
|
||||
OnUndone();
|
||||
}
|
||||
|
||||
public void Redo(T context)
|
||||
{
|
||||
if (!CanRedo)
|
||||
{
|
||||
throw new InvalidOperationException();
|
||||
}
|
||||
|
||||
undoRedoActions[undoRedoPosition].Redo(context);
|
||||
undoRedoPosition++;
|
||||
OnRedone();
|
||||
}
|
||||
|
||||
protected virtual void OnTracked()
|
||||
{
|
||||
Tracked?.Invoke(this, new EventArgs());
|
||||
}
|
||||
|
||||
protected virtual void OnUndone()
|
||||
{
|
||||
Undone?.Invoke(this, new EventArgs());
|
||||
}
|
||||
|
||||
protected virtual void OnRedone()
|
||||
{
|
||||
Redone?.Invoke(this, new EventArgs());
|
||||
}
|
||||
}
|
||||
}
|
689
CnCTDRAMapEditor/Utility/WWCompression.cs
Normal file
689
CnCTDRAMapEditor/Utility/WWCompression.cs
Normal file
|
@ -0,0 +1,689 @@
|
|||
//
|
||||
// Copyright 2020 Electronic Arts Inc.
|
||||
//
|
||||
// The Command & Conquer Map Editor and corresponding source code is free
|
||||
// software: you can redistribute it and/or modify it under the terms of
|
||||
// the GNU General Public License as published by the Free Software Foundation,
|
||||
// either version 3 of the License, or (at your option) any later version.
|
||||
|
||||
// The Command & Conquer Map Editor and corresponding source code is distributed
|
||||
// in the hope that it will be useful, but with permitted additional restrictions
|
||||
// under Section 7 of the GPL. See the GNU General Public License in LICENSE.TXT
|
||||
// distributed with this program. You should have received a copy of the
|
||||
// GNU General Public License along with permitted additional restrictions
|
||||
// with this program. If not, see https://github.com/electronicarts/CnC_Remastered_Collection
|
||||
using System;
|
||||
|
||||
namespace MobiusEditor.Utility
|
||||
{
|
||||
/// <summary>
|
||||
/// This class contains encoders and decoders for the Westwood XOR Delta and LCW compression schemes.
|
||||
/// </summary>
|
||||
public static class WWCompression
|
||||
{
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Notes
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// LCW streams should always start and end with the fill command (& 0x80) though
|
||||
// the decompressor doesn't strictly require that it start with one the ability
|
||||
// to use the offset commands in place of the RLE command early in the stream
|
||||
// relies on it. Streams larger than 64k that need the relative versions of the
|
||||
// 3 and 5 byte commands should start with a null byte before the first 0x80
|
||||
// command to flag that they are relative compressed.
|
||||
//
|
||||
// LCW uses the following rules to decide which command to use:
|
||||
// 1. Runs of the same colour should only use 4 byte RLE command if longer than
|
||||
// 64 bytes. 2 and 3 byte offset commands are more efficient otherwise.
|
||||
// 2. Runs of less than 3 should just be stored as is with the one byte fill
|
||||
// command.
|
||||
// 3. Runs greater than 10 or if the relative offset is greater than
|
||||
// 4095 use an absolute copy. Less than 64 bytes uses 3 byte command, else it
|
||||
// uses the 5 byte command.
|
||||
// 4. If Absolute rule isn't met then copy from a relative offset with 2 byte
|
||||
// command.
|
||||
//
|
||||
// Absolute LCW can efficiently compress data that is 64k in size, much greater
|
||||
// and relative offsets for the 3 and 5 byte commands are needed.
|
||||
//
|
||||
// The XOR delta generator code works to the following assumptions
|
||||
//
|
||||
// 1. Any skip command is preferable if source and base are same
|
||||
// 2. Fill is preferable to XOR if 4 or larger, XOR takes same data plus at
|
||||
// least 1 byte
|
||||
//
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Some defines used by the encoders
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
public const Byte XOR_SMALL = 0x7F;
|
||||
public const Byte XOR_MED = 0xFF;
|
||||
public const Int32 XOR_LARGE = 0x3FFF;
|
||||
public const Int32 XOR_MAX = 0x7FFF;
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Some utility functions to get worst case sizes for buffer allocation
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
public static Int32 LCWWorstCase(Int32 datasize)
|
||||
{
|
||||
return datasize + (datasize / 63) + 1;
|
||||
}
|
||||
|
||||
public static Int32 XORWorstCase(Int32 datasize)
|
||||
{
|
||||
return datasize + ((datasize / 63) * 3) + 4;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compresses data to the proprietary LCW format used in
|
||||
/// many games developed by Westwood Studios. Compression is better
|
||||
/// than that achieved by popular community tools. This is a new
|
||||
/// implementation based on understanding of the compression gained from
|
||||
/// the reference code.
|
||||
/// </summary>
|
||||
/// <param name="input">Array of the data to compress.</param>
|
||||
/// <returns>The compressed data.</returns>
|
||||
/// <remarks>Commonly known in the community as "format80".</remarks>
|
||||
public static Byte[] LcwCompress(Byte[] input)
|
||||
{
|
||||
if (input == null || input.Length == 0)
|
||||
return new Byte[0];
|
||||
|
||||
//Decide if we are going to do relative offsets for 3 and 5 byte commands
|
||||
Boolean relative = input.Length > UInt16.MaxValue;
|
||||
|
||||
// Nyer's C# conversion: replacements for write and read for pointers.
|
||||
Int32 getp = 0;
|
||||
Int32 putp = 0;
|
||||
// Input length. Used commonly enough to warrant getting it out in advance I guess.
|
||||
Int32 getend = input.Length;
|
||||
// "Worst case length" code by OmniBlade. We'll just use a buffer of
|
||||
// that max length and cut it down to the actual used size at the end.
|
||||
// Not using it- it's not big enough in case of some small images.
|
||||
//LCWWorstCase(getend)
|
||||
Int32 worstcase = Math.Max(10000, getend * 2);
|
||||
Byte[] output = new Byte[worstcase];
|
||||
// relative LCW starts with 0 as flag to decoder.
|
||||
// this is only used by later games for decoding hi-color vqa files.
|
||||
if (relative)
|
||||
output[putp++] = 0;
|
||||
|
||||
//Implementations that properly conform to the WestWood encoder should
|
||||
//write a starting cmd1. It's important for using the offset copy commands
|
||||
//to do more efficient RLE in some cases than the cmd4.
|
||||
|
||||
//we also set bool to flag that we have an on going cmd1.
|
||||
Int32 cmd_onep = putp;
|
||||
output[putp++] = 0x81;
|
||||
output[putp++] = input[getp++];
|
||||
Boolean cmd_one = true;
|
||||
|
||||
//Compress data until we reach end of input buffer.
|
||||
while (getp < getend)
|
||||
{
|
||||
//Is RLE encode (4bytes) worth evaluating?
|
||||
if (getend - getp > 64 && input[getp] == input[getp + 64])
|
||||
{
|
||||
//RLE run length is encoded as a short so max is UINT16_MAX
|
||||
Int32 rlemax = (getend - getp) < UInt16.MaxValue ? getend : getp + UInt16.MaxValue;
|
||||
Int32 rlep = getp + 1;
|
||||
while (rlep < rlemax && input[rlep] == input[getp])
|
||||
rlep++;
|
||||
|
||||
UInt16 run_length = (UInt16)(rlep - getp);
|
||||
|
||||
//If run length is long enough, write the command and start loop again
|
||||
if (run_length >= 0x41)
|
||||
{
|
||||
//write 4byte command 0b11111110
|
||||
cmd_one = false;
|
||||
output[putp++] = 0xFE;
|
||||
output[putp++] = (Byte)(run_length & 0xFF);
|
||||
output[putp++] = (Byte)((run_length >> 8) & 0xFF);
|
||||
output[putp++] = input[getp];
|
||||
getp = rlep;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
//current block size for an offset copy
|
||||
UInt16 block_size = 0;
|
||||
//Set where we start looking for matching runs.
|
||||
Int32 offstart = relative ? getp < UInt16.MaxValue ? 0 : getp - UInt16.MaxValue : 0;
|
||||
|
||||
//Look for matching runs
|
||||
Int32 offchk = offstart;
|
||||
Int32 offsetp = getp;
|
||||
while (offchk < getp)
|
||||
{
|
||||
//Move offchk to next matching position
|
||||
while (offchk < getp && input[offchk] != input[getp])
|
||||
offchk++;
|
||||
|
||||
//If the checking pointer has reached current pos, break
|
||||
if (offchk >= getp)
|
||||
break;
|
||||
|
||||
//find out how long the run of matches goes for
|
||||
Int32 i;
|
||||
for (i = 1; getp + i < getend; ++i)
|
||||
if (input[offchk + i] != input[getp + i])
|
||||
break;
|
||||
if (i >= block_size)
|
||||
{
|
||||
block_size = (UInt16)i;
|
||||
offsetp = offchk;
|
||||
}
|
||||
offchk++;
|
||||
}
|
||||
|
||||
//decide what encoding to use for current run
|
||||
//If it's less than 2 bytes long, we store as is with cmd1
|
||||
if (block_size <= 2)
|
||||
{
|
||||
//short copy 0b10??????
|
||||
//check we have an existing 1 byte command and if its value is still
|
||||
//small enough to handle additional bytes
|
||||
//start a new command if current one doesn't have space or we don't
|
||||
//have one to continue
|
||||
if (cmd_one && output[cmd_onep] < 0xBF)
|
||||
{
|
||||
//increment command value
|
||||
output[cmd_onep]++;
|
||||
output[putp++] = input[getp++];
|
||||
}
|
||||
else
|
||||
{
|
||||
cmd_onep = putp;
|
||||
output[putp++] = 0x81;
|
||||
output[putp++] = input[getp++];
|
||||
cmd_one = true;
|
||||
}
|
||||
//Otherwise we need to decide what relative copy command is most efficient
|
||||
}
|
||||
else
|
||||
{
|
||||
Int32 offset;
|
||||
Int32 rel_offset = getp - offsetp;
|
||||
if (block_size > 0xA || ((rel_offset) > 0xFFF))
|
||||
{
|
||||
//write 5 byte command 0b11111111
|
||||
if (block_size > 0x40)
|
||||
{
|
||||
output[putp++] = 0xFF;
|
||||
output[putp++] = (Byte)(block_size & 0xFF);
|
||||
output[putp++] = (Byte)((block_size >> 8) & 0xFF);
|
||||
//write 3 byte command 0b11??????
|
||||
}
|
||||
else
|
||||
{
|
||||
output[putp++] = (Byte)((block_size - 3) | 0xC0);
|
||||
}
|
||||
|
||||
offset = relative ? rel_offset : offsetp;
|
||||
//write 2 byte command? 0b0???????
|
||||
}
|
||||
else
|
||||
{
|
||||
offset = rel_offset << 8 | (16 * (block_size - 3) + (rel_offset >> 8));
|
||||
}
|
||||
output[putp++] = (Byte)(offset & 0xFF);
|
||||
output[putp++] = (Byte)((offset >> 8) & 0xFF);
|
||||
getp += block_size;
|
||||
cmd_one = false;
|
||||
}
|
||||
}
|
||||
|
||||
//write final 0x80, basically an empty cmd1 to signal the end of the stream.
|
||||
output[putp++] = 0x80;
|
||||
|
||||
Byte[] finalOutput = new Byte[putp];
|
||||
Array.Copy(output, 0, finalOutput, 0, putp);
|
||||
// Return the final compressed data.
|
||||
return finalOutput;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Decompresses data in the proprietary LCW format used in many games
|
||||
/// developed by Westwood Studios.
|
||||
/// </summary>
|
||||
/// <param name="input">The data to decompress.</param>
|
||||
/// <param name="readOffset">Location to start at in the input array.</param>
|
||||
/// <param name="output">The buffer to store the decompressed data. This is assumed to be initialized to the correct size.</param>
|
||||
/// <param name="readEnd">End offset for reading. Use 0 to take the end of the given data array.</param>
|
||||
/// <returns>Length of the decompressed data in bytes.</returns>
|
||||
public static Int32 LcwDecompress(Byte[] input, ref Int32 readOffset, Byte[] output, Int32 readEnd)
|
||||
{
|
||||
if (input == null || input.Length == 0 || output == null || output.Length == 0)
|
||||
return 0;
|
||||
Boolean relative = false;
|
||||
// Nyer's C# conversion: replacements for write and read for pointers.
|
||||
Int32 writeOffset = 0;
|
||||
// Output length should be part of the information given in the file format using LCW.
|
||||
// Techncically it can just be cropped at the end, though this value is used to
|
||||
// automatically cut off repeat-commands that go too far.
|
||||
Int32 writeEnd = output.Length;
|
||||
if (readEnd <= 0)
|
||||
readEnd = input.Length;
|
||||
|
||||
//Decide if the stream uses relative 3 and 5 byte commands
|
||||
//Extension allows effective compression of data > 64k
|
||||
//https://github.com/madmoose/scummvm/blob/bladerunner/engines/bladerunner/decompress_lcw.cpp
|
||||
// this is only used by later games for decoding hi-color vqa files.
|
||||
// For other stuff (like shp), just check in advance to decide if the data is too big.
|
||||
if (readOffset >= readEnd)
|
||||
return writeOffset;
|
||||
if (input[readOffset] == 0)
|
||||
{
|
||||
relative = true;
|
||||
readOffset++;
|
||||
}
|
||||
//DEBUG_SAY("LCW Decompression... \n");
|
||||
while (writeOffset < writeEnd)
|
||||
{
|
||||
if (readOffset >= readEnd)
|
||||
return writeOffset;
|
||||
Byte flag = input[readOffset++];
|
||||
UInt16 cpysize;
|
||||
UInt16 offset;
|
||||
|
||||
if ((flag & 0x80) != 0)
|
||||
{
|
||||
if ((flag & 0x40) != 0)
|
||||
{
|
||||
cpysize = (UInt16)((flag & 0x3F) + 3);
|
||||
//long set 0b11111110
|
||||
if (flag == 0xFE)
|
||||
{
|
||||
if (readOffset >= readEnd)
|
||||
return writeOffset;
|
||||
cpysize = input[readOffset++];
|
||||
if (readOffset >= readEnd)
|
||||
return writeOffset;
|
||||
cpysize += (UInt16)((input[readOffset++]) << 8);
|
||||
if (cpysize > writeEnd - writeOffset)
|
||||
cpysize = (UInt16)(writeEnd - writeOffset);
|
||||
if (readOffset >= readEnd)
|
||||
return writeOffset;
|
||||
//DEBUG_SAY("0b11111110 Source Pos %ld, Dest Pos %ld, Count %d\n", source - sstart - 3, dest - start, cpysize);
|
||||
for (; cpysize > 0; --cpysize)
|
||||
{
|
||||
if (writeOffset >= writeEnd)
|
||||
return writeOffset;
|
||||
output[writeOffset++] = input[readOffset];
|
||||
}
|
||||
readOffset++;
|
||||
}
|
||||
else
|
||||
{
|
||||
Int32 s;
|
||||
//long move, abs 0b11111111
|
||||
if (flag == 0xFF)
|
||||
{
|
||||
if (readOffset >= readEnd)
|
||||
return writeOffset;
|
||||
cpysize = input[readOffset++];
|
||||
if (readOffset >= readEnd)
|
||||
return writeOffset;
|
||||
cpysize += (UInt16)((input[readOffset++]) << 8);
|
||||
if (cpysize > writeEnd - writeOffset)
|
||||
cpysize = (UInt16)(writeEnd - writeOffset);
|
||||
if (readOffset >= readEnd)
|
||||
return writeOffset;
|
||||
offset = input[readOffset++];
|
||||
if (readOffset >= readEnd)
|
||||
return writeOffset;
|
||||
offset += (UInt16)((input[readOffset++]) << 8);
|
||||
//extended format for VQA32
|
||||
if (relative)
|
||||
s = writeOffset - offset;
|
||||
else
|
||||
s = offset;
|
||||
//DEBUG_SAY("0b11111111 Source Pos %ld, Dest Pos %ld, Count %d, Offset %d\n", source - sstart - 5, dest - start, cpysize, offset);
|
||||
for (; cpysize > 0; --cpysize)
|
||||
{
|
||||
if (writeOffset >= writeEnd)
|
||||
return writeOffset;
|
||||
output[writeOffset++] = output[s++];
|
||||
}
|
||||
//short move abs 0b11??????
|
||||
}
|
||||
else
|
||||
{
|
||||
if (cpysize > writeEnd - writeOffset)
|
||||
cpysize = (UInt16)(writeEnd - writeOffset);
|
||||
if (readOffset >= readEnd)
|
||||
return writeOffset;
|
||||
offset = input[readOffset++];
|
||||
if (readOffset >= readEnd)
|
||||
return writeOffset;
|
||||
offset += (UInt16)((input[readOffset++]) << 8);
|
||||
//extended format for VQA32
|
||||
if (relative)
|
||||
s = writeOffset - offset;
|
||||
else
|
||||
s = offset;
|
||||
//DEBUG_SAY("0b11?????? Source Pos %ld, Dest Pos %ld, Count %d, Offset %d\n", source - sstart - 3, dest - start, cpysize, offset);
|
||||
for (; cpysize > 0; --cpysize)
|
||||
{
|
||||
if (writeOffset >= writeEnd)
|
||||
return writeOffset;
|
||||
output[writeOffset++] = output[s++];
|
||||
}
|
||||
}
|
||||
}
|
||||
//short copy 0b10??????
|
||||
}
|
||||
else
|
||||
{
|
||||
if (flag == 0x80)
|
||||
{
|
||||
//DEBUG_SAY("0b10?????? Source Pos %ld, Dest Pos %ld, Count %d\n", source - sstart - 1, dest - start, 0);
|
||||
return writeOffset;
|
||||
}
|
||||
cpysize = (UInt16)(flag & 0x3F);
|
||||
if (cpysize > writeEnd - writeOffset)
|
||||
cpysize = (UInt16)(writeEnd - writeOffset);
|
||||
//DEBUG_SAY("0b10?????? Source Pos %ld, Dest Pos %ld, Count %d\n", source - sstart - 1, dest - start, cpysize);
|
||||
for (; cpysize > 0; --cpysize)
|
||||
{
|
||||
if (readOffset >= readEnd || writeOffset >= writeEnd)
|
||||
return writeOffset;
|
||||
output[writeOffset++] = input[readOffset++];
|
||||
}
|
||||
}
|
||||
//short move rel 0b0???????
|
||||
}
|
||||
else
|
||||
{
|
||||
cpysize = (UInt16)((flag >> 4) + 3);
|
||||
if (cpysize > writeEnd - writeOffset)
|
||||
cpysize = (UInt16)(writeEnd - writeOffset);
|
||||
if (readOffset >= readEnd)
|
||||
return writeOffset;
|
||||
offset = (UInt16)(((flag & 0xF) << 8) + input[readOffset++]);
|
||||
//DEBUG_SAY("0b0??????? Source Pos %ld, Dest Pos %ld, Count %d, Offset %d\n", source - sstart - 2, dest - start, cpysize, offset);
|
||||
for (; cpysize > 0; --cpysize)
|
||||
{
|
||||
if (writeOffset >= writeEnd || writeOffset < offset)
|
||||
return writeOffset;
|
||||
output[writeOffset] = output[writeOffset - offset];
|
||||
writeOffset++;
|
||||
}
|
||||
}
|
||||
}
|
||||
// If buffer is full, make sure to skip end command!
|
||||
if (writeOffset == writeEnd && readOffset < input.Length && input[readOffset] == 0x80)
|
||||
readOffset++;
|
||||
return writeOffset;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a binary delta between two buffers. Mainly used for image data.
|
||||
/// </summary>
|
||||
/// <param name="source">Buffer containing data to generate the delta for.</param>
|
||||
/// <param name="base">Buffer containing data that is the base for the delta.</param>
|
||||
/// <returns>The generated delta as bytes array.</returns>
|
||||
/// <remarks>Commonly known in the community as "format40".</remarks>
|
||||
public static Byte[] GenerateXorDelta(Byte[] source, Byte[] @base)
|
||||
{
|
||||
// Nyer's C# conversion: replacements for write and read for pointers.
|
||||
// -for our delta (output)
|
||||
Int32 putp = 0;
|
||||
// -for the image we go to
|
||||
Int32 getsp = 0;
|
||||
// -for the image we come from
|
||||
Int32 getbp = 0;
|
||||
//Length to process
|
||||
Int32 getsendp = Math.Min(source.Length, @base.Length);
|
||||
Byte[] dest = new Byte[XORWorstCase(getsendp)];
|
||||
|
||||
//Only check getsp to save a redundant check.
|
||||
//Both source and base should be same size and both pointers should be
|
||||
//incremented at the same time.
|
||||
while (getsp < getsendp)
|
||||
{
|
||||
UInt32 fillcount = 0;
|
||||
UInt32 xorcount = 0;
|
||||
UInt32 skipcount = 0;
|
||||
Byte lastxor = (Byte)(source[getsp] ^ @base[getbp]);
|
||||
Int32 testsp = getsp;
|
||||
Int32 testbp = getbp;
|
||||
|
||||
//Only evaluate other options if we don't have a matched pair
|
||||
while (testsp < getsendp && source[testsp] != @base[testbp])
|
||||
{
|
||||
if ((source[testsp] ^ @base[testbp]) == lastxor)
|
||||
{
|
||||
++fillcount;
|
||||
++xorcount;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (fillcount > 3)
|
||||
break;
|
||||
lastxor = (Byte)(source[testsp] ^ @base[testbp]);
|
||||
fillcount = 1;
|
||||
++xorcount;
|
||||
}
|
||||
testsp++;
|
||||
testbp++;
|
||||
}
|
||||
|
||||
//fillcount should always be lower than xorcount and should be greater
|
||||
//than 3 to warrant using the fill commands.
|
||||
fillcount = fillcount > 3 ? fillcount : 0;
|
||||
|
||||
//Okay, lets see if we have any xor bytes we need to handle
|
||||
xorcount -= fillcount;
|
||||
while (xorcount != 0)
|
||||
{
|
||||
UInt16 count;
|
||||
//It's cheaper to do the small cmd twice than do the large cmd once
|
||||
//for data that can be handled by two small cmds.
|
||||
//cmd 0???????
|
||||
if (xorcount < XOR_MED)
|
||||
{
|
||||
count = (UInt16)(xorcount <= XOR_SMALL ? xorcount : XOR_SMALL);
|
||||
dest[putp++] = (Byte)count;
|
||||
//cmd 10000000 10?????? ??????
|
||||
}
|
||||
else
|
||||
{
|
||||
count = (UInt16)(xorcount <= XOR_LARGE ? xorcount : XOR_LARGE);
|
||||
dest[putp++] = 0x80;
|
||||
dest[putp++] = (Byte)(count & 0xFF);
|
||||
dest[putp++] = (Byte)(((count >> 8) & 0xFF) | 0x80);
|
||||
}
|
||||
|
||||
while (count != 0)
|
||||
{
|
||||
dest[putp++] = (Byte)(source[getsp++] ^ @base[getbp++]);
|
||||
count--;
|
||||
xorcount--;
|
||||
}
|
||||
}
|
||||
|
||||
//lets handle the bytes that are best done as xorfill
|
||||
while (fillcount != 0)
|
||||
{
|
||||
UInt16 count;
|
||||
//cmd 00000000 ????????
|
||||
if (fillcount <= XOR_MED)
|
||||
{
|
||||
count = (UInt16)fillcount;
|
||||
dest[putp++] = 0;
|
||||
dest[putp++] = (Byte)(count & 0xFF);
|
||||
//cmd 10000000 11?????? ??????
|
||||
}
|
||||
else
|
||||
{
|
||||
count = (UInt16)(fillcount <= XOR_LARGE ? fillcount : XOR_LARGE);
|
||||
dest[putp++] = 0x80;
|
||||
dest[putp++] = (Byte)(count & 0xFF);
|
||||
dest[putp++] = (Byte)(((count >> 8) & 0xFF) | 0xC0);
|
||||
}
|
||||
dest[putp++] = (Byte)(source[getsp] ^ @base[getbp]);
|
||||
fillcount -= count;
|
||||
getsp += count;
|
||||
getbp += count;
|
||||
}
|
||||
|
||||
//Handle regions that match exactly
|
||||
while (testsp < getsendp && source[testsp] == @base[testbp])
|
||||
{
|
||||
skipcount++;
|
||||
testsp++;
|
||||
testbp++;
|
||||
}
|
||||
|
||||
while (skipcount != 0)
|
||||
{
|
||||
UInt16 count;
|
||||
//Again it's cheaper to do the small cmd twice than do the large cmd
|
||||
//once for data that can be handled by two small cmds.
|
||||
//cmd 1???????
|
||||
if (skipcount < XOR_MED)
|
||||
{
|
||||
count = (Byte)(skipcount <= XOR_SMALL ? skipcount : XOR_SMALL);
|
||||
dest[putp++] = (Byte)(count | 0x80);
|
||||
//cmd 10000000 0??????? ????????
|
||||
}
|
||||
else
|
||||
{
|
||||
count = (UInt16)(skipcount <= XOR_MAX ? skipcount : XOR_MAX);
|
||||
dest[putp++] = 0x80;
|
||||
dest[putp++] = (Byte)(count & 0xFF);
|
||||
dest[putp++] = (Byte)((count >> 8) & 0xFF);
|
||||
}
|
||||
skipcount -= count;
|
||||
getsp += count;
|
||||
getbp += count;
|
||||
}
|
||||
}
|
||||
|
||||
//final skip command of 0 to signal end of stream.
|
||||
dest[putp++] = 0x80;
|
||||
dest[putp++] = 0;
|
||||
dest[putp++] = 0;
|
||||
|
||||
Byte[] finalOutput = new Byte[putp];
|
||||
Array.Copy(dest, 0, finalOutput, 0, putp);
|
||||
// Return the final data
|
||||
return finalOutput;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies a binary delta to a buffer.
|
||||
/// </summary>
|
||||
/// <param name="data">The data to apply the xor to.</param>
|
||||
/// <param name="xorSource">The the delta data to apply.</param>
|
||||
/// <param name="xorStart">Start offset in the data.</param>
|
||||
/// <param name="xorEnd">End offset in the data. Use 0 to take the end of the whole array.</param>
|
||||
public static void ApplyXorDelta(Byte[] data, Byte[] xorSource, ref Int32 xorStart, Int32 xorEnd)
|
||||
{
|
||||
// Nyer's C# conversion: replacements for write and read for pointers.
|
||||
Int32 putp = 0;
|
||||
Byte value = 0;
|
||||
Int32 dataEnd = data.Length;
|
||||
if (xorEnd <= 0)
|
||||
xorEnd = xorSource.Length;
|
||||
while (putp < dataEnd && xorStart < xorEnd)
|
||||
{
|
||||
//DEBUG_SAY("XOR_Delta Put pos: %u, Get pos: %u.... ", putp - scast<sint8*>(dest), getp - scast<sint8*>(source));
|
||||
Byte cmd = xorSource[xorStart++];
|
||||
UInt16 count = cmd;
|
||||
Boolean xorval = false;
|
||||
|
||||
if ((cmd & 0x80) == 0)
|
||||
{
|
||||
//0b00000000
|
||||
if (cmd == 0)
|
||||
{
|
||||
if (xorStart >= xorEnd)
|
||||
return;
|
||||
count = (UInt16)(xorSource[xorStart++] & 0xFF);
|
||||
if (xorStart >= xorEnd)
|
||||
return;
|
||||
value = xorSource[xorStart++];
|
||||
xorval = true;
|
||||
//DEBUG_SAY("0b00000000 Val Count %d ", count);
|
||||
//0b0???????
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
//0b1??????? remove most significant bit
|
||||
count &= 0x7F;
|
||||
if (count != 0)
|
||||
{
|
||||
putp += count;
|
||||
//DEBUG_SAY("0b1??????? Skip Count %d\n", count);
|
||||
continue;
|
||||
}
|
||||
if (xorStart >= xorEnd)
|
||||
return;
|
||||
count = (UInt16) (xorSource[xorStart++] & 0xFF);
|
||||
if (xorStart >= xorEnd)
|
||||
return;
|
||||
count += (UInt16) (xorSource[xorStart++] << 8);
|
||||
|
||||
//0b10000000 0 0
|
||||
if (count == 0)
|
||||
{
|
||||
//DEBUG_SAY("0b10000000 Count %d to end delta\n", count);
|
||||
return;
|
||||
}
|
||||
|
||||
//0b100000000 0?
|
||||
if ((count & 0x8000) == 0)
|
||||
{
|
||||
putp += count;
|
||||
//DEBUG_SAY("0b100000000 0? Skip Count %d\n", count);
|
||||
continue;
|
||||
}
|
||||
//0b10000000 11
|
||||
if ((count & 0x4000) != 0)
|
||||
{
|
||||
count &= 0x3FFF;
|
||||
if (xorStart >= xorEnd)
|
||||
return;
|
||||
value = xorSource[xorStart++];
|
||||
//DEBUG_SAY("0b10000000 11 Val Count %d ", count);
|
||||
xorval = true;
|
||||
//0b10000000 10
|
||||
}
|
||||
else
|
||||
{
|
||||
count &= 0x3FFF;
|
||||
//DEBUG_SAY("0b10000000 10 XOR Count %d ", count);
|
||||
}
|
||||
}
|
||||
|
||||
if (xorval)
|
||||
{
|
||||
//DEBUG_SAY("XOR Val %d\n", value);
|
||||
for (; count > 0; --count)
|
||||
{
|
||||
if (putp >= dataEnd)
|
||||
return;
|
||||
data[putp++] ^= value;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
//DEBUG_SAY("XOR Source to Dest\n");
|
||||
for (; count > 0; --count)
|
||||
{
|
||||
if (putp >= dataEnd || xorStart >= xorEnd)
|
||||
return;
|
||||
data[putp++] ^= xorSource[xorStart++];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
Reference in a new issue