This rule no longer appears to be buggy, so enforce it. Some of the automated fixes are adjusted in order to improve the result. #pragma directives have no option to control indentation, so remove them where possible.
575 lines
19 KiB
C#
575 lines
19 KiB
C#
#region Copyright & License Information
|
|
/*
|
|
* Copyright (c) The OpenRA Developers and Contributors
|
|
* This file is part of OpenRA, which is free software. It is made
|
|
* available to you 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. For more
|
|
* information, see COPYING.
|
|
*/
|
|
#endregion
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using OpenRA.FileSystem;
|
|
using OpenRA.Mods.Cnc.FileFormats;
|
|
using OpenRA.Mods.Common;
|
|
using OpenRA.Mods.Common.FileFormats;
|
|
using OpenRA.Mods.Common.Terrain;
|
|
using OpenRA.Mods.Common.Traits;
|
|
using OpenRA.Primitives;
|
|
using OpenRA.Traits;
|
|
|
|
namespace OpenRA.Mods.Cnc.UtilityCommands
|
|
{
|
|
public abstract class ImportGen2MapCommand
|
|
{
|
|
// The top few map rows on the original games are visible but not part of the interactable map area.
|
|
// These are imported as being outside the map bounds but within the shroud visible margin,
|
|
// which requires some extra padding to make the PPos calculations work.
|
|
// The first value is added to the left, right, and bottom edges. The second value is added to the top.
|
|
// There are 6 tiles in the top uninteractable region (3 in og TS, double for OpenRA).
|
|
// So far testing hasn't shown the need for a left/right/bottom uninteractable margin except in cases of odd elevation, which affects us but not the original games.
|
|
protected virtual int2 UninteractableMargin { get; } = new int2(0, 6);
|
|
|
|
// Having the top cordon equal the maximum possible elevation for the mod is important!
|
|
// It needs to be equal to the top row's maximum elevation because there may be an actor there that will later crash the game.
|
|
// But since we need to create the map's canvas before importing tile data (like elevation), we opt for the maximum possible elevation to be safe.
|
|
protected virtual (int Left, int Top, int Right, int Bottom) Cordon { get; } = (1, 16, 1, 4);
|
|
|
|
protected abstract Dictionary<byte, string> OverlayToActor { get; }
|
|
|
|
protected abstract Dictionary<byte, Size> OverlayShapes { get; }
|
|
|
|
protected abstract Dictionary<byte, DamageState> OverlayToHealth { get; }
|
|
|
|
protected abstract Dictionary<byte, byte[]> ResourceFromOverlay { get; }
|
|
|
|
protected abstract Dictionary<string, string> DeployableActors { get; }
|
|
|
|
protected abstract string[] LampActors { get; }
|
|
|
|
protected abstract string[] CreepActors { get; }
|
|
|
|
protected void Run(Utility utility, string[] args)
|
|
{
|
|
// HACK: The engine code assumes that Game.modData is set.
|
|
Game.ModData = utility.ModData;
|
|
|
|
var filename = args[1];
|
|
var file = new IniFile(File.Open(args[1], FileMode.Open));
|
|
var basic = file.GetSection("Basic");
|
|
var mapSection = file.GetSection("Map");
|
|
var tileset = mapSection.GetValue("Theater", "");
|
|
var iniSize = mapSection.GetValue("Size", "0, 0, 0, 0").Split(',').Select(int.Parse).ToArray();
|
|
var iniBounds = mapSection.GetValue("LocalSize", "0, 0, 0, 0").Split(',').Select(int.Parse).ToArray();
|
|
|
|
if (!utility.ModData.DefaultTerrainInfo.TryGetValue(tileset, out var terrainInfo))
|
|
throw new InvalidDataException($"Unknown tileset {tileset}");
|
|
|
|
var usedAreaSize = new Size(iniSize[2], 2 * iniSize[3]);
|
|
var mapCanvasSize = new Size(usedAreaSize.Width + Cordon.Left + Cordon.Right, usedAreaSize.Height + Cordon.Top + Cordon.Bottom);
|
|
|
|
var map = new Map(Game.ModData, terrainInfo, mapCanvasSize.Width, mapCanvasSize.Height)
|
|
{
|
|
Title = basic.GetValue("Name", Path.GetFileNameWithoutExtension(filename)),
|
|
Author = "Westwood Studios",
|
|
RequiresMod = utility.ModData.Manifest.Id
|
|
};
|
|
|
|
var fullSize = new int2(iniSize[2], iniSize[3]);
|
|
ReadTiles(map, file, fullSize);
|
|
ReadActors(map, file, "Structures", fullSize);
|
|
ReadActors(map, file, "Units", fullSize);
|
|
ReadActors(map, file, "Infantry", fullSize);
|
|
ReadTerrainActors(map, file, fullSize);
|
|
ReadWaypoints(map, file, fullSize);
|
|
ReadOverlay(map, file, fullSize);
|
|
ReadLighting(map, file);
|
|
ReadLamps(map, file);
|
|
|
|
var spawnCount = map.ActorDefinitions.Count(n => n.Value.Value == "mpspawn");
|
|
var mapPlayers = new MapPlayers(map.Rules, spawnCount);
|
|
map.PlayerDefinitions = mapPlayers.ToMiniYaml();
|
|
|
|
// This needs to be called after ReadTiles because it depends on terrain elevation.
|
|
SetInteractableBounds(map, iniBounds);
|
|
|
|
var dest = Path.GetFileNameWithoutExtension(args[1]) + ".oramap";
|
|
map.Save(ZipFileLoader.Create(dest));
|
|
Console.WriteLine(dest + " saved.");
|
|
}
|
|
|
|
protected virtual void ReadTiles(Map map, IniFile file, int2 fullSize)
|
|
{
|
|
var terrainInfo = (ITemplatedTerrainInfo)Game.ModData.DefaultTerrainInfo[map.Tileset];
|
|
var mapSection = file.GetSection("IsoMapPack5");
|
|
|
|
var data = Convert.FromBase64String(string.Concat(mapSection.Select(kvp => kvp.Value)));
|
|
var cells = (fullSize.X * 2 - 1) * fullSize.Y;
|
|
var lzoPackSize = cells * 11 + 4; // The last 4 bytes contain a LZO pack header saying no more data is left.
|
|
var isoMapPack = new byte[lzoPackSize];
|
|
UnpackLZO(data, isoMapPack);
|
|
|
|
var mf = new MemoryStream(isoMapPack);
|
|
for (var i = 0; i < cells; i++)
|
|
{
|
|
var rx = mf.ReadUInt16();
|
|
var ry = mf.ReadUInt16();
|
|
var tilenum = mf.ReadUInt16();
|
|
mf.ReadInt16(); // zero1
|
|
var subtile = mf.ReadUInt8();
|
|
var z = mf.ReadUInt8();
|
|
mf.ReadUInt8(); // zero2
|
|
|
|
var uv = ToMPos(rx, ry, fullSize.X);
|
|
|
|
if (map.Tiles.Contains(uv))
|
|
{
|
|
if (!terrainInfo.Templates.ContainsKey(tilenum))
|
|
tilenum = subtile = 0;
|
|
|
|
map.Tiles[uv] = new TerrainTile(tilenum, subtile);
|
|
map.Height[uv] = z;
|
|
}
|
|
}
|
|
}
|
|
|
|
protected virtual void ReadOverlay(Map map, IniFile file, int2 fullSize)
|
|
{
|
|
var overlaySection = file.GetSection("OverlayPack");
|
|
var overlayCompressed = Convert.FromBase64String(string.Concat(overlaySection.Select(kvp => kvp.Value)));
|
|
var overlayPack = new byte[1 << 18];
|
|
var temp = new byte[1 << 18];
|
|
UnpackLCW(overlayCompressed, overlayPack, temp);
|
|
|
|
var overlayDataSection = file.GetSection("OverlayDataPack");
|
|
var overlayDataCompressed = Convert.FromBase64String(string.Concat(overlayDataSection.Select(kvp => kvp.Value)));
|
|
var overlayDataPack = new byte[1 << 18];
|
|
UnpackLCW(overlayDataCompressed, overlayDataPack, temp);
|
|
|
|
var overlayIndex = new CellLayer<int>(map);
|
|
overlayIndex.Clear(0xFF);
|
|
|
|
for (var y = 0; y < fullSize.Y; y++)
|
|
{
|
|
for (var x = fullSize.X * 2 - 2; x >= 0; x--)
|
|
{
|
|
var dx = (ushort)x;
|
|
var dy = (ushort)(y * 2 + x % 2);
|
|
|
|
var uv = ToMPos(dx, dy);
|
|
var rx = (ushort)((dx + dy) / 2 + 1);
|
|
var ry = (ushort)(dy - rx + fullSize.X + 1);
|
|
|
|
if (!map.Resources.Contains(uv))
|
|
continue;
|
|
|
|
overlayIndex[uv] = rx + 512 * ry;
|
|
}
|
|
}
|
|
|
|
var nodes = new List<MiniYamlNode>();
|
|
foreach (var cell in map.AllCells)
|
|
{
|
|
var overlayType = overlayPack[overlayIndex[cell]];
|
|
if (overlayType == 0xFF)
|
|
continue;
|
|
|
|
if (TryHandleOverlayToActorInner(cell, overlayPack, overlayIndex, overlayType, out var ar))
|
|
{
|
|
if (ar != null)
|
|
nodes.Add(new MiniYamlNode("Actor" + (map.ActorDefinitions.Count + nodes.Count), ar.Save()));
|
|
|
|
continue;
|
|
}
|
|
|
|
if (TryHandleResourceFromOverlayInner(overlayType, overlayDataPack[overlayIndex[cell]], out var resourceTile))
|
|
{
|
|
map.Resources[cell] = resourceTile;
|
|
continue;
|
|
}
|
|
|
|
if (TryHandleOtherOverlayInner(map, cell, overlayDataPack, overlayIndex, overlayType))
|
|
continue;
|
|
|
|
Console.WriteLine($"Cell {cell}: unknown overlay {overlayType}");
|
|
}
|
|
|
|
map.ActorDefinitions = map.ActorDefinitions.Concat(nodes).ToArray();
|
|
}
|
|
|
|
protected virtual void ReadWaypoints(Map map, IniFile file, int2 fullSize)
|
|
{
|
|
var nodes = new List<MiniYamlNode>();
|
|
var waypointsSection = file.GetSection("Waypoints", true);
|
|
foreach (var kv in waypointsSection)
|
|
{
|
|
var pos = Exts.ParseInt32Invariant(kv.Value);
|
|
var ry = pos / 1000;
|
|
var rx = pos - ry * 1000;
|
|
var cell = ToMPos(rx, ry, fullSize.X).ToCPos(map);
|
|
|
|
var ar = new ActorReference((!int.TryParse(kv.Key, out var wpindex) || wpindex > 7) ? "waypoint" : "mpspawn")
|
|
{
|
|
new LocationInit(cell),
|
|
new OwnerInit("Neutral")
|
|
};
|
|
|
|
nodes.Add(new MiniYamlNode("Actor" + (map.ActorDefinitions.Count + nodes.Count), ar.Save()));
|
|
}
|
|
|
|
map.ActorDefinitions = map.ActorDefinitions.Concat(nodes).ToArray();
|
|
}
|
|
|
|
protected virtual void ReadTerrainActors(Map map, IniFile file, int2 fullSize)
|
|
{
|
|
var nodes = new List<MiniYamlNode>();
|
|
var terrainSection = file.GetSection("Terrain", true);
|
|
foreach (var kv in terrainSection)
|
|
{
|
|
var pos = Exts.ParseInt32Invariant(kv.Key);
|
|
var ry = pos / 1000;
|
|
var rx = pos - ry * 1000;
|
|
var cell = ToMPos(rx, ry, fullSize.X).ToCPos(map);
|
|
var name = kv.Value.ToLowerInvariant();
|
|
|
|
var ar = new ActorReference(name)
|
|
{
|
|
new LocationInit(cell),
|
|
new OwnerInit("Neutral")
|
|
};
|
|
|
|
if (!map.Rules.Actors.ContainsKey(name))
|
|
Console.WriteLine($"Ignoring unknown actor type: `{name}`");
|
|
else
|
|
nodes.Add(new MiniYamlNode("Actor" + (map.ActorDefinitions.Count + nodes.Count), ar.Save()));
|
|
}
|
|
|
|
map.ActorDefinitions = map.ActorDefinitions.Concat(nodes).ToArray();
|
|
}
|
|
|
|
protected virtual void ReadActors(Map map, IniFile file, string type, int2 fullSize)
|
|
{
|
|
var nodes = new List<MiniYamlNode>();
|
|
var structuresSection = file.GetSection(type, true);
|
|
foreach (var kv in structuresSection)
|
|
{
|
|
var isDeployed = false;
|
|
var entries = kv.Value.Split(',');
|
|
|
|
var name = entries[1].ToLowerInvariant();
|
|
|
|
if (DeployableActors.ContainsKey(name))
|
|
{
|
|
name = DeployableActors[name];
|
|
isDeployed = true;
|
|
}
|
|
|
|
var health = Exts.ParseInt16Invariant(entries[2]);
|
|
var rx = Exts.ParseInt32Invariant(entries[3]);
|
|
var ry = Exts.ParseInt32Invariant(entries[4]);
|
|
var facing = (byte)(224 - Exts.ParseByteInvariant(entries[type == "Infantry" ? 7 : 5]));
|
|
|
|
var cell = ToMPos(rx, ry, fullSize.X).ToCPos(map);
|
|
|
|
var ar = new ActorReference(name)
|
|
{
|
|
new LocationInit(cell),
|
|
new OwnerInit(CreepActors.Contains(entries[1]) ? "Creeps" : "Neutral")
|
|
};
|
|
|
|
if (type == "Infantry")
|
|
{
|
|
var subcell = 0;
|
|
switch (Exts.ParseByteInvariant(entries[5]))
|
|
{
|
|
case 2: subcell = 3; break;
|
|
case 3: subcell = 1; break;
|
|
case 4: subcell = 2; break;
|
|
}
|
|
|
|
if (subcell != 0)
|
|
ar.Add(new SubCellInit((SubCell)subcell));
|
|
}
|
|
|
|
if (health != 256)
|
|
ar.Add(new HealthInit(100 * health / 256));
|
|
|
|
ar.Add(new FacingInit(WAngle.FromFacing(facing)));
|
|
|
|
if (isDeployed)
|
|
ar.Add(new DeployStateInit(DeployState.Deployed));
|
|
|
|
if (!map.Rules.Actors.ContainsKey(name))
|
|
Console.WriteLine($"Ignoring unknown actor type: `{name}`");
|
|
else
|
|
nodes.Add(new MiniYamlNode("Actor" + (map.ActorDefinitions.Count + nodes.Count), ar.Save()));
|
|
}
|
|
|
|
map.ActorDefinitions = map.ActorDefinitions.Concat(nodes).ToArray();
|
|
}
|
|
|
|
protected virtual void ReadLighting(Map map, IniFile file)
|
|
{
|
|
var lightingTypes = new Dictionary<string, string>()
|
|
{
|
|
{ "Red", "RedTint" },
|
|
{ "Green", "GreenTint" },
|
|
{ "Blue", "BlueTint" },
|
|
{ "Ambient", "Intensity" },
|
|
{ "Level", "HeightStep" },
|
|
{ "Ground", null }
|
|
};
|
|
|
|
var lightingSection = file.GetSection("Lighting");
|
|
var parsed = new Dictionary<string, float>();
|
|
var lightingNodes = new List<MiniYamlNode>();
|
|
|
|
foreach (var kv in lightingSection)
|
|
{
|
|
if (lightingTypes.ContainsKey(kv.Key))
|
|
parsed[kv.Key] = FieldLoader.GetValue<float>(kv.Key, kv.Value);
|
|
else
|
|
Console.WriteLine($"Ignoring unknown lighting type: `{kv.Key}`");
|
|
}
|
|
|
|
// Merge Ground into Ambient
|
|
if (parsed.TryGetValue("Ground", out var ground))
|
|
{
|
|
if (!parsed.ContainsKey("Ambient"))
|
|
parsed["Ambient"] = 1f;
|
|
parsed["Ambient"] -= ground;
|
|
}
|
|
|
|
foreach (var node in lightingTypes)
|
|
{
|
|
if (node.Value != null && parsed.TryGetValue(node.Key, out var val) && ((node.Key == "Level" && val != 0) || (node.Key != "Level" && val != 1.0f)))
|
|
lightingNodes.Add(new MiniYamlNode(node.Value, FieldSaver.FormatValue(val)));
|
|
}
|
|
|
|
if (lightingNodes.Count > 0)
|
|
{
|
|
map.RuleDefinitions = map.RuleDefinitions.WithNodesAppended(new[]
|
|
{
|
|
new MiniYamlNode("^BaseWorld", new MiniYaml("", new[]
|
|
{
|
|
new MiniYamlNode("TerrainLighting", new MiniYaml("", lightingNodes))
|
|
}))
|
|
});
|
|
}
|
|
}
|
|
|
|
protected virtual void ReadLamps(Map map, IniFile file)
|
|
{
|
|
var lightingTypes = new Dictionary<string, string>()
|
|
{
|
|
{ "LightIntensity", "Intensity" },
|
|
{ "LightRedTint", "RedTint" },
|
|
{ "LightGreenTint", "GreenTint" },
|
|
{ "LightBlueTint", "BlueTint" },
|
|
};
|
|
|
|
var nodes = new List<MiniYamlNode>();
|
|
foreach (var lamp in LampActors)
|
|
{
|
|
var lightingSection = file.GetSection(lamp, true);
|
|
var lightingNodes = new List<MiniYamlNode>();
|
|
|
|
foreach (var kv in lightingSection)
|
|
{
|
|
if (kv.Key == "LightVisibility")
|
|
{
|
|
// Convert leptons to WDist
|
|
var visibility = FieldLoader.GetValue<int>(kv.Key, kv.Value);
|
|
lightingNodes.Add(new MiniYamlNode("Range", FieldSaver.FormatValue(new WDist(visibility * 4))));
|
|
}
|
|
else if (lightingTypes.TryGetValue(kv.Key, out var lightingType))
|
|
{
|
|
// Some maps use "," instead of "."!
|
|
var value = FieldLoader.GetValue<float>(kv.Key, kv.Value.Replace(',', '.'));
|
|
lightingNodes.Add(new MiniYamlNode(lightingType, FieldSaver.FormatValue(value)));
|
|
}
|
|
}
|
|
|
|
if (lightingNodes.Count > 0)
|
|
{
|
|
nodes.Add(new MiniYamlNode(lamp, new MiniYaml("", new[]
|
|
{
|
|
new MiniYamlNode("TerrainLightSource", new MiniYaml("", lightingNodes))
|
|
})));
|
|
}
|
|
}
|
|
|
|
map.RuleDefinitions = map.RuleDefinitions.WithNodesAppended(nodes);
|
|
}
|
|
|
|
protected virtual void SetInteractableBounds(Map map, int[] iniBounds)
|
|
{
|
|
// Apply cordon cells + the ini-specified map margin.
|
|
var left = Cordon.Left + iniBounds[0];
|
|
var top = Cordon.Top + 2 * iniBounds[1];
|
|
|
|
var playableAreaWidth = iniBounds[2];
|
|
var playableAreaHeight = 2 * iniBounds[3] - UninteractableMargin.Y;
|
|
|
|
// Black magic, don't ask. Non-flat maps seen to require additional height padding.
|
|
var unknownHeightPadding = map.Height.Max() == 0 ? 0 : 8;
|
|
|
|
// Calculate Bounds edge tile coordinates.
|
|
// Reduce bottom and right by 1 because map.SetBounds() increases them.
|
|
var topLeft = new PPos(left, top);
|
|
|
|
if (map.Height.Max() != 0)
|
|
{
|
|
// Workaround: Some custom TS maps have invalid boundaries, with no padding area at the bottom
|
|
// The boundary needs to be reduced until the projections are valid
|
|
bool hasValidBoundaries;
|
|
var paddingOffset = 1;
|
|
do
|
|
{
|
|
var bottomRight = new PPos(playableAreaWidth + left - 1, playableAreaHeight + unknownHeightPadding + top - paddingOffset);
|
|
map.SetBounds(topLeft, bottomRight);
|
|
|
|
var (topBound, bottomBound) = map.GetCellSpaceBounds();
|
|
|
|
hasValidBoundaries = topBound != int.MaxValue && bottomBound != int.MinValue;
|
|
paddingOffset++;
|
|
}
|
|
while (!hasValidBoundaries);
|
|
}
|
|
else
|
|
{
|
|
var bottomRight = new PPos(playableAreaWidth + left - 1, playableAreaHeight + unknownHeightPadding + top - 1);
|
|
map.SetBounds(topLeft, bottomRight);
|
|
}
|
|
}
|
|
|
|
protected virtual bool TryHandleOverlayToActorInner(CPos cell, byte[] overlayPack, CellLayer<int> overlayIndex, byte overlayType, out ActorReference actorReference)
|
|
{
|
|
actorReference = null;
|
|
if (!OverlayToActor.TryGetValue(overlayType, out var actorType))
|
|
return false;
|
|
|
|
// This could be just a dummy handler that we want to ignore.
|
|
if (string.IsNullOrEmpty(actorType))
|
|
return true;
|
|
|
|
if (OverlayShapes.TryGetValue(overlayType, out var shape))
|
|
{
|
|
// Only import the top-left cell of multi-celled overlays
|
|
// Returning true here means this is a part of a bigger overlay that has already been handled.
|
|
var aboveType = overlayPack[overlayIndex[cell - new CVec(1, 0)]];
|
|
if (shape.Width > 1 && aboveType != 0xFF && OverlayToActor.TryGetValue(aboveType, out var a) && a == actorType)
|
|
return true;
|
|
|
|
var leftType = overlayPack[overlayIndex[cell - new CVec(0, 1)]];
|
|
if (shape.Height > 1 && leftType != 0xFF && OverlayToActor.TryGetValue(leftType, out var l) && l == actorType)
|
|
return true;
|
|
}
|
|
|
|
actorReference = new ActorReference(actorType)
|
|
{
|
|
new LocationInit(cell),
|
|
new OwnerInit("Neutral")
|
|
};
|
|
|
|
TryHandleOverlayToHealthInner(overlayType, actorReference);
|
|
|
|
return true;
|
|
}
|
|
|
|
protected virtual bool TryHandleOverlayToHealthInner(byte overlayType, ActorReference actorReference)
|
|
{
|
|
if (!OverlayToHealth.TryGetValue(overlayType, out var damageState))
|
|
return false;
|
|
|
|
var health = 100;
|
|
if (damageState == DamageState.Critical)
|
|
health = 25;
|
|
else if (damageState == DamageState.Heavy)
|
|
health = 50;
|
|
else if (damageState == DamageState.Medium)
|
|
health = 75;
|
|
|
|
if (health != 100)
|
|
actorReference.Add(new HealthInit(health));
|
|
|
|
return true;
|
|
}
|
|
|
|
protected virtual bool TryHandleResourceFromOverlayInner(byte overlayType, byte densityIndex, out ResourceTile resourceTile)
|
|
{
|
|
var resourceType = ResourceFromOverlay
|
|
.Where(kv => kv.Value.Contains(overlayType))
|
|
.Select(kv => kv.Key)
|
|
.FirstOrDefault();
|
|
|
|
resourceTile = new ResourceTile(resourceType, densityIndex);
|
|
return resourceType != 0;
|
|
}
|
|
|
|
protected virtual bool TryHandleOtherOverlayInner(Map map, CPos cell, byte[] overlayDataPack, CellLayer<int> overlayIndex, byte overlayType)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
#region Helper methods
|
|
|
|
protected static void UnpackLZO(byte[] src, byte[] dest)
|
|
{
|
|
var srcOffset = 0U;
|
|
var destOffset = 0U;
|
|
|
|
while (destOffset < dest.Length && srcOffset < src.Length)
|
|
{
|
|
var srcLength = BitConverter.ToUInt16(src, (int)srcOffset);
|
|
var destLength = (uint)BitConverter.ToUInt16(src, (int)srcOffset + 2);
|
|
srcOffset += 4;
|
|
LZOCompression.DecodeInto(src, srcOffset, srcLength, dest, destOffset, ref destLength);
|
|
srcOffset += srcLength;
|
|
destOffset += destLength;
|
|
}
|
|
}
|
|
|
|
protected static void UnpackLCW(byte[] src, byte[] dest, byte[] temp)
|
|
{
|
|
var srcOffset = 0;
|
|
var destOffset = 0;
|
|
|
|
while (destOffset < dest.Length)
|
|
{
|
|
var srcLength = BitConverter.ToUInt16(src, srcOffset);
|
|
var destLength = BitConverter.ToUInt16(src, srcOffset + 2);
|
|
srcOffset += 4;
|
|
LCWCompression.DecodeInto(src, temp, srcOffset);
|
|
Array.Copy(temp, 0, dest, destOffset, destLength);
|
|
srcOffset += srcLength;
|
|
destOffset += destLength;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Convert TS relative position to OpenRA MPos, accounting for map cordons.
|
|
/// </summary>
|
|
protected MPos ToMPos(int dx, int dy)
|
|
{
|
|
return new MPos(Cordon.Left + (dx + Cordon.Top % 2) / 2, Cordon.Top + dy);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Convert TS relative position to OpenRA MPos, accounting for map cordons.
|
|
/// </summary>
|
|
protected MPos ToMPos(int rx, int ry, int mapWidthWithoutCordon)
|
|
{
|
|
var dx = rx - ry + mapWidthWithoutCordon - 1;
|
|
var dy = rx + ry - mapWidthWithoutCordon - 1;
|
|
return ToMPos(dx, dy);
|
|
}
|
|
|
|
#endregion
|
|
}
|
|
}
|