C&C Remastered Map Editor

Initial commit of C&C Remastered Map Editor code
This commit is contained in:
PG-SteveT 2020-09-10 11:12:58 -07:00
parent 1f6350fe6e
commit e37e174be1
289 changed files with 80922 additions and 7 deletions

View 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;
}
}

View 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;
}
}
}

View 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];
}
}
}
}
}

View 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);
}
}

View 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);
}
}
}

View 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
}
}

View 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);
}
}
}
}
}

View 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
}
}

View 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;
}
}
}

View 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);
}
}
}

File diff suppressed because it is too large Load diff

View 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;
}
}
}

View 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;
}
}
}

View 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);
}
}
}
}
}

View 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;
}
}
}

View 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);
}
}
}

View 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());
}
}
}

View 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++];
}
}
}
}
}
}