* Fixed gem value calculation

* Added all code for classic color handling
This commit is contained in:
Nyerguds 2023-06-09 01:13:25 +02:00
parent 303762eb31
commit 58be2ebd90
15 changed files with 806 additions and 146 deletions

View File

@ -583,6 +583,7 @@
<Compile Include="Tools\Dialogs\WaypointsToolDialog.Designer.cs">
<DependentUpon>WaypointsToolDialog.cs</DependentUpon>
</Compile>
<Compile Include="Utility\ArrayUtils.cs" />
<Compile Include="Utility\BlobDetection.cs" />
<Compile Include="Tools\InfantryTool.cs" />
<Compile Include="Tools\OverlaysTool.cs" />
@ -596,6 +597,7 @@
<Compile Include="Tools\CellTriggersTool.cs" />
<Compile Include="Tools\WaypointsTool.cs" />
<Compile Include="Utility\BooleanStringStyle.cs" />
<Compile Include="Utility\ClassicSpriteLoader.cs" />
<Compile Include="Utility\CRC.cs" />
<Compile Include="Utility\ExtensionMethods.cs" />
<Compile Include="Utility\GameTextManager.cs" />

View File

@ -6,6 +6,7 @@
// 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.
using MobiusEditor.Model;
using System.Drawing;
namespace MobiusEditor.Interface
@ -21,6 +22,6 @@ namespace MobiusEditor.Interface
Color GetBaseColor(string key);
void Load(string path);
void Reset(GameType gameType, string theater);
void Reset(GameType gameType, TheaterType theater);
}
}

View File

@ -1317,61 +1317,73 @@ namespace MobiusEditor
private static IGamePlugin LoadNewPlugin(GameType gameType, string theater, bool isTdMegaMap, string[] modPaths, bool noImage)
{
// Get plugin type
IGamePlugin plugin = null;
if (gameType == GameType.TiberianDawn)
{
plugin = new TiberianDawn.GamePluginTD(!noImage, isTdMegaMap);
}
else if (gameType == GameType.RedAlert)
{
plugin = new RedAlert.GamePluginRA(!noImage);
}
else if (gameType == GameType.SoleSurvivor)
{
plugin = new SoleSurvivor.GamePluginSS(!noImage, isTdMegaMap);
}
// Get theater object
TheaterTypeConverter ttc = new TheaterTypeConverter();
TheaterType theaterType = ttc.ConvertFrom(new MapContext(plugin.Map, false), theater);
// Resetting to a specific game type will take care of classic mode.
Globals.TheArchiveManager.ExpandModPaths = modPaths;
Globals.TheArchiveManager.Reset(gameType);
Globals.TheGameTextManager.Reset(gameType);
Globals.TheTextureManager.Reset(gameType, theater);
Globals.TheTextureManager.Reset(gameType, theaterType);
Globals.TheTilesetManager.Reset();
Globals.TheTeamColorManager.Reset(gameType, theater);
IGamePlugin plugin = null;
Globals.TheTeamColorManager.Reset(gameType, theaterType);
// Load game-specific data
if (gameType == GameType.TiberianDawn)
{
Globals.TheTeamColorManager.Load(@"DATA\XML\CNCTDTEAMCOLORS.XML");
AddTeamColorNone(Globals.TheTeamColorManager);
AddTeamColorPurple(Globals.TheTeamColorManager);
// TODO split classic and remaster team color load.
plugin = new TiberianDawn.GamePluginTD(!noImage, isTdMegaMap);
AddTeamColorsTD(Globals.TheTeamColorManager);
}
else if (gameType == GameType.RedAlert)
{
Globals.TheTeamColorManager.Load(@"DATA\XML\CNCRATEAMCOLORS.XML");
Globals.TheTeamColorManager.Load("palette.cps");
plugin = new RedAlert.GamePluginRA(!noImage);
AddTeamColorsRA(Globals.TheTeamColorManager);
}
else if (gameType == GameType.SoleSurvivor)
{
Globals.TheTeamColorManager.Load(@"DATA\XML\CNCTDTEAMCOLORS.XML");
AddTeamColorNone(Globals.TheTeamColorManager);
AddTeamColorPurple(Globals.TheTeamColorManager);
plugin = new SoleSurvivor.GamePluginSS(!noImage, isTdMegaMap);
AddTeamColorsTD(Globals.TheTeamColorManager);
}
return plugin;
}
private static void AddTeamColorNone(ITeamColorManager teamColorManager)
private static void AddTeamColorsTD(ITeamColorManager teamColorManager)
{
if (teamColorManager is TeamColorManager tcm)
{
// Add default black for unowned.
// Neutral
TeamColor teamColorSNeutral = new TeamColor(tcm);
teamColorSNeutral.Load(tcm.GetItem("GOOD"), "NEUTRAL");
tcm.AddTeamColor(teamColorSNeutral);
// Special
TeamColor teamColorSpecial = new TeamColor(tcm);
teamColorSpecial.Load(tcm.GetItem("GOOD"), "SPECIAL");
tcm.AddTeamColor(teamColorSpecial);
// Black for unowned.
TeamColor teamColorNone = new TeamColor(tcm);
teamColorNone.Load("NONE", "BASE_TEAM",
Color.FromArgb(66, 255, 0), Color.FromArgb(0, 255, 56), 0,
new Vector3(0.30f, -1.00f, 0.00f), new Vector3(0f, 1f, 1f), new Vector2(0.0f, 0.1f),
new Vector3(0, 1, 1), new Vector2(0, 1), Color.FromArgb(61, 61, 59));
tcm.AddTeamColor(teamColorNone);
}
}
private static void AddTeamColorPurple(ITeamColorManager teamColorManager)
{
if (teamColorManager is TeamColorManager tcm)
{
// Add extra colors for flags.
// Extra colors for flags 7 and 8.
TeamColor teamColorSeven = new TeamColor(tcm);
teamColorSeven.Load(tcm.GetItem("BAD_UNIT"), "MULTI7");
tcm.AddTeamColor(teamColorSeven);
TeamColor teamColorEight = new TeamColor(tcm);
teamColorEight.Load("MULTI8", "BASE_TEAM",
Color.FromArgb(66, 255, 0), Color.FromArgb(0, 255, 56), 0,
@ -1381,6 +1393,17 @@ namespace MobiusEditor
}
}
private static void AddTeamColorsRA(ITeamColorManager teamColorManager)
{
if (teamColorManager is TeamColorManager tcm)
{
// Special
TeamColor teamColorSpecial = new TeamColor(tcm);
teamColorSpecial.Load(tcm.GetItem("SPAIN"), "SPECIAL");
tcm.AddTeamColor(teamColorSpecial);
}
}
/// <summary>
/// The separate-threaded part for making a new map.
/// </summary>

View File

@ -472,7 +472,7 @@ namespace MobiusEditor.Model
this.Overlappers = new OverlapperSet<ICellOverlapper>(this.Metrics);
this.triggers = new List<Trigger>();
this.TeamTypes = new List<TeamType>();
this.HousesIncludingNone = this.HouseTypesIncludingNone.Select(t => { var h = (House)Activator.CreateInstance(this.HouseType, t); h.SetDefault(); return h; }).ToArray();
this.HousesIncludingNone = this.HouseTypesIncludingNone.Select(t => { House h = (House)Activator.CreateInstance(this.HouseType, t); h.SetDefault(); return h; }).ToArray();
this.Houses = this.HousesIncludingNone.Where(h => h.Type.ID >= 0).ToArray();
this.Waypoints = waypoints.ToArray();
for (int i = 0; i < this.Waypoints.Length; ++i)
@ -521,35 +521,35 @@ namespace MobiusEditor.Model
public void InitTheater(GameType gameType)
{
foreach (var templateType in this.TemplateTypes.Where(itm => itm.Theaters == null || itm.Theaters.Contains(this.Theater)))
foreach (TemplateType templateType in this.TemplateTypes.Where(itm => itm.Theaters == null || itm.Theaters.Contains(this.Theater)))
{
templateType.Init(this.Theater);
}
foreach (var smudgeType in this.SmudgeTypes.Where(itm => !Globals.FilterTheaterObjects || itm.Theaters == null || itm.Theaters.Contains(this.Theater)))
foreach (SmudgeType smudgeType in this.SmudgeTypes.Where(itm => !Globals.FilterTheaterObjects || itm.Theaters == null || itm.Theaters.Contains(this.Theater)))
{
smudgeType.Init(this.Theater);
}
foreach (var overlayType in this.OverlayTypes.Where(itm => !Globals.FilterTheaterObjects || itm.Theaters == null || itm.Theaters.Contains(this.Theater)))
foreach (OverlayType overlayType in this.OverlayTypes.Where(itm => !Globals.FilterTheaterObjects || itm.Theaters == null || itm.Theaters.Contains(this.Theater)))
{
overlayType.Init(gameType, this.Theater);
}
foreach (var terrainType in this.TerrainTypes.Where(itm => !Globals.FilterTheaterObjects || itm.Theaters == null || itm.Theaters.Contains(this.Theater)))
foreach (TerrainType terrainType in this.TerrainTypes.Where(itm => !Globals.FilterTheaterObjects || itm.Theaters == null || itm.Theaters.Contains(this.Theater)))
{
terrainType.Init(this.Theater);
}
// Ignore expansion status for these; they can still be enabled later.
DirectionType infDir = this.UnitDirectionTypes.Where(d => d.Facing == FacingType.South).First();
foreach (var infantryType in this.AllInfantryTypes)
foreach (InfantryType infantryType in this.AllInfantryTypes)
{
infantryType.Init(gameType, this.Theater, this.HouseTypesIncludingNone.Where(h => h.Equals(infantryType.OwnerHouse)).FirstOrDefault(), infDir);
}
DirectionType unitDir = this.UnitDirectionTypes.Where(d => d.Facing == FacingType.SouthWest).First();
foreach (var unitType in this.AllUnitTypes)
foreach (UnitType unitType in this.AllUnitTypes)
{
unitType.Init(gameType, this.Theater, this.HouseTypesIncludingNone.Where(h => h.Equals(unitType.OwnerHouse)).FirstOrDefault(), unitDir);
}
DirectionType bldDir = this.UnitDirectionTypes.Where(d => d.Facing == FacingType.North).First();
foreach (var buildingType in this.BuildingTypes.Where(itm => !Globals.FilterTheaterObjects || itm.Theaters == null || itm.Theaters.Contains(this.Theater)))
foreach (BuildingType buildingType in this.BuildingTypes.Where(itm => !Globals.FilterTheaterObjects || itm.Theaters == null || itm.Theaters.Contains(this.Theater)))
{
buildingType.Init(gameType, this.Theater, this.HouseTypesIncludingNone.Where(h => h.Equals(buildingType.OwnerHouse)).FirstOrDefault(), bldDir);
}
@ -574,7 +574,7 @@ namespace MobiusEditor.Model
if (this.invalidateOverlappers)
{
this.Overlappers.Clear();
foreach (var (location, techno) in this.Technos)
foreach ((Point location, ICellOccupier techno) in this.Technos)
{
if (techno is ICellOverlapper)
{
@ -596,7 +596,7 @@ namespace MobiusEditor.Model
private int GetTotalResources(bool inBounds)
{
int totalResources = 0;
foreach (var (cell, value) in this.Overlay)
foreach ((Int32 cell, Overlay value) in this.Overlay)
{
Point point;
if (!value.Type.IsResource || !this.Metrics.GetLocation(cell, out point))
@ -622,12 +622,11 @@ namespace MobiusEditor.Model
}
}
int thickness = value.Type.IsGem ? gemStages[adj] : tiberiumStages[adj];
// Harvesting has a bug where the final stage returns a value of 0. In case this is fixed, this is the calculation:
//totalResources += value.Type.IsGem ? (thickness + 1) * GemValue * 4 : (thickness + 1) * TiberiumOrGoldValue;
// Harvesting one gem stage fills one bail, plus 3 extra bails. Last stage is 0 (due to a bug), but still gets the extra bails.
// Harvesting has a bug where the final stage returns a value of 0 since it uses the 0-based icon index.
// Harvesting one gem stage fills one bail, plus 3 extra bails. Last stage is 0 (due to that bug), but still gets the extra bails.
if (Globals.ApplyHarvestBug)
{
totalResources += value.Type.IsGem ? thickness * this.GemValue * 4 + this.GemValue * 3 : thickness * this.TiberiumOrGoldValue;
totalResources += value.Type.IsGem ? thickness * this.GemValue + this.GemValue * 3 : thickness * this.TiberiumOrGoldValue;
}
else
{
@ -651,17 +650,17 @@ namespace MobiusEditor.Model
if (tiberiumOrGoldTypes.Length == 0) tiberiumOrGoldTypes = null;
OverlayType[] gemTypes = this.OverlayTypes.Where(t => t.IsGem).ToArray();
if (gemTypes.Length == 0) gemTypes = null;
foreach (var (location, overlay) in this.Overlay.IntersectsWithPoints(locations).Where(o => o.Value.Type.IsResource))
foreach ((Point location, Overlay overlay) in this.Overlay.IntersectsWithPoints(locations).Where(o => o.Value.Type.IsResource))
{
int count = 0;
bool inBounds = checkBounds.Contains(location);
if (inBounds)
{
foreach (var facing in CellMetrics.AdjacentFacings)
foreach (FacingType facing in CellMetrics.AdjacentFacings)
{
if (this.Metrics.Adjacent(location, facing, out Point adjacent) && checkBounds.Contains(adjacent))
{
var adjacentOverlay = this.Overlay[adjacent];
Overlay adjacentOverlay = this.Overlay[adjacent];
if (adjacentOverlay?.Type.IsResource ?? false)
{
count++;
@ -685,7 +684,7 @@ namespace MobiusEditor.Model
private void UpdateWallOverlays(ISet<Point> locations)
{
foreach (var (location, overlay) in this.Overlay.IntersectsWithPoints(locations).Where(o => o.Value.Type.IsWall))
foreach ((Point location, Overlay overlay) in this.Overlay.IntersectsWithPoints(locations).Where(o => o.Value.Type.IsWall))
{
Overlay northWall = this.Overlay.Adjacent(location, FacingType.North);
Overlay eastWall = this.Overlay.Adjacent(location, FacingType.East);
@ -714,18 +713,18 @@ namespace MobiusEditor.Model
private void UpdateConcreteOverlays(ISet<Point> locations)
{
foreach (var (cell, overlay) in this.Overlay.IntersectsWithCells(locations).Where(o => o.Value.Type.IsConcrete))
foreach ((Int32 cell, Overlay overlay) in this.Overlay.IntersectsWithCells(locations).Where(o => o.Value.Type.IsConcrete))
{
// in order: top, topnext, next, bottomnext, bottom
// Cells to check around the current cell. In order: top, topnext, next, bottomnext, bottom
FacingType[] even = { FacingType.North, FacingType.NorthWest, FacingType.West, FacingType.SouthWest, FacingType.South };
FacingType[] odd = { FacingType.North, FacingType.NorthEast, FacingType.East, FacingType.SouthEast, FacingType.South };
int isodd = cell & 1;
FacingType[] cells = isodd != 0 ? odd : even;
Boolean[] conc = new bool[cells.Length];
for (int i = 0; i < cells.Length; i++)
FacingType[] adjCells = isodd != 0 ? odd : even;
Boolean[] conc = new bool[adjCells.Length];
for (int i = 0; i < adjCells.Length; i++)
{
var neighbor = this.Overlay.Adjacent(cell, cells[i]);
if (neighbor != null && neighbor.Type == overlay.Type)
Overlay neighbor = this.Overlay.Adjacent(cell, adjCells[i]);
if (neighbor?.Type == overlay.Type)
{
int ic = overlay.Icon;
if (ic < 4 || (ic > 7 && ic < 12))
@ -742,21 +741,26 @@ namespace MobiusEditor.Model
bool bottomnext = conc[3];
bool bottom = conc[4];
int icon = 0;
int state = 0;
if (top && next || topnext && next)
{
icon = bottom ? 1 : 5;
state = bottom ? 1 : 5;
}
else if (bottom && next || bottom && bottomnext)
{
icon = topnext ? 1 : 4;
state = topnext ? 1 : 4;
}
else if (top && topnext)
{
icon = 5;
state = 5;
}
icon = icon == 0 ? isodd : (icon * 2) + 1 - isodd;
overlay.Icon = icon;
// For some reason the odd and even icons for state 0 are swapped compared to all the others, so
// an extra check has to be added for that. Otherwise just "(icon * 2) + 1 - isodd" would suffice.
overlay.Icon = state == 0 ? isodd : (state * 2) + 1 - isodd;
// Possibly todo: add concrete to fill up corners and actually complete the logic as intended?
// This would require that all logic in the editor treats these corner states (2 and 3) as 'expendable',
// automatically overwriting them with other overlay, walls or tiberium, and not saving them in the ini.
}
}
@ -781,7 +785,7 @@ namespace MobiusEditor.Model
private void UpdateConcreteOverlays_ORIG(ISet<Point> locations)
{
foreach (var (cell, overlay) in this.Overlay.IntersectsWithCells(locations).Where(o => o.Value.Type.IsConcrete))
foreach ((Int32 cell, Overlay overlay) in this.Overlay.IntersectsWithCells(locations).Where(o => o.Value.Type.IsConcrete))
{
// Original logic as it is in the game code. Still doesn't match reality, probably due to bugs in the logic to add side cells.
FacingType[] odd = { FacingType.North, FacingType.NorthEast, FacingType.East, FacingType.SouthEast, FacingType.South };
@ -791,7 +795,7 @@ namespace MobiusEditor.Model
int index = 0;
for (int i = 0; i < cells.Length; i++)
{
var neighbor = this.Overlay.Adjacent(cell, cells[i]);
Overlay neighbor = this.Overlay.Adjacent(cell, cells[i]);
if (neighbor != null && neighbor.Type == overlay.Type)
{
int ic = overlay.Icon;
@ -1010,22 +1014,22 @@ namespace MobiusEditor.Model
return "No cell";
}
bool inBounds = this.Bounds.Contains(location);
var sb = new StringBuilder();
StringBuilder sb = new StringBuilder();
sb.AppendFormat("X = {0}, Y = {1}, Cell = {2}", location.X, location.Y, cell);
var template = this.Templates[cell];
var templateType = template?.Type;
Template template = this.Templates[cell];
TemplateType templateType = template?.Type;
if (templateType != null)
{
sb.AppendFormat(", Template = {0} ({1})", templateType.DisplayName, template.Icon);
}
var smudge = this.Smudge[cell];
var smudgeType = smudge?.Type;
Smudge smudge = this.Smudge[cell];
SmudgeType smudgeType = smudge?.Type;
if (smudgeType != null)
{
sb.AppendFormat(", Smudge = {0}{1}", smudgeType.DisplayName, smudgeType.IsAutoBib ? " (Attached)" : String.Empty);
}
var overlay = this.Overlay[cell];
var overlayType = overlay?.Type;
Overlay overlay = this.Overlay[cell];
OverlayType overlayType = overlay?.Type;
if (overlayType != null)
{
if (overlayType.IsResource)
@ -1037,8 +1041,8 @@ namespace MobiusEditor.Model
sb.AppendFormat(", Overlay = {0}", overlayType.DisplayName);
}
}
var terrain = this.Technos[location] as Terrain;
var terrainType = terrain?.Type;
Terrain terrain = this.Technos[location] as Terrain;
TerrainType terrainType = terrain?.Type;
if (terrainType != null)
{
sb.AppendFormat(", Terrain = {0}", terrainType.DisplayName);
@ -1052,14 +1056,14 @@ namespace MobiusEditor.Model
sb.AppendFormat(", Infantry = {0} ({1})", inf.Type.DisplayName, InfantryGroup.GetStoppingTypeName(i));
}
}
var unit = this.Technos[location] as Unit;
var unitType = unit?.Type;
Unit unit = this.Technos[location] as Unit;
UnitType unitType = unit?.Type;
if (unitType != null)
{
sb.AppendFormat(", Unit = {0}", unitType.DisplayName);
}
var building = this.Buildings[location] as Building;
var buildingType = building?.Type;
Building building = this.Buildings[location] as Building;
BuildingType buildingType = building?.Type;
if (buildingType != null)
{
sb.AppendFormat(", Building = {0}", buildingType.DisplayName);
@ -1082,8 +1086,8 @@ namespace MobiusEditor.Model
private void RemoveBibs(Building building)
{
var bibCells = this.Smudge.IntersectsWithCells(building.BibCells).Where(x => x.Value.Type.IsAutoBib).Select(x => x.Cell).ToArray();
foreach (var cell in bibCells)
Int32[] bibCells = this.Smudge.IntersectsWithCells(building.BibCells).Where(x => x.Value.Type.IsAutoBib).Select(x => x.Cell).ToArray();
foreach (Int32 cell in bibCells)
{
this.Smudge[cell] = null;
}
@ -1117,7 +1121,7 @@ namespace MobiusEditor.Model
}
// This is a shallow clone; the map is new, but the placed contents all still reference the original objects.
// These shallow copies are used for map preview during editing, where dummy objects can be added without any issue.
var map = new Map(this.BasicSection, this.Theater, this.Metrics.Size, this.HouseType, this.HouseTypesIncludingNone,
Map map = new Map(this.BasicSection, this.Theater, this.Metrics.Size, this.HouseType, this.HouseTypesIncludingNone,
this.FlagColors, this.TheaterTypes, this.TemplateTypes, this.TerrainTypes, this.OverlayTypes, this.SmudgeTypes,
this.EventTypes, this.CellEventTypes, this.UnitEventTypes, this.BuildingEventTypes, this.TerrainEventTypes,
this.ActionTypes, this.CellActionTypes, this.UnitActionTypes, this.BuildingActionTypes, this.TerrainActionTypes,
@ -1139,13 +1143,13 @@ namespace MobiusEditor.Model
this.Overlay.CopyTo(map.Overlay);
this.Smudge.CopyTo(map.Smudge);
this.CellTriggers.CopyTo(map.CellTriggers);
foreach (var (location, occupier) in this.Technos)
foreach ((Point location, ICellOccupier occupier) in this.Technos)
{
if (occupier is InfantryGroup infantryGroup)
{
// This creates an InfantryGroup not linked to its infantry, but it is necessary
// to ensure that the real InfantryGroups are not polluted with dummy objects.
var newInfantryGroup = new InfantryGroup();
InfantryGroup newInfantryGroup = new InfantryGroup();
Array.Copy(infantryGroup.Infantry, newInfantryGroup.Infantry, newInfantryGroup.Infantry.Length);
map.Technos.Add(location, newInfantryGroup);
}
@ -1154,7 +1158,7 @@ namespace MobiusEditor.Model
map.Technos.Add(location, occupier);
}
}
foreach (var (location, building) in this.Buildings)
foreach ((Point location, ICellOccupier building) in this.Buildings)
{
// Silly side effect: this fixes any building bibs.
map.Buildings.Add(location, building);
@ -1263,11 +1267,11 @@ namespace MobiusEditor.Model
public IEnumerable<ITechno> GetAllTechnos()
{
foreach (var (location, occupier) in this.Technos)
foreach ((Point location, ICellOccupier occupier) in this.Technos)
{
if (occupier is InfantryGroup infantryGroup)
{
foreach (var inf in infantryGroup.Infantry)
foreach (Infantry inf in infantryGroup.Infantry)
{
if (inf != null)
{
@ -1324,9 +1328,9 @@ namespace MobiusEditor.Model
mask = bld.BaseOccupyMask;
ylen = mask.GetLength(0);
xlen = mask.GetLength(1);
for (var y = 0; y < ylen; ++y)
for (Int32 y = 0; y < ylen; ++y)
{
for (var x = 0; x < xlen; ++x)
for (Int32 x = 0; x < xlen; ++x)
{
if (mask[y, x])
{
@ -1350,9 +1354,9 @@ namespace MobiusEditor.Model
mask = obj.OccupyMask;
ylen = mask.GetLength(0);
xlen = mask.GetLength(1);
for (var y = 0; y < ylen; ++y)
for (Int32 y = 0; y < ylen; ++y)
{
for (var x = 0; x < xlen; ++x)
for (Int32 x = 0; x < xlen; ++x)
{
if (mask[y, x])
{
@ -1396,18 +1400,18 @@ namespace MobiusEditor.Model
mapBounds = new Rectangle(0, 0, this.Metrics.Width * Globals.OriginalTileWidth, this.Metrics.Height * Globals.OriginalTileHeight);
//locations
}
var previewScale = Math.Min(previewSize.Width / (float)mapBounds.Width, previewSize.Height / (float)mapBounds.Height);
var scaledSize = new Size((int)(previewSize.Width / previewScale), (int)(previewSize.Height / previewScale));
Single previewScale = Math.Min(previewSize.Width / (float)mapBounds.Width, previewSize.Height / (float)mapBounds.Height);
Size scaledSize = new Size((int)(previewSize.Width / previewScale), (int)(previewSize.Height / previewScale));
using (var fullBitmap = new Bitmap(this.Metrics.Width * Globals.OriginalTileWidth, this.Metrics.Height * Globals.OriginalTileHeight))
using (var croppedBitmap = new Bitmap(previewSize.Width, previewSize.Height))
using (Bitmap fullBitmap = new Bitmap(this.Metrics.Width * Globals.OriginalTileWidth, this.Metrics.Height * Globals.OriginalTileHeight))
using (Bitmap croppedBitmap = new Bitmap(previewSize.Width, previewSize.Height))
{
using (var g = Graphics.FromImage(fullBitmap))
using (Graphics g = Graphics.FromImage(fullBitmap))
{
MapRenderer.SetRenderSettings(g, smooth);
MapRenderer.Render(gameType, this, g, locations, toRender, 1);
}
using (var g = Graphics.FromImage(croppedBitmap))
using (Graphics g = Graphics.FromImage(croppedBitmap))
{
MapRenderer.SetRenderSettings(g, true);
Matrix transform = new Matrix();
@ -1419,7 +1423,7 @@ namespace MobiusEditor.Model
}
if (sharpen)
{
using (var sharpenedImage = croppedBitmap.Sharpen(1.0f))
using (Bitmap sharpenedImage = croppedBitmap.Sharpen(1.0f))
{
return TGA.FromBitmap(sharpenedImage);
}
@ -1460,7 +1464,7 @@ namespace MobiusEditor.Model
{
return;
}
foreach (var overlay in new Overlay[] { e.OldValue, e.Value })
foreach (Overlay overlay in new Overlay[] { e.OldValue, e.Value })
{
if (overlay == null)
{
@ -1736,7 +1740,7 @@ namespace MobiusEditor.Model
}
}
// Clean teamtypes
foreach (var team in this.TeamTypes)
foreach (TeamType team in this.TeamTypes)
{
String trig = team.Trigger;
if (!Trigger.IsEmpty(trig) && !availableUnitTriggers.Contains(trig))
@ -1753,7 +1757,7 @@ namespace MobiusEditor.Model
}
}
// Clean triggers. Not covered in undo/redo actions since it is applied on the new list directly.
foreach (var trig in triggers)
foreach (Trigger trig in triggers)
{
if (trig == null)
{

View File

@ -48,8 +48,12 @@ namespace MobiusEditor.Model
{
return null;
}
return ConvertFrom(context as MapContext, value as string);
}
var map = (context as MapContext).Map;
public TheaterType ConvertFrom(MapContext context, string value)
{
var map = context.Map;
return map.TheaterTypes.Where(t => t.Equals(value)).FirstOrDefault() ?? map.TheaterTypes.First();
}
}

View File

@ -255,7 +255,6 @@ namespace MobiusEditor
// Buildings
gtm["TEXT_STRUCTURE_TITLE_OIL_PUMP"] = "Oil Pump";
gtm["TEXT_STRUCTURE_TITLE_OIL_TANKER"] = "Oil Tanker";
String fake = " (" + gtm["TEXT_UI_FAKE"] + ")";
if (!gtm["TEXT_STRUCTURE_RA_WEAF"].EndsWith(fake)) gtm["TEXT_STRUCTURE_RA_WEAF"] = Globals.TheGameTextManager["TEXT_STRUCTURE_RA_WEAF"] + fake;
if (!gtm["TEXT_STRUCTURE_RA_FACF"].EndsWith(fake)) gtm["TEXT_STRUCTURE_RA_FACF"] = Globals.TheGameTextManager["TEXT_STRUCTURE_RA_FACF"] + fake;
@ -272,7 +271,6 @@ namespace MobiusEditor
gtm["TEXT_OVERLAY_WCRATE"] = "Wooden Crate";
gtm["TEXT_OVERLAY_SCRATE"] = "Steel Crate";
gtm["TEXT_OVERLAY_WATER_CRATE"] = "Water Crate";
// Smudge
gtm["TEXT_SMUDGE_CRATER"] = "Crater";
gtm["TEXT_SMUDGE_SCORCH"] = "Scorch Mark";

View File

@ -31,8 +31,8 @@ namespace MobiusEditor.RedAlert
public static readonly HouseType Turkey = new HouseType(7, "Turkey", "TURKEY");
public static readonly HouseType Good = new HouseType(8, "GoodGuy", "GOOD");
public static readonly HouseType Bad = new HouseType(9, "BadGuy", "BAD");
public static readonly HouseType Neutral = new HouseType(10, "Neutral");
public static readonly HouseType Special = new HouseType(11, "Special", "SPAIN");
public static readonly HouseType Neutral = new HouseType(10, "Neutral", "NEUTRAL");
public static readonly HouseType Special = new HouseType(11, "Special", "SPECIAL");
public static readonly HouseType Multi1 = new HouseType(12, "Multi1", WaypointFlag.PlayerStart1, "MULTI1"); // yellow
public static readonly HouseType Multi2 = new HouseType(13, "Multi2", WaypointFlag.PlayerStart2, "MULTI2"); // teal
public static readonly HouseType Multi3 = new HouseType(14, "Multi3", WaypointFlag.PlayerStart3, "MULTI3"); // red

View File

@ -27,8 +27,8 @@ namespace MobiusEditor.TiberianDawn
public static readonly HouseType Good = new HouseType(0, "GoodGuy", "GOOD");
public static readonly HouseType Bad = new HouseType(1, "BadGuy", "BAD_UNIT", "BAD_STRUCTURE", ("harv", "BAD_STRUCTURE"), ("mcv", "BAD_STRUCTURE"));
// Added actual recoloring
public static readonly HouseType Neutral = new HouseType(2, "Neutral", "GOOD");
public static readonly HouseType Special = new HouseType(3, "Special", "GOOD");
public static readonly HouseType Neutral = new HouseType(2, "Neutral", "NEUTRAL");
public static readonly HouseType Special = new HouseType(3, "Special", "SPECIAL");
// Fixed to match actual game. Seems they messed up the naming of the colors in the xml files by taking the color definitions from the C&C
// game code in order, arbitrarily naming those "Multi1" to "Multi6", and then correctly applying those obviously wrongly named colors to
// the multi-Houses in the Remastered game. The editor code logically assumed they were named after their House, and thus got it all wrong.

View File

@ -0,0 +1,374 @@
using System;
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
namespace MobiusEditor.Utility
{
public static class ArrayUtils
{
public static T[][] SwapDimensions<T>(T[][] original)
{
Int32 origHeight = original.Length;
if (origHeight == 0)
return new T[0][];
// Since this is for images, it is assumed that the array is a perfectly rectangular matrix
Int32 origWidth = original[0].Length;
T[][] swapped = new T[origWidth][];
for (Int32 newHeight = 0; newHeight < origWidth; ++newHeight)
{
swapped[newHeight] = new T[origHeight];
for (Int32 newWidth = 0; newWidth < origHeight; ++newWidth)
swapped[newHeight][newWidth] = original[newWidth][newHeight];
}
return swapped;
}
public static Boolean ArraysAreEqual<T>(T[] arr1, T[] arr2) where T : IEquatable<T>
{
// There's probably a Linq version of this though... Probably .All() or something.
// But this is with simple arrays.
if (arr1 == null && arr2 == null)
return true;
if (arr1 == null || arr2 == null)
return false;
Int32 arr1len = arr1.Length;
if (arr1len != arr2.Length)
return false;
for (Int32 i = 0; i < arr1len; ++i)
if (!arr1[i].Equals(arr2[i]))
return false;
return true;
}
/// <summary>
/// Creates and returns a new array, containing the contents of all the given arrays, in the given order.
/// </summary>
/// <typeparam name="T">Type of the arrays</typeparam>
/// <param name="arrays">Arrays to join together.</param>
/// <returns>A new array containing the contents of all given arrays, joined together.</returns>
public static T[] MergeArrays<T>(params T[][] arrays)
{
Int32 length = 0;
Int32 nrOfArrays = arrays.Length;
for (Int32 i = 0; i < nrOfArrays; ++i)
{
T[] array = arrays[i];
if (array != null)
length += array.Length;
}
T[] result = new T[length];
Int32 copyIndex = 0;
for (Int32 i = 0; i < nrOfArrays; ++i)
{
T[] array = arrays[i];
if (array == null)
continue;
array.CopyTo(result, copyIndex);
copyIndex += array.Length;
}
return result;
}
public static T[] CloneArray<T>(T[] array)
{
Int32 len = array.Length;
T[] copy = new T[len];
Array.Copy(array, copy, len);
return copy;
}
public static UInt64 ReadIntFromByteArray(Byte[] data, Int32 startIndex, Int32 bytes, Boolean littleEndian)
{
Int32 lastByte = bytes - 1;
if (data.Length < startIndex + bytes)
throw new ArgumentOutOfRangeException("startIndex", "Data array is too small to read a " + bytes + "-byte value at offset " + startIndex + ".");
UInt64 value = 0;
for (Int32 index = 0; index < bytes; ++index)
{
Int32 offs = startIndex + (littleEndian ? index : lastByte - index);
value |= (((UInt64)data[offs]) << (8 * index));
}
return value;
}
#region ReadIntFromByteArray shortcuts
public static Int16 ReadInt16FromByteArrayLe(Byte[] data, Int32 startIndex)
{
return (Int16)ReadIntFromByteArray(data, startIndex, 2, true);
}
public static Int16 ReadInt16FromByteArrayBe(Byte[] data, Int32 startIndex)
{
return (Int16)ReadIntFromByteArray(data, startIndex, 2, false);
}
public static UInt16 ReadUInt16FromByteArrayLe(Byte[] data, Int32 startIndex)
{
return (UInt16)ReadIntFromByteArray(data, startIndex, 2, true);
}
public static UInt16 ReadUInt16FromByteArrayBe(Byte[] data, Int32 startIndex)
{
return (UInt16)ReadIntFromByteArray(data, startIndex, 2, false);
}
public static Int32 ReadInt32FromByteArrayLe(Byte[] data, Int32 startIndex)
{
return (Int32)ReadIntFromByteArray(data, startIndex, 4, true);
}
public static Int32 ReadInt32FromByteArrayBe(Byte[] data, Int32 startIndex)
{
return (Int32)ReadIntFromByteArray(data, startIndex, 4, false);
}
public static UInt32 ReadUInt32FromByteArrayLe(Byte[] data, Int32 startIndex)
{
return (UInt32)ReadIntFromByteArray(data, startIndex, 4, true);
}
public static UInt32 ReadUInt32FromByteArrayBe(Byte[] data, Int32 startIndex)
{
return (UInt32)ReadIntFromByteArray(data, startIndex, 4, false);
}
public static Int64 ReadInt64FromByteArrayLe(Byte[] data, Int32 startIndex)
{
return (Int64)ReadIntFromByteArray(data, startIndex, 8, true);
}
public static Int64 ReadInt64FromByteArrayBe(Byte[] data, Int32 startIndex)
{
return (Int64)ReadIntFromByteArray(data, startIndex, 8, true);
}
public static UInt64 ReadUInt64FromByteArrayLe(Byte[] data, Int32 startIndex)
{
return (UInt64)ReadIntFromByteArray(data, startIndex, 8, true);
}
public static UInt64 ReadUInt64FromByteArrayBe(Byte[] data, Int32 startIndex)
{
return (UInt64)ReadIntFromByteArray(data, startIndex, 8, true);
}
#endregion
public static void WriteIntToByteArray(Byte[] data, Int32 startIndex, Int32 bytes, Boolean littleEndian, UInt64 value)
{
Int32 lastByte = bytes - 1;
if (data.Length < startIndex + bytes)
throw new ArgumentOutOfRangeException("startIndex", "Data array is too small to write a " + bytes + "-byte value at offset " + startIndex + ".");
for (Int32 index = 0; index < bytes; ++index)
{
Int32 offs = startIndex + (littleEndian ? index : lastByte - index);
data[offs] = (Byte) (value >> (8 * index) & 0xFF);
}
}
#region WriteIntToByteArray shortcuts
public static void WriteInt16ToByteArrayLe(Byte[] data, Int32 startIndex, Int16 value)
{
WriteIntToByteArray(data, startIndex, 2, true, (UInt64)value);
}
public static void WriteInt16ToByteArrayBe(Byte[] data, Int32 startIndex, Int16 value)
{
WriteIntToByteArray(data, startIndex, 2, false, (UInt64)value);
}
public static void WriteUInt16ToByteArrayLe(Byte[] data, Int32 startIndex, UInt16 value)
{
WriteIntToByteArray(data, startIndex, 2, true, value);
}
public static void WriteUInt16ToByteArrayBe(Byte[] data, Int32 startIndex, UInt16 value)
{
WriteIntToByteArray(data, startIndex, 2, false, value);
}
public static void WriteInt32ToByteArrayLe(Byte[] data, Int32 startIndex, Int32 value)
{
WriteIntToByteArray(data, startIndex, 4, true, (UInt64)value);
}
public static void WriteInt32ToByteArrayBe(Byte[] data, Int32 startIndex, Int32 value)
{
WriteIntToByteArray(data, startIndex, 4, false, (UInt64)value);
}
public static void WriteUInt32ToByteArrayLe(Byte[] data, Int32 startIndex, UInt32 value)
{
WriteIntToByteArray(data, startIndex, 4, true, value);
}
public static void WriteUInt32ToByteArrayBe(Byte[] data, Int32 startIndex, UInt32 value)
{
WriteIntToByteArray(data, startIndex, 4, false, value);
}
public static void WriteInt64ToByteArrayLe(Byte[] data, Int32 startIndex, Int64 value)
{
WriteIntToByteArray(data, startIndex, 8, true, (UInt64)value);
}
public static void WriteInt64ToByteArrayBe(Byte[] data, Int32 startIndex, Int64 value)
{
WriteIntToByteArray(data, startIndex, 8, true, (UInt64)value);
}
public static void WriteUInt64ToByteArrayLe(Byte[] data, Int32 startIndex, UInt64 value)
{
WriteIntToByteArray(data, startIndex, 8, true, value);
}
public static void WriteUInt64ToByteArrayBe(Byte[] data, Int32 startIndex, UInt64 value)
{
WriteIntToByteArray(data, startIndex, 8, true, value);
}
#endregion
public static Int32 ReadBitsFromByteArray(Byte[] dataArr, ref Int32 bitIndex, Int32 codeLen, Int32 bufferInEnd)
{
Int32 intCode = 0;
Int32 byteIndex = bitIndex / 8;
Int32 ignoreBitsAtIndex = bitIndex % 8;
Int32 bitsToReadAtIndex = Math.Min(codeLen, 8 - ignoreBitsAtIndex);
Int32 totalUsedBits = 0;
while (codeLen > 0)
{
if (byteIndex >= bufferInEnd)
return -1;
Int32 toAdd = (dataArr[byteIndex] >> ignoreBitsAtIndex) & ((1 << bitsToReadAtIndex) - 1);
intCode |= (toAdd << totalUsedBits);
totalUsedBits += bitsToReadAtIndex;
codeLen -= bitsToReadAtIndex;
bitsToReadAtIndex = Math.Min(codeLen, 8);
ignoreBitsAtIndex = 0;
byteIndex++;
}
bitIndex += totalUsedBits;
return intCode;
}
public static void WriteBitsToByteArray(Byte[] dataArr, Int32 bitIndex, Int32 codeLen, Int32 intCode)
{
Int32 byteIndex = bitIndex / 8;
Int32 usedBitsAtIndex = bitIndex % 8;
Int32 bitsToWriteAtIndex = Math.Min(codeLen, 8 - usedBitsAtIndex);
while (codeLen > 0)
{
Int32 codeToWrite = (intCode & ((1 << bitsToWriteAtIndex) - 1)) << usedBitsAtIndex;
intCode = intCode >> bitsToWriteAtIndex;
dataArr[byteIndex] |= (Byte) codeToWrite;
codeLen -= bitsToWriteAtIndex;
bitsToWriteAtIndex = Math.Min(codeLen, 8);
usedBitsAtIndex = 0;
byteIndex++;
}
}
public static T CloneStruct<T>(T obj) where T : struct
{
Endianness e = BitConverter.IsLittleEndian ? Endianness.LittleEndian : Endianness.BigEndian;
return ReadStructFromByteArray<T>(StructToByteArray(obj, e), 0, e);
}
private static void AdjustEndianness(Type type, Byte[] data, Endianness endianness, Int32 startOffset)
{
// nothing to change => return
if (BitConverter.IsLittleEndian == (endianness == Endianness.LittleEndian))
return;
FieldInfo[] fields = type.GetFields();
Int32 fieldsLen = fields.Length;
for (Int32 i = 0; i < fieldsLen; ++i)
{
FieldInfo field = fields[i];
Type fieldType = field.FieldType;
if (field.IsStatic)
// don't process static fields
continue;
if (fieldType == typeof (String))
// don't swap bytes for strings
continue;
Int32 offset = Marshal.OffsetOf(type, field.Name).ToInt32();
// handle enums
if (fieldType.IsEnum)
fieldType = Enum.GetUnderlyingType(fieldType);
// check for sub-fields to recurse if necessary
FieldInfo[] subFields = fieldType.GetFields().Where(subField => subField.IsStatic == false).ToArray();
Int32 effectiveOffset = startOffset + offset;
if (subFields.Length == 0)
Array.Reverse(data, effectiveOffset, Marshal.SizeOf(fieldType));
else // recurse
AdjustEndianness(fieldType, data, endianness, effectiveOffset);
}
}
public static T StructFromByteArray<T>(Byte[] rawData, Endianness endianness) where T : struct
{
return ReadStructFromByteArray<T>(rawData, 0, endianness);
}
public static T ReadStructFromByteArray<T>(Byte[] rawData, Int32 offset, Endianness endianness) where T : struct
{
Type tType = typeof (T);
Int32 size = Marshal.SizeOf(tType);
if (size + offset > rawData.Length)
throw new IndexOutOfRangeException("Array is too small to get the requested struct!");
IntPtr ptr = IntPtr.Zero;
try
{
ptr = Marshal.AllocHGlobal(size);
// Adjust array to preferred endianness
AdjustEndianness(tType, rawData, endianness, offset);
Marshal.Copy(rawData, offset, ptr, size);
// Revert array to original data order
AdjustEndianness(tType, rawData, endianness, offset);
Object obj = Marshal.PtrToStructure(ptr, tType);
return (T) obj;
}
finally
{
if (ptr != IntPtr.Zero)
Marshal.FreeHGlobal(ptr);
}
}
public static Byte[] StructToByteArray<T>(T obj, Endianness endianness) where T : struct
{
Int32 size = Marshal.SizeOf(typeof (T));
Byte[] target = new Byte[size];
WriteStructToByteArray(obj, target, 0, endianness);
return target;
}
public static void WriteStructToByteArray<T>(T obj, Byte[] target, Int32 index, Endianness endianness) where T : struct
{
Type tType = typeof (T);
Int32 size = Marshal.SizeOf(tType);
IntPtr ptr = IntPtr.Zero;
try
{
ptr = Marshal.AllocHGlobal(size);
Marshal.StructureToPtr(obj, ptr, true);
Marshal.Copy(ptr, target, index, size);
AdjustEndianness(typeof (T), target, endianness, 0);
}
finally
{
if (ptr != IntPtr.Zero)
Marshal.FreeHGlobal(ptr);
}
}
}
public enum Endianness
{
BigEndian,
LittleEndian
}
}

View File

@ -0,0 +1,122 @@
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Imaging;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MobiusEditor.Utility
{
public class ClassicSpriteLoader
{
private static readonly Byte[] ConvertToEightBit = new Byte[64];
static ClassicSpriteLoader()
{
// Build easy lookup tables for this, so no calculations are ever needed for this later.
for (Int32 i = 0; i < 64; ++i)
ConvertToEightBit[i] = (Byte)Math.Round(i * 255.0 / 63.0, MidpointRounding.ToEven);
}
/// <summary>
/// Retrieves the CPS image.
/// </summary>
/// <param name="fileData">Original file data.</param>
/// <param name="start">Start offset of the data.</param>
/// <returns>The raw 8-bit linear image data in a 64000 byte array.</returns>
public static Bitmap LoadCpsFile(Byte[] fileData, Int32 start)
{
Byte[] imageData = GetCpsData(fileData, 0, out Color[] palette);
if (palette == null)
palette = Enumerable.Range(0, 0x100).Select(i => Color.FromArgb(i, i, i)).ToArray();
return ImageUtils.BuildImage(imageData, 320, 200, 200, PixelFormat.Format8bppIndexed, palette, null);
}
/// <summary>
/// Retrieves the CPS image.
/// </summary>
/// <param name="fileData">Original file data.</param>
/// <param name="start">Start offset of the data.</param>
/// <returns>The raw 8-bit linear image data in a 64000 byte array.</returns>
public static Byte[] GetCpsData(Byte[] fileData, Int32 start, out Color[] palette)
{
int dataLen = fileData.Length - start;
if (dataLen < 10)
throw new ArgumentException("File is not long enough to be a valid CPS file.", "fileData");
int fileSize = ArrayUtils.ReadInt16FromByteArrayLe(fileData, start + 0);
int compression = ArrayUtils.ReadUInt16FromByteArrayLe(fileData, start + 2);
if (compression < 0 || compression > 4)
throw new ArgumentException("Unknown compression type " + compression, "fileData");
// compressions other than 0 and 4 count the full file including size header.
if (compression == 0 || compression == 4)
fileSize += 2;
else
throw new ArgumentException("LZW and RLE compression are not supported.", "fileData");
if (fileSize != dataLen)
throw new ArgumentException("File size in header does not match!", "fileData");
Int32 bufferSize = ArrayUtils.ReadInt32FromByteArrayLe(fileData, start + 4);
Int32 paletteLength = ArrayUtils.ReadInt16FromByteArrayLe(fileData, start + 8);
Boolean isPc = bufferSize == 64000;
if (bufferSize != 64000)
throw new ArgumentException("Unknown CPS type.", "fileData");
if (paletteLength > 0)
{
Int32 palStart = start + 10;
if (paletteLength % 3 != 0)
throw new ArgumentException("Bad length for 6-bit CPS palette.", "fileData");
Int32 colors = paletteLength / 3;
palette = ReadSixBitPaletteAsEightBit(fileData, palStart, colors);
}
else
palette = null;
Byte[] imageData;
Int32 dataOffset = start + 10 + paletteLength;
if (compression == 0 && dataLen < dataOffset + bufferSize)
throw new ArgumentException("File is not long enough to contain the image data!", "fileData");
try
{
switch (compression)
{
case 0:
imageData = new Byte[bufferSize];
Array.Copy(fileData, dataOffset, imageData, 0, bufferSize);
break;
case 4:
imageData = new Byte[bufferSize];
WWCompression.LcwDecompress(fileData, ref dataOffset, imageData, 0);
break;
default:
throw new ArgumentException("Unsupported compression format \"" + compression + "\".", "fileData");
}
}
catch (Exception e)
{
throw new ArgumentException("Error decompressing image data: " + e.Message, "fileData", e);
}
if (imageData == null)
throw new ArgumentException("Error decompressing image data.", "fileData");
return imageData;
}
public static Color[] ReadSixBitPaletteAsEightBit(Byte[] fileData, int palStart, int colors)
{
Color[] palette = Enumerable.Repeat(Color.Black, colors).ToArray();
// Palette data should always be be 0x300 long, but this code works regardless of that.
int len = Math.Min(fileData.Length / 3, colors);
int offs = palStart;
for (int i = 0; i < len; ++i)
{
byte r = ConvertToEightBit[fileData[offs + 0] & 0x3F];
byte g = ConvertToEightBit[fileData[offs + 1] & 0x3F];
byte b = ConvertToEightBit[fileData[offs + 2] & 0x3F];
palette[i] = Color.FromArgb(r, g, b);
offs += 3;
}
return palette;
}
}
}

View File

@ -402,7 +402,7 @@ namespace MobiusEditor.Utility
Marshal.Copy(data.Scan0, bytes, 0, bytes.Length);
if (bitmap.PixelFormat == PixelFormat.Format8bppIndexed)
{
return CalculateOpaqueBounds8bpp(bytes, data.Width, data.Height, data.Stride, transColors);
return CalculateOpaqueBounds8bpp(bytes, data.Width, data.Height, data.Stride, transColors.ToArray());
}
else
{
@ -501,7 +501,7 @@ namespace MobiusEditor.Utility
return opaqueBounds;
}
private static Rectangle CalculateOpaqueBounds8bpp(byte[] data, int width, int height, int stride, List<int> transparentColors)
public static Rectangle CalculateOpaqueBounds8bpp(byte[] data, int width, int height, int stride, params int[] transparentColors)
{
HashSet<int> trMap = new HashSet<int>(transparentColors);
// Only handle 32bpp data.

View File

@ -13,6 +13,7 @@
// GNU General Public License along with permitted additional restrictions
// with this program. If not, see https://github.com/electronicarts/CnC_Remastered_Collection
using MobiusEditor.Interface;
using MobiusEditor.Model;
using System.Collections.Generic;
using System.Drawing;
using System.IO;
@ -69,7 +70,7 @@ namespace MobiusEditor.Utility
this.megafileManager = megafileManager;
}
public void Reset(GameType gameType, string theater)
public void Reset(GameType gameType, TheaterType theater)
{
teamColors.Clear();
}
@ -87,6 +88,7 @@ namespace MobiusEditor.Utility
}
if (xmlDoc != null)
{
teamColors.Clear();
foreach (XmlNode teamColorNode in xmlDoc.SelectNodes("/*/TeamColorTypeClass"))
{
var teamColor = new TeamColor(this);

View File

@ -2,7 +2,9 @@
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Imaging;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
@ -16,6 +18,15 @@ namespace MobiusEditor.Utility
private byte[] remapTable;
public TeamRemap(string newName, TeamRemap baseRemap)
{
this.Name = newName;
this.UnitRadarColor = baseRemap.UnitRadarColor;
this.BuildingRadarColor = baseRemap.BuildingRadarColor;
this.remapTable = new byte[0x100];
Array.Copy(baseRemap.remapTable, remapTable, remapTable.Length);
}
public TeamRemap(string name, byte unitRadarColor, byte buildingRadarColor, byte remapstart, byte[] remapValues)
{
this.Name = name;
@ -42,15 +53,66 @@ namespace MobiusEditor.Utility
}
}
public void ApplyToImage(Bitmap image)
{
throw new NotImplementedException();
ApplyToImage(image, out _);
}
public void ApplyToImage(Bitmap image, out Rectangle opaqueBounds)
{
throw new NotImplementedException();
if (image == null)
{
opaqueBounds = Rectangle.Empty;
return;
}
int bytesPerPixel = Image.GetPixelFormatSize(image.PixelFormat) / 8;
if (bytesPerPixel != 3 && bytesPerPixel != 4)
{
opaqueBounds = new Rectangle(Point.Empty, image.Size);
return;
}
BitmapData data = null;
try
{
data = image.LockBits(new Rectangle(0, 0, image.Width, image.Height), ImageLockMode.ReadWrite, image.PixelFormat);
byte[] bytes = new byte[data.Stride * data.Height];
Marshal.Copy(data.Scan0, bytes, 0, bytes.Length);
int width = data.Width;
int height = data.Height;
int stride = data.Stride;
opaqueBounds = ImageUtils.CalculateOpaqueBounds8bpp(bytes, width, height, stride, 0);
ApplyToImage(bytes, width, height, bytesPerPixel, stride, opaqueBounds);
Marshal.Copy(bytes, 0, data.Scan0, bytes.Length);
}
finally
{
if (data != null)
{
image.UnlockBits(data);
}
}
}
public void ApplyToImage(byte[] bytes, int width, int height, int bytesPerPixel, int stride, Rectangle? opaqueBounds)
{
// Only handle 8bpp data.
if (bytesPerPixel != 1)
{
return;
}
Rectangle bounds = opaqueBounds ?? new Rectangle(0, 0, width, height);
int boundsBottom = Math.Min(height, bounds.Bottom);
int boundsWidth = Math.Min(Math.Max(0, width - bounds.Left), bounds.Width);
int linePtr = 0;
for (int y = bounds.Top; y < boundsBottom; y++)
{
int ptr = linePtr + bounds.Left;
for (int x = 0; x < boundsWidth; x++)
{
bytes[ptr] = remapTable[bytes[ptr]];
ptr++;
}
linePtr += stride;
}
}
}
}

View File

@ -1,55 +1,74 @@
using MobiusEditor.Interface;
using MobiusEditor.Model;
using System;
using System.Collections.Generic;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
namespace MobiusEditor.Utility
{
class TeamRemapManager : ITeamColorManager
{
// TD remap
public static readonly TeamRemap RemapTdGoodGuy = new TeamRemap("GOOD", 176, 180, 176, new byte[] { 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191 });
public static readonly TeamRemap RemapTdBadGuyRed = new TeamRemap("BAD_STRUCTURE", 127, 123, 176, new byte[] { 127, 126, 125, 124, 122, 46, 120, 47, 125, 124, 123, 122, 42, 121, 120, 120 });
public static readonly TeamRemap RemapTdBadGuyGray = new TeamRemap("BAD_UNIT", 127, 123, 176, new byte[] { 161, 200, 201, 202, 204, 205, 206, 12, 201, 202, 203, 204, 205, 115, 198, 114 });
public static readonly TeamRemap RemapTdGood = new TeamRemap("GOOD", 176, 180, 176, new byte[] { 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191 });
public static readonly TeamRemap RemapTdBadRed = new TeamRemap("BAD_STRUCTURE", 127, 123, 176, new byte[] { 127, 126, 125, 124, 122, 46, 120, 47, 125, 124, 123, 122, 42, 121, 120, 120 });
public static readonly TeamRemap RemapTdBadGray = new TeamRemap("BAD_UNIT", 127, 123, 176, new byte[] { 161, 200, 201, 202, 204, 205, 206, 12, 201, 202, 203, 204, 205, 115, 198, 114 });
// The color name identifiers in the remaster are all out of wack.
public static readonly TeamRemap RemapTdBlue = new TeamRemap("MULTI2", 2, 135, 176, new byte[] {2, 119, 118, 135, 136, 138, 112, 12, 118, 135, 136, 137, 138, 139, 114, 112});
public static readonly TeamRemap RemapTdBlue = new TeamRemap("MULTI2", 2, 135, 176, new byte[] { 2, 119, 118, 135, 136, 138, 112, 12, 118, 135, 136, 137, 138, 139, 114, 112 });
public static readonly TeamRemap RemapTdOrange = new TeamRemap("MULTI5", 24, 26, 176, new byte[] { 24, 25, 26, 27, 29, 31, 46, 47, 26, 27, 28, 29, 30, 31, 43, 47 });
public static readonly TeamRemap RemapTdGreen = new TeamRemap("MULTI4", 167, 159, 176, new byte[] { 5, 165, 166, 167, 159, 142, 140, 199, 166, 167, 157, 3, 159, 143, 142, 141 });
public static readonly TeamRemap RemapTdLtBlue = new TeamRemap("MULTI6", 201, 203, 176, new byte[] { 161, 200, 201, 202, 204, 205, 206, 12, 201, 202, 203, 204, 205, 115, 198, 114 });
public static readonly TeamRemap RemapTdYellow = new TeamRemap("MULTI1", 5, 157, 176, new byte[] { 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191 });
public static readonly TeamRemap RemapTdRed = new TeamRemap("MULTI3", 127, 123, 176, new byte[] { 127, 126, 125, 124, 122, 46, 120, 47, 125, 124, 123, 122, 42, 121, 120, 120 });
// Added extra colours for flags
public static readonly TeamRemap RemapTdMul7 = new TeamRemap("MULTI7", 146, 209, 176, new byte[] { 146, 152, 209, 151, 173, 150, 173, 183, 146, 152, 209, 151, 173, 150, 173, 183 }); // Brown
public static readonly TeamRemap RemapTdMul8 = new TeamRemap("MULTI8", 214, 213, 176, new byte[] { 132, 133, 134, 213, 214, 121, 120, 12, 133, 134, 213, 214, 121, 174, 120, 199 }); // Burgundy
public static readonly TeamRemap RemapTdNone = new TeamRemap("NONE", 199, 199, 176, new byte[] { 14, 195, 196, 13, 169, 198, 199, 112, 14, 195, 196, 13, 169, 198, 199, 112 }); // Black
public static readonly TeamRemap RemapTdBrown = new TeamRemap("MULTI7", 146, 209, 176, new byte[] { 146, 152, 209, 151, 173, 150, 173, 183, 146, 152, 209, 151, 173, 150, 173, 183 }); // Brown
//public static readonly TeamRemap RemapTdBurg = new TeamRemap("Burgudy", 214, 213, 176, new byte[] { 132, 133, 134, 213, 214, 121, 120, 12, 133, 134, 213, 214, 121, 174, 120, 199 }); // Burgundy
public static readonly TeamRemap RemapTdPink = new TeamRemap("MULTI8", 217, 218, 176, new byte[] { 17, 17, 217, 218, 209, 213, 174, 120, 217, 217, 218, 209, 213, 214, 214, 174 }); // Pink
public static readonly TeamRemap RemapTdBlack = new TeamRemap("NONE", 199, 199, 176, new byte[] { 14, 195, 196, 13, 169, 198, 199, 112, 14, 195, 196, 13, 169, 198, 199, 112 }); // Black
private static readonly Dictionary<string, TeamRemap> RemapsTd;
public const Byte MaxValueSixBit = 63;
private static readonly Byte[] ConvertToEightBit = new Byte[64];
static TeamRemapManager()
{
RemapsTd = (from field in typeof(ITeamColorManager).GetFields(BindingFlags.Static | BindingFlags.Public)
where field.IsInitOnly && typeof(TeamRemap).IsAssignableFrom(field.FieldType) && field.Name.StartsWith("RemapTd")
select field.GetValue(null) as TeamRemap).ToDictionary(trm => trm.Name);
// Build easy lookup tables for this, so no calculations are ever needed for this later.
for (Int32 i = 0; i < 64; ++i)
ConvertToEightBit[i] = (Byte)Math.Round(i * 255.0 / 63.0, MidpointRounding.ToEven);
}
private Dictionary<string, TeamRemap> remapsRa = new Dictionary<string, TeamRemap>();
private GameType currentlyLoadedGameType;
private Color[] currentlyLoadedPalette;
private readonly IArchiveManager mixfileManager;
private readonly string[] remapsColorsRa =
{
"GOLD",
"LTBLUE",
"RED",
"GREEN",
"ORANGE",
"GREY",
"BLUE",
"BROWN",
//"TYPE",
//"REALLY_BLUE",
//"DIALOG_BLUE",
};
private readonly Dictionary<string, string[]> remapUseRa = new Dictionary<string, string[]> {
{ "GOLD", new string[]{ "SPAIN", "NEUTRAL", "SPECIAL" , "MULTI1" } },
{ "LTBLUE", new string[]{ "GREECE", "GOOD", "MULTI2" } },
{ "RED", new string[]{ "USSR", "BAD", "MULTI3" } },
{ "GREEN", new string[]{ "ENGLAND", "MULTI4" } },
{ "ORANGE", new string[]{ "UKRAINE", "MULTI5" } },
{ "GREY", new string[]{ "GERMANY", "MULTI6" } },
{ "BLUE", new string[]{ "FRANCE", "MULTI7" } },
{ "BROWN", new string[]{ "TURKEY", "MULTI8" } },
//{ "TYPE", new string[]{ } },
//{ "REALLY_BLUE", new string[]{ } },
//{ "DIALOG_BLUE", new string[]{ } },
};
public ITeamColor this[string key] => GetforCurrentGame(key);
public Color GetBaseColor(string key)
@ -89,7 +108,64 @@ namespace MobiusEditor.Utility
public void Load(string path)
{
// Check if CPS, if so, fill in "this.remapsRa"
if (currentlyLoadedGameType != GameType.RedAlert)
{
return;
}
byte[] cpsData;
Color[] palette;
using (Stream palettecps = mixfileManager.OpenFile(path))
{
if (palettecps == null)
{
return;
}
try
{
Byte[] cpsFileBytes;
using (BinaryReader sr = new BinaryReader(palettecps))
{
cpsFileBytes = GeneralUtils.ReadAllBytes(sr);
}
cpsData = ClassicSpriteLoader.GetCpsData(cpsFileBytes, 0, out palette);
}
catch (ArgumentException ex)
{
return;
}
}
// Data found; re-initialise RA remaps.
this.remapsRa.Clear();
int height = Math.Min(200, remapsColorsRa.Length);
Dictionary<string, TeamRemap> raRemapColors = new Dictionary<string, TeamRemap>();
byte[] remapSource = new byte[16];
Array.Copy(cpsData, 0, remapSource, 0, 16);
for (int y = 0; y < height; ++y)
{
int ptr = 320 * y;
String name = remapsColorsRa[y];
Byte[] remap = new byte[16];
Array.Copy(cpsData, ptr, remap, 0, 16);
// Apparently the same in RA?
byte unitRadarColor = cpsData[ptr + 6];
byte buildingRadarColor = cpsData[ptr + 6];
TeamRemap col = new TeamRemap(name, unitRadarColor, buildingRadarColor, remapSource, remap);
raRemapColors.Add(name, col);
}
foreach (String col in remapsColorsRa)
{
string[] usedRemaps;
TeamRemap remapColor;
if (remapUseRa.TryGetValue(col, out usedRemaps) && raRemapColors.TryGetValue(col, out remapColor))
{
for (int i = 0; i < usedRemaps.Length; ++i)
{
String actualName = usedRemaps[i];
TeamRemap actualCol = new TeamRemap(usedRemaps[i], remapColor);
remapsRa.Add(actualName, actualCol);
}
}
}
}
public TeamRemapManager(IArchiveManager fileManager)
@ -97,41 +173,32 @@ namespace MobiusEditor.Utility
this.mixfileManager = fileManager;
}
public void Reset(GameType gameType, string theater)
public void Reset(GameType gameType, TheaterType theater)
{
this.remapsRa.Clear();
currentlyLoadedGameType = gameType;
currentlyLoadedPalette = new Color[0x100];
// file manager should already be reset to read from the correct game at this point.
using (Stream palette = mixfileManager.OpenFile(theater + ".pal"))
using (Stream palette = mixfileManager.OpenFile(theater.ClassicTileset + ".pal"))
{
if (palette != null)
if (palette == null)
{
// Grayscale palette; looks awful but still allows distinguishing stuff.
currentlyLoadedPalette = Enumerable.Range(0, 0x100).Select(i => Color.FromArgb(i, i, i)).ToArray();
}
else
{
byte[] pal;
using (BinaryReader sr = new BinaryReader(palette))
{
pal = GeneralUtils.ReadAllBytes(sr);
}
int len = Math.Min(pal.Length / 3, 0x100);
int offs = 0;
for (int i = 0; i < len; ++i)
{
byte r = ConvertToEightBit[pal[offs + 0] & 0x3F];
byte g = ConvertToEightBit[pal[offs + 1] & 0x3F];
byte b = ConvertToEightBit[pal[offs + 2] & 0x3F];
currentlyLoadedPalette[i] = Color.FromArgb(r, g, b);
offs += 3;
}
currentlyLoadedPalette = ClassicSpriteLoader.ReadSixBitPaletteAsEightBit(pal, 0, 0x100);
}
}
if (gameType == GameType.TiberianDawn || gameType == GameType.SoleSurvivor)
{
}
}
public void setTheater()
{
// Set background transparent
currentlyLoadedPalette[0] = Color.FromArgb(0x00, currentlyLoadedPalette[0]);
// Set shadow color to semitransparent black. I'm not gonna mess around with classic fading table remapping for this.
currentlyLoadedPalette[4] = Color.FromArgb(0x80, Color.Black);
}
}
}

View File

@ -24,6 +24,7 @@ using System.IO.Compression;
using System.Runtime.InteropServices;
using TGASharpLib;
using MobiusEditor.Interface;
using MobiusEditor.Model;
namespace MobiusEditor.Utility
{
@ -42,7 +43,7 @@ namespace MobiusEditor.Utility
this.megafileManager = megafileManager;
}
public void Reset(GameType gameType, string theater)
public void Reset(GameType gameType, TheaterType theater)
{
Bitmap[] cachedImages = cachedTextures.Values.ToArray();
cachedTextures.Clear();