From 58be2ebd90ca7ed70337e1f9c2ecea9249242160 Mon Sep 17 00:00:00 2001 From: Nyerguds Date: Fri, 9 Jun 2023 01:13:25 +0200 Subject: [PATCH] * Fixed gem value calculation * Added all code for classic color handling --- CnCTDRAMapEditor/CnCTDRAMapEditor.csproj | 2 + .../Interface/ITeamColorManager.cs | 3 +- CnCTDRAMapEditor/MainForm.cs | 67 ++-- CnCTDRAMapEditor/Model/Map.cs | 140 +++---- CnCTDRAMapEditor/Model/MapSection.cs | 6 +- CnCTDRAMapEditor/Program.cs | 2 - CnCTDRAMapEditor/RedAlert/HouseTypes.cs | 4 +- CnCTDRAMapEditor/TiberianDawn/HouseTypes.cs | 4 +- CnCTDRAMapEditor/Utility/ArrayUtils.cs | 374 ++++++++++++++++++ .../Utility/ClassicSpriteLoader.cs | 122 ++++++ CnCTDRAMapEditor/Utility/ImageUtils.cs | 4 +- CnCTDRAMapEditor/Utility/TeamColorManager.cs | 4 +- CnCTDRAMapEditor/Utility/TeamRemap.cs | 68 +++- CnCTDRAMapEditor/Utility/TeamRemapManager.cs | 149 +++++-- CnCTDRAMapEditor/Utility/TextureManager.cs | 3 +- 15 files changed, 806 insertions(+), 146 deletions(-) create mode 100644 CnCTDRAMapEditor/Utility/ArrayUtils.cs create mode 100644 CnCTDRAMapEditor/Utility/ClassicSpriteLoader.cs diff --git a/CnCTDRAMapEditor/CnCTDRAMapEditor.csproj b/CnCTDRAMapEditor/CnCTDRAMapEditor.csproj index a1efe9d..b6dcaa3 100644 --- a/CnCTDRAMapEditor/CnCTDRAMapEditor.csproj +++ b/CnCTDRAMapEditor/CnCTDRAMapEditor.csproj @@ -583,6 +583,7 @@ WaypointsToolDialog.cs + @@ -596,6 +597,7 @@ + diff --git a/CnCTDRAMapEditor/Interface/ITeamColorManager.cs b/CnCTDRAMapEditor/Interface/ITeamColorManager.cs index 701f304..c07ac51 100644 --- a/CnCTDRAMapEditor/Interface/ITeamColorManager.cs +++ b/CnCTDRAMapEditor/Interface/ITeamColorManager.cs @@ -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); } } \ No newline at end of file diff --git a/CnCTDRAMapEditor/MainForm.cs b/CnCTDRAMapEditor/MainForm.cs index eb64871..ee1e5e0 100644 --- a/CnCTDRAMapEditor/MainForm.cs +++ b/CnCTDRAMapEditor/MainForm.cs @@ -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); + } + } + /// /// The separate-threaded part for making a new map. /// diff --git a/CnCTDRAMapEditor/Model/Map.cs b/CnCTDRAMapEditor/Model/Map.cs index 5494ddf..9c94d23 100644 --- a/CnCTDRAMapEditor/Model/Map.cs +++ b/CnCTDRAMapEditor/Model/Map.cs @@ -472,7 +472,7 @@ namespace MobiusEditor.Model this.Overlappers = new OverlapperSet(this.Metrics); this.triggers = new List(); this.TeamTypes = new List(); - 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 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 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 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 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) { diff --git a/CnCTDRAMapEditor/Model/MapSection.cs b/CnCTDRAMapEditor/Model/MapSection.cs index 90abf02..968f093 100644 --- a/CnCTDRAMapEditor/Model/MapSection.cs +++ b/CnCTDRAMapEditor/Model/MapSection.cs @@ -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(); } } diff --git a/CnCTDRAMapEditor/Program.cs b/CnCTDRAMapEditor/Program.cs index 4830605..7ff9b4c 100644 --- a/CnCTDRAMapEditor/Program.cs +++ b/CnCTDRAMapEditor/Program.cs @@ -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"; diff --git a/CnCTDRAMapEditor/RedAlert/HouseTypes.cs b/CnCTDRAMapEditor/RedAlert/HouseTypes.cs index f823454..a959bf9 100644 --- a/CnCTDRAMapEditor/RedAlert/HouseTypes.cs +++ b/CnCTDRAMapEditor/RedAlert/HouseTypes.cs @@ -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 diff --git a/CnCTDRAMapEditor/TiberianDawn/HouseTypes.cs b/CnCTDRAMapEditor/TiberianDawn/HouseTypes.cs index 6cbb387..dfc2d99 100644 --- a/CnCTDRAMapEditor/TiberianDawn/HouseTypes.cs +++ b/CnCTDRAMapEditor/TiberianDawn/HouseTypes.cs @@ -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. diff --git a/CnCTDRAMapEditor/Utility/ArrayUtils.cs b/CnCTDRAMapEditor/Utility/ArrayUtils.cs new file mode 100644 index 0000000..f82c7ca --- /dev/null +++ b/CnCTDRAMapEditor/Utility/ArrayUtils.cs @@ -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[][] 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[] arr1, T[] arr2) where T : IEquatable + { + // 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; + } + + /// + /// Creates and returns a new array, containing the contents of all the given arrays, in the given order. + /// + /// Type of the arrays + /// Arrays to join together. + /// A new array containing the contents of all given arrays, joined together. + public static T[] MergeArrays(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[] 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 obj) where T : struct + { + Endianness e = BitConverter.IsLittleEndian ? Endianness.LittleEndian : Endianness.BigEndian; + return ReadStructFromByteArray(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(Byte[] rawData, Endianness endianness) where T : struct + { + return ReadStructFromByteArray(rawData, 0, endianness); + } + + public static T ReadStructFromByteArray(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 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 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 + } +} diff --git a/CnCTDRAMapEditor/Utility/ClassicSpriteLoader.cs b/CnCTDRAMapEditor/Utility/ClassicSpriteLoader.cs new file mode 100644 index 0000000..2cf21c5 --- /dev/null +++ b/CnCTDRAMapEditor/Utility/ClassicSpriteLoader.cs @@ -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); + } + + + /// + /// Retrieves the CPS image. + /// + /// Original file data. + /// Start offset of the data. + /// The raw 8-bit linear image data in a 64000 byte array. + 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); + } + + /// + /// Retrieves the CPS image. + /// + /// Original file data. + /// Start offset of the data. + /// The raw 8-bit linear image data in a 64000 byte array. + 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; + } + } +} diff --git a/CnCTDRAMapEditor/Utility/ImageUtils.cs b/CnCTDRAMapEditor/Utility/ImageUtils.cs index b9a66e5..e2e33a0 100644 --- a/CnCTDRAMapEditor/Utility/ImageUtils.cs +++ b/CnCTDRAMapEditor/Utility/ImageUtils.cs @@ -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 transparentColors) + public static Rectangle CalculateOpaqueBounds8bpp(byte[] data, int width, int height, int stride, params int[] transparentColors) { HashSet trMap = new HashSet(transparentColors); // Only handle 32bpp data. diff --git a/CnCTDRAMapEditor/Utility/TeamColorManager.cs b/CnCTDRAMapEditor/Utility/TeamColorManager.cs index 2d9d2e6..039121d 100644 --- a/CnCTDRAMapEditor/Utility/TeamColorManager.cs +++ b/CnCTDRAMapEditor/Utility/TeamColorManager.cs @@ -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); diff --git a/CnCTDRAMapEditor/Utility/TeamRemap.cs b/CnCTDRAMapEditor/Utility/TeamRemap.cs index f66ce04..017716d 100644 --- a/CnCTDRAMapEditor/Utility/TeamRemap.cs +++ b/CnCTDRAMapEditor/Utility/TeamRemap.cs @@ -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; + } } } } diff --git a/CnCTDRAMapEditor/Utility/TeamRemapManager.cs b/CnCTDRAMapEditor/Utility/TeamRemapManager.cs index d0eaab9..585379b 100644 --- a/CnCTDRAMapEditor/Utility/TeamRemapManager.cs +++ b/CnCTDRAMapEditor/Utility/TeamRemapManager.cs @@ -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 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 remapsRa = new Dictionary(); 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 remapUseRa = new Dictionary { + { "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 raRemapColors = new Dictionary(); + 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); } } } diff --git a/CnCTDRAMapEditor/Utility/TextureManager.cs b/CnCTDRAMapEditor/Utility/TextureManager.cs index 03b1979..a8a4772 100644 --- a/CnCTDRAMapEditor/Utility/TextureManager.cs +++ b/CnCTDRAMapEditor/Utility/TextureManager.cs @@ -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();