added function comments, cleaned up some obsolete code, added some tools necessary for classic support.

This commit is contained in:
Nyerguds 2023-05-31 17:45:31 +02:00
parent 26a7854409
commit 02d1992055
22 changed files with 743 additions and 457 deletions

View File

@ -138,8 +138,8 @@ namespace MobiusEditor.Controls
case Building building:
isOnMap = building.IsPrebuilt;
items = Plugin.Map.FilterStructureTriggers().Select(t => t.Name).Distinct().ToArray();
filteredEvents = Plugin.Map.EventTypes.Where(ac => Plugin.Map.StructureEventTypes.Contains(ac)).Distinct().ToArray();
filteredActions = Plugin.Map.ActionTypes.Where(ac => Plugin.Map.StructureActionTypes.Contains(ac)).Distinct().ToArray();
filteredEvents = Plugin.Map.EventTypes.Where(ac => Plugin.Map.BuildingEventTypes.Contains(ac)).Distinct().ToArray();
filteredActions = Plugin.Map.ActionTypes.Where(ac => Plugin.Map.BuildingActionTypes.Contains(ac)).Distinct().ToArray();
break;
default:
items = Plugin.Map.Triggers.Select(t => t.Name).Distinct().ToArray();

View File

@ -254,7 +254,8 @@ namespace MobiusEditor.Dialogs
briefingSettingsTracker.TryGetMember("Briefing", out object brf);
if (brf is String brief)
{
if (!plugin.EvaluateBriefing(brief, out String message) && !String.IsNullOrEmpty(message))
string message = plugin.EvaluateBriefing(brief);
if (message != null)
{
message += "\n\nPress Cancel to go back and edit the briefing, or OK to ignore the issue and continue.";
DialogResult dres = MessageBox.Show(message, "Warning", MessageBoxButtons.OKCancel, MessageBoxIcon.Warning, MessageBoxDefaultButton.Button1);

View File

@ -16,6 +16,7 @@ namespace MobiusEditor.Interface
{
public interface ICellOccupier
{
/// <summary>Footprint of this object, determining where no other objects can be placed.</summary>
bool[,] OccupyMask { get; }
}
}

View File

@ -18,7 +18,9 @@ namespace MobiusEditor.Interface
{
public interface ICellOverlapper
{
/// <summary>Rectangular bounds of this overlapper.</summary>
Rectangle OverlapBounds { get; }
/// <summary>Determines for each cell whether other graphics drawn under this one are considered to be mostly visible.</summary>
bool[,] OpaqueMask { get; }
}
}

View File

@ -38,42 +38,113 @@ namespace MobiusEditor.Interface
public interface IGamePlugin : IDisposable
{
/// <summary>Name of the game.</summary>
string Name { get; }
/// <summary>The game type as enum.</summary>
GameType GameType { get; }
/// <summary>True if the plugin is initialised to handle a megamap.</summary>
bool IsMegaMap { get; }
/// <summary>The map.</summary>
Map Map { get; }
/// <summary>The map image to show.</summary>
Image MapImage { get; }
/// <summary>Feedback handler that can be attached to the plugin.</summary>
IFeedBackHandler FeedBackHandler { get; set; }
/// <summary>True if the currently loaded map was modified.</summary>
bool Dirty { get; set; }
/// <summary>Extra ini text that can be freely edited by the user.</summary>
string ExtraIniText { get; set; }
/// <summary>
/// Create a new map in the chosen theater.
/// </summary>
/// <param name="theater">The name of the theater to use.</param>
void New(string theater);
/// <summary>
/// Load a map.
/// </summary>
/// <param name="path">Path of the map to load.</param>
/// <param name="fileType">File type of the actual file in the path, so accompanying files can be loaded correctly.</param>
/// <returns>Any issues encountered when loading the map.</returns>
IEnumerable<string> Load(string path, FileType fileType);
/// <summary>
/// Save the current map to the given path, with the given file type.
/// </summary>
/// <param name="path">Path of the map to save.</param>
/// <param name="fileType">File type of the actual file in the path, so accompanying files can be saved correctly.</param>
/// <returns>true if the saving succeeded.</returns>
bool Save(string path, FileType fileType);
/// <summary>
/// Save the current map to the given path, with the given file type.
/// </summary>
/// <param name="path">Path of the map to save.</param>
/// <param name="fileType">File type of the actual file in the path, so accompanying files can be saved correctly.</param>
/// <param name="customPreview">Custom preview given to the map.</param>
/// <param name="dontResavePreview">True to not resave the preview on disc when doing the save operation.</param>
/// <returns>true if the saving succeeded.</returns>
bool Save(string path, FileType fileType, Bitmap customPreview, bool dontResavePreview);
/// <summary>
/// Validate the map to see if there are any blocking errors preventing it from saving.
/// </summary>
/// <returns>true if the validation succeeded.</returns>
string Validate();
/// <summary>
/// Generates an overview of how many items are on the map and how many are allowed, and does a trigger analysis.
/// </summary>
/// <returns>The generated map items overview.</returns>
IEnumerable<string> AssessMapItems();
/// <summary>
/// Retrieves a hash set of all houses for which production is started by triggers.
/// </summary>
/// <returns>A hash set of all houses for which production is started by triggers.</returns>
HashSet<string> GetHousesWithProduction();
/// <summary>
/// Returns an array containing the reveal radius for each waypoint on the map.
/// </summary>
/// <param name="map">The map to gheck the waypoints on.</param>
/// <param name="forLargeReveal">False for small flare reveal, true for large area reveal.</param>
/// <returns></returns>
int[] GetRevealRadiusForWaypoints(Map map, bool forLargeReveal);
/// <summary>
/// Check whether there are any errors in the currect scripting.
/// </summary>
/// <param name="triggers">List of triggers to check.</param>
/// <param name="includeExternalData">True to fetch extra data from the map, such as map objects that triggers can be linked to.</param>
/// <param name="prefixNames">True to prefix the trigger name before every line. If false, each trigger's analysis will be preceded with a header line containing the trigger name.</param>
/// <param name="fatalOnly">True to report fatal issues only.</param>
/// <param name="fatal">Returns true if fatal issues were encountered.</param>
/// <param name="fix">True to fix issues that are encountered whenever possible.</param>
/// <param name="wasFixed">Returns true if issues were fixed.</param>
/// <returns>A summation of the encountered issues.</returns>
IEnumerable<string> CheckTriggers(IEnumerable<Trigger> triggers, bool includeExternalData, bool prefixNames, bool fatalOnly, out bool fatal, bool fix, out bool wasFixed);
/// <summary>
/// Checks whether the name has a default map name that is considered empty by this game plugin.
/// </summary>
/// <param name="name">Map name to check.</param>
/// <returns>True if the given name is considered empty by this game plugin.</returns>
bool MapNameIsEmpty(string name);
Boolean EvaluateBriefing(string briefing, out string message);
/// <summary>
/// Checks whether the briefing has any kind of issues concerning length or supported characters.
/// </summary>
/// <param name="briefing">The briefing to check</param>
/// <returns>Null if everything is okay, otherwise any issues to show on the user interface.</returns>
string EvaluateBriefing(string briefing);
}
}

View File

@ -12,10 +12,22 @@ namespace MobiusEditor.Interface
{
public interface ITeamColor
{
/// <summary>Name of the color, used as identifier.</summary>
string Name { get; }
/// <summary>A general color representing this team color.</summary>
Color BaseColor { get; }
/// <summary>
/// Apply this color to a given image.
/// </summary>
/// <param name="image">The image to process.</param>
void ApplyToImage(Bitmap image);
/// <summary>
/// Apply this color to a given image, and output the actually-used bounds of the graphics.
/// </summary>
/// <param name="image">The image to process.</param>
/// <param name="opaqueBounds">The actually-used nontransparent area of the graphics that need to be painted.</param>
void ApplyToImage(Bitmap image, out Rectangle opaqueBounds);
void ApplyToImage(byte[] bytes, int width, int height, int bytesPerPixel, int stride, Rectangle? opaqueBounds);
}
}

View File

@ -16,11 +16,17 @@ namespace MobiusEditor.Interface
{
public interface ITechnoType: IBrowsableType
{
/// <summary>Object ID</summary>
sbyte ID { get; }
/// <summary>Object ini name</summary>
string Name { get; }
/// <summary>True if this object has a weapon. This affects the default orders for placing it on the map.</summary>
bool IsArmed { get; }
/// <summary>True if this object is an aircraft, and is normally not placeable on the map.</summary>
bool IsAircraft { get; }
/// <summary>True if this object is a fixed-wing aircraft. This treats it as 16-frame rotation, and affects the default orders for placing it on the map.</summary>
bool IsFixedWing { get; }
/// <summary>True if this object can harvest resources. This affects the default orders for placing it on the map.</summary>
bool IsHarvester { get; }
}
}

View File

@ -24,14 +24,23 @@ namespace MobiusEditor.Model
[Flags]
public enum BuildingTypeFlag
{
/// <summary>No flags set.</summary>
None = 0,
/// <summary>Produces structures.</summary>
Factory = (1 << 0),
/// <summary>Has a bib attached.</summary>
Bib = (1 << 1),
/// <summary>Is a fake building.</summary>
Fake = (1 << 2),
/// <summary>Has a rotating turret, and accepts a Facing value in the ini file.</summary>
Turret = (1 << 3),
/// <summary>Only has a single frame of graphics.</summary>
SingleFrame = (1 << 4),
/// <summary>Does not adjust to house colors.</summary>
NoRemap = (1 << 5),
/// <summary>Is flat on the ground; anything around will overlap this building's graphics.</summary>
Flat = (1 << 6),
/// <summary>Can show a gap area-of-effect radius indicator.</summary>
IsGapGenerator = (1 << 7),
}
@ -70,7 +79,8 @@ namespace MobiusEditor.Model
get { return (Flag & BuildingTypeFlag.Bib) == BuildingTypeFlag.Bib; }
set
{
if (value)
// Bibs are only supported for widths 2 to 4
if (value && Size.Width >= 2 && Size.Width <= 4)
{
Flag |= BuildingTypeFlag.Bib;
}
@ -118,7 +128,8 @@ namespace MobiusEditor.Model
this.OwnerHouse = ownerHouse;
this.Theaters = theaters;
this.FactoryOverlay = factoryOverlay;
this.RecalculateBibs();
// Check on width and disable if needed. This also calls RecalculateBibs.
this.HasBib = this.HasBib;
}
public BuildingType(sbyte id, string name, string textId, int powerProd, int powerUse, int width, int height, string occupyMask, string ownerHouse, TheaterType[] theaters, String graphicsSource, BuildingTypeFlag flag)

File diff suppressed because it is too large Load Diff

View File

@ -24,16 +24,27 @@ namespace MobiusEditor.Model
[Flags]
public enum OverlayTypeFlag
{
/// <summary>No flags set.</summary>
None = 0,
/// <summary>Is a basic resource overlay.</summary>
TiberiumOrGold = (1 << 0),
/// <summary>Is a high value resource overlay.</summary>
Gems = (1 << 1),
/// <summary>Is a wall.</summary>
Wall = (1 << 2),
/// <summary>Is a wooden crate. This affects the color of the outline it gets.</summary>
WoodCrate = (1 << 3),
/// <summary>Is a steel crate. This affects the color of the outline it gets.</summary>
SteelCrate = (1 << 4),
/// <summary>Is the flag placement indicator.</summary>
Flag = (1 << 5),
/// <summary>Is a pavement type.</summary>
Pavement = (1 << 6),
/// <summary>Needs to use the special concrete pavement connection logic.</summary>
Concrete = (1 << 7),
/// <summary>Is a solid object that obstructs placement.</summary>
Solid = (1 << 8),
/// <summary>Is a crate.</summary>
Crate = WoodCrate | SteelCrate,
}

View File

@ -25,12 +25,27 @@ namespace MobiusEditor.Model
[Flags]
public enum TemplateTypeFlag
{
/// <summary>No flags set.</summary>
None = 0,
/// <summary>Used to filter out default terrain.</summary>
Clear = (1 << 0),
Water = (1 << 1),
/// <summary>This tileset is 1x1, and any additional tiles it contains are treated as randomisable alternate tiles, not as parts of a larger shape.</summary>
RandomCell = (1 << 2),
/// <summary>
/// This is a virtual tileset group, used to group together loose equivalent 1x1 tiles as randomisable. It should never actually be placed down on the map.
/// GroupTiles will contain all tiles that are part of this group.
/// </summary>
Group = RandomCell | (1 << 3),
/// <summary>
/// This tile is a 1x1 tile that has a bunch of equivalent alternates, and because of that, it is grouped in a virtual tileset group as randomisable.
/// GroupTiles will contain the group it belongs to, so picking it from the map can easily select the group it belongs to.
/// </summary>
IsGrouped = (1 << 4),
/// <summary>
/// This tileset has equivalent tilesets, and when drag-placing this, it will switch to randomly placing the alternates as well.
/// GroupTiles contains all equivalents that can be used.
/// </summary>
HasEquivalents = (1 << 5),
}
@ -200,8 +215,8 @@ namespace MobiusEditor.Model
/// <param name="iconHeight">Height in cells.</param>
/// <param name="theaters">Theaters that contain this tile.</param>
/// <param name="maskOverrides">Mask override for tiles that contain too many graphics in the Remaster. Indices with '0' are removed from the tiles. Spaces are ignored and can be added for visual separation.</param>
/// <param name="equivalentOffset"></param>
/// <param name="equivalentTiles"></param>
/// <param name="equivalentOffset">Thez position this tileset should be placed on when used as equivalent.</param>
/// <param name="equivalentTiles">Equivalent tiles that can be placed down when drag-placing multiple of this.</param>
public TemplateType(ushort id, string name, int iconWidth, int iconHeight, TheaterType[] theaters, string[] maskOverrides, Point equivalentOffset, string[] equivalentTiles)
: this(id, name, iconWidth, iconHeight, theaters, TemplateTypeFlag.None, maskOverrides)
{
@ -230,7 +245,6 @@ namespace MobiusEditor.Model
{
}
/// <summary>
/// Creates a TemplateType object.
/// </summary>
@ -244,6 +258,7 @@ namespace MobiusEditor.Model
: this(id, name, iconWidth, iconHeight, theaters, TemplateTypeFlag.None, maskOverride)
{
}
/// <summary>
/// Creates a TemplateType object.
/// </summary>
@ -293,7 +308,6 @@ namespace MobiusEditor.Model
{
return string.Equals(Name, obj as string, StringComparison.OrdinalIgnoreCase);
}
return base.Equals(obj);
}

View File

@ -23,14 +23,23 @@ namespace MobiusEditor.Model
[Flags]
public enum UnitTypeFlag
{
/// <summary>No flags set.</summary>
None = 0,
/// <summary>Is a fixed-wing airplane. This treats it as 16-frame rotation, and affects the default orders for placing it on the map.</summary>
IsFixedWing = 1 << 0,
/// <summary>Has a turret drawn on the unit.</summary>
HasTurret = 1 << 1,
/// <summary>Needs to render two turrets.</summary>
HasDoubleTurret = 1 << 2,
/// <summary>Can attack units. This affects the default orders for placing it on the map.</summary>
IsArmed = 1 << 3,
/// <summary>Can harvest resources. This affects the default orders for placing it on the map.</summary>
IsHarvester = 1 << 4,
/// <summary>Is a unit that is filtered out of the lists if expansion units are disabled.</summary>
IsExpansionUnit = 1 << 5,
/// <summary>Can show a mobile gap area-of-effect radius indicator.</summary>
IsGapGenerator = 1 << 6,
/// <summary>Can show a radar jamming area-of-effect radius indicator.</summary>
IsJammer = 1 << 7,
}

View File

@ -2314,7 +2314,7 @@ namespace MobiusEditor.RedAlert
{
return changed;
}
List<(Point p, Building b)> foundBuildings = buildings.Where(lo => bType.Name.Equals(lo.Occupier.Type.Name, StringComparison.InvariantCultureIgnoreCase))
List<(Point p, Building b)> foundBuildings = buildings.Where(lo => bType.ID == lo.Occupier.Type.ID)
.OrderBy(lo => lo.Location.Y * map.Metrics.Width + lo.Location.X).ToList();
bType.HasBib = hasBib;
foreach ((Point p, Building b) in foundBuildings)
@ -2535,7 +2535,7 @@ namespace MobiusEditor.RedAlert
}
string nameToIndexString<T>(IList<T> list, string name) => nameToIndex(list, name).ToString();
INISection teamTypesSection = ini.Sections.Add("TeamTypes");
foreach (TeamType teamType in Map.TeamTypes.OrderBy(t => t.Name.ToUpperInvariant()))
foreach (TeamType teamType in Map.TeamTypes)
{
string[] classes = teamType.Classes
.Select(c => string.Format("{0}:{1}", c.Type.Name.ToUpperInvariant(), c.Count))
@ -3586,19 +3586,19 @@ namespace MobiusEditor.RedAlert
return String.IsNullOrEmpty(name) || "<none>".Equals(name, StringComparison.OrdinalIgnoreCase);
}
public bool EvaluateBriefing(string briefing, out string message)
public string EvaluateBriefing(string briefing)
{
bool briefLenOvfl = false;
bool briefLenSplitOvfl = false;
const int cutoff = 40;
string brief = briefing.Replace('\t', ' ').Trim('\r', '\n', ' ').Replace("\r\n", "\n").Replace("\r", "\n");
int lines = brief.Count(c => c == '\n') + 1;
string briefText = (briefing ?? String.Empty).Replace('\t', ' ').Trim('\r', '\n', ' ').Replace("\r\n", "\n").Replace("\r", "\n");
int lines = briefText.Count(c => c == '\n') + 1;
briefLenOvfl = lines > 25;
if (!briefLenOvfl)
{
// split in lines of 40; that's more or less the average line length in the brief screen.
List<string> txtLines = new List<string>();
string[] briefLines = brief.Split('\n');
string[] briefLines = briefText.Split('\n');
for (int i = 0; i < briefLines.Length; ++i)
{
string line = briefLines[i].Trim();
@ -3627,7 +3627,7 @@ namespace MobiusEditor.RedAlert
briefLenSplitOvfl = txtLines.Count > 25;
}
const string warn25Lines = "Red Alert's briefing screen in the Remaster can only show 25 lines of briefing text. ";
message = null;
string message = null;
if (briefLenOvfl)
{
message = warn25Lines + "Your current briefing exceeds that.";
@ -3636,7 +3636,7 @@ namespace MobiusEditor.RedAlert
{
message = warn25Lines + "The lines average to about 40 characters per line, and when split that way, your current briefing exceeds that, meaning it will most likely not display correctly in-game.";
}
if (Globals.WriteClassicBriefing && brief.Length > maxBriefLengthClassic)
if (Globals.WriteClassicBriefing && briefText.Length > maxBriefLengthClassic)
{
if (message == null)
{
@ -3648,7 +3648,7 @@ namespace MobiusEditor.RedAlert
}
message += "Classic Red Alert briefings cannot exceed " + maxBriefLengthClassic + " characters. This includes line breaks.\n\nThis will not affect the mission when playing in the Remaster, but the briefing will be truncated when playing in the original game.";
}
return message != null;
return message;
}
private void BasicSection_PropertyChanged(object sender, PropertyChangedEventArgs e)

View File

@ -752,21 +752,22 @@ namespace MobiusEditor.Render
string turret2Name = unit.Type.HasDoubleTurret ? unit.Type.SecondTurret ?? unit.Type.Turret ?? unit.Type.Name : null;
int turretIcon = unit.Type.Name.Equals(turretName, StringComparison.OrdinalIgnoreCase) ? icon + 32 : icon;
int turret2Icon = unit.Type.Name.Equals(turret2Name, StringComparison.OrdinalIgnoreCase) ? icon + 32 : icon;
// Special frame handling
if (gameType == GameType.RedAlert)
{
if (unit.Type == RedAlert.UnitTypes.Phase)
if (unit.Type.ID == RedAlert.UnitTypes.Phase.ID)
{
// Compensate for unload frames.
turretIcon += 6;
}
else if (unit.Type == RedAlert.UnitTypes.MGG)
else if (unit.Type.ID == RedAlert.UnitTypes.MGG.ID)
{
// 16-frame rotation, but saved as 8 frames because the other 8 are identical.
turretIcon = 32 + ((icon / 2) & 7);
}
else if (unit.Type == RedAlert.UnitTypes.Tesla)
else if (unit.Type.ID == RedAlert.UnitTypes.Tesla.ID)
{
// Fixed turret frame.
// turret is an animation rather than a rotation; always take the first frame.
turretIcon = 32;
}
}
@ -874,26 +875,26 @@ namespace MobiusEditor.Render
turret2Adjust.Y += unit.Type.TurretY;
}
Point center = new Point(renderBounds.Width / 2, renderBounds.Height / 2);
if (turretTile != null) {
Size turretSize = turretTile.Image.Size;
var turretRenderSize = new Size(turretSize.Width * tileSize.Width / Globals.OriginalTileWidth, turretSize.Height * tileSize.Height / Globals.OriginalTileHeight);
var turretBounds = new Rectangle(center - new Size(turretRenderSize.Width / 2, turretRenderSize.Height / 2), turretRenderSize);
turretBounds.Offset(
turretAdjust.X * tileSize.Width / Globals.PixelWidth,
turretAdjust.Y * tileSize.Height / Globals.PixelHeight
void RenderTurret(Graphics ug, Tile turrTile, Point turrAdjust, Size tSize)
{
Size turretSize = turrTile.Image.Size;
var turretRenderSize = new Size(turretSize.Width * tSize.Width / Globals.OriginalTileWidth, turretSize.Height * tSize.Height / Globals.OriginalTileHeight);
var turrBounds = new Rectangle(center - new Size(turretRenderSize.Width / 2, turretRenderSize.Height / 2), turretRenderSize);
turrBounds.Offset(
turrAdjust.X * tSize.Width / Globals.PixelWidth,
turrAdjust.Y * tSize.Height / Globals.PixelHeight
);
unitG.DrawImage(turretTile.Image, turretBounds, 0, 0, turretTile.Image.Width, turretTile.Image.Height, GraphicsUnit.Pixel);
ug.DrawImage(turrTile.Image, turrBounds, 0, 0, turrTile.Image.Width, turrTile.Image.Height, GraphicsUnit.Pixel);
}
if (turretTile != null)
{
RenderTurret(unitG, turretTile, turretAdjust, tileSize);
}
if (unit.Type.HasDoubleTurret && turret2Tile != null)
{
Size turret2Size = turret2Tile.Image.Size;
var turret2RenderSize = new Size(turret2Size.Width * tileSize.Width / Globals.OriginalTileWidth, turret2Size.Height * tileSize.Height / Globals.OriginalTileHeight);
var turret2Bounds = new Rectangle(center - new Size(turret2RenderSize.Width / 2, turret2RenderSize.Height / 2), turret2RenderSize);
turret2Bounds.Offset(
turret2Adjust.X * tileSize.Width / Globals.PixelWidth,
turret2Adjust.Y * tileSize.Height / Globals.PixelHeight
);
unitG.DrawImage(turret2Tile.Image, turret2Bounds, 0, 0, turret2Tile.Image.Width, turret2Tile.Image.Height, GraphicsUnit.Pixel);
RenderTurret(unitG, turret2Tile, turret2Adjust, tileSize);
}
}
}
@ -1571,7 +1572,7 @@ namespace MobiusEditor.Render
{
return;
}
ITeamColor tc = building.Type.CanRemap ? Globals.TheTeamColorManager[building.House.BuildingTeamColor] : null;
ITeamColor tc = Globals.TheTeamColorManager[building.House.BuildingTeamColor];
Color circleColor = tc?.BaseColor ?? Globals.TheTeamColorManager.RemapBaseColor;
bool[,] cells = building.Type.BaseOccupyMask;
int maskY = cells.GetLength(0);
@ -1599,12 +1600,7 @@ namespace MobiusEditor.Render
{
return;
}
String teamColor;
if (!unit.House.OverrideTeamColors.TryGetValue(unit.Type.Name, out teamColor))
{
teamColor = unit.House.UnitTeamColor;
}
ITeamColor tc = Globals.TheTeamColorManager[teamColor];
ITeamColor tc = Globals.TheTeamColorManager[unit.House.BuildingTeamColor];
Color circleColor = tc?.BaseColor ?? Globals.TheTeamColorManager.RemapBaseColor;
Color alphacorr = Color.FromArgb(unit.Tint.A * 128 / 256, circleColor);
if (isJammer)

View File

@ -275,10 +275,9 @@ namespace MobiusEditor.SoleSurvivor
return info;
}
public override Boolean EvaluateBriefing(string briefing, out string message)
public override string EvaluateBriefing(string briefing)
{
message = null;
return true;
return null;
}
}
}

View File

@ -3201,21 +3201,21 @@ namespace MobiusEditor.TiberianDawn
return String.IsNullOrEmpty(name) || "None".Equals(name, StringComparison.OrdinalIgnoreCase);
}
public virtual bool EvaluateBriefing(string briefing, out string message)
public virtual string EvaluateBriefing(string briefing)
{
message = null;
string message = null;
if (!Globals.WriteClassicBriefing)
{
return true;
return null;
}
string briefText = briefing.Replace('\t', ' ').Trim('\r', '\n', ' ').Replace("\r\n", "\r").Replace("\n", "\r");
string briefText = (briefing ?? String.Empty).Replace('\t', ' ').Trim('\r', '\n', ' ').Replace("\r\n", "\n").Replace("\r", "\n");
// Remove duplicate spaces
briefText = Regex.Replace(briefText, " +", " ");
if (briefText.Length > maxBriefLengthClassic)
{
message = "Classic Tiberian Dawn briefings cannot exceed " + maxBriefLengthClassic + " characters. This includes line breaks.\n\nThis will not affect the mission when playing in the Remaster, but the briefing will be truncated when playing in the original game.";
}
return message == null;
return message;
}
protected void BasicSection_PropertyChanged(object sender, PropertyChangedEventArgs e)

View File

@ -23,7 +23,6 @@ namespace MobiusEditor.Tools.Dialogs
{
public partial class ResourcesToolDialog : ToolDialog<ResourcesTool>
{
//public Label TotalResourcesLbl => lblResTotalVal;
public Label BoundsResourcesLbl => lblResBoundsVal;
public NumericUpDown ResourceBrushSizeNud => nudBrushSize;
@ -38,7 +37,7 @@ namespace MobiusEditor.Tools.Dialogs
protected override void InitializeInternal(MapPanel mapPanel, MapLayerFlag activeLayers, ToolStripStatusLabel toolStatusLabel,
ToolTip mouseToolTip, IGamePlugin plugin, UndoRedoList<UndoRedoEventArgs> undoRedoList)
{
Tool = new ResourcesTool(mapPanel, activeLayers, toolStatusLabel, null, BoundsResourcesLbl,
Tool = new ResourcesTool(mapPanel, activeLayers, toolStatusLabel, BoundsResourcesLbl,
ResourceBrushSizeNud, GemsCheckBox, plugin, undoRedoList);
}
}

View File

@ -35,7 +35,6 @@ namespace MobiusEditor.Tools
/// </summary>
protected override MapLayerFlag ManuallyHandledLayers => MapLayerFlag.None;
private readonly Label totalResourcesLbl;
private readonly Label boundsResourcesLbl;
private readonly NumericUpDown brushSizeNud;
private readonly CheckBox gemsCheckBox;
@ -51,11 +50,10 @@ namespace MobiusEditor.Tools
private readonly Dictionary<int, Overlay> undoOverlays = new Dictionary<int, Overlay>();
private readonly Dictionary<int, Overlay> redoOverlays = new Dictionary<int, Overlay>();
public ResourcesTool(MapPanel mapPanel, MapLayerFlag layers, ToolStripStatusLabel statusLbl, Label totalResourcesLbl, Label boundsResourcesLbl,
public ResourcesTool(MapPanel mapPanel, MapLayerFlag layers, ToolStripStatusLabel statusLbl, Label boundsResourcesLbl,
NumericUpDown brushSizeNud, CheckBox gemsCheckBox, IGamePlugin plugin, UndoRedoList<UndoRedoEventArgs> url)
: base(mapPanel, layers, statusLbl, plugin, url)
{
this.totalResourcesLbl = totalResourcesLbl;
this.boundsResourcesLbl = boundsResourcesLbl;
this.brushSizeNud = brushSizeNud;
this.gemsCheckBox = gemsCheckBox;
@ -315,11 +313,7 @@ namespace MobiusEditor.Tools
private void Update()
{
if (totalResourcesLbl != null)
{
totalResourcesLbl.Text = map.TotalResources.ToString();
}
boundsResourcesLbl.Text = map.ResourcesInBounds.ToString();
boundsResourcesLbl.Text = map.ResourcesOnMap.ToString();
if (map.OverlayTypes.Any(t => t.IsGem))
{
gemsCheckBox.Visible = true;

View File

@ -379,11 +379,36 @@ namespace MobiusEditor.Utility
BitmapData data = null;
try
{
data = bitmap.LockBits(new Rectangle(0, 0, bitmap.Width, bitmap.Height), ImageLockMode.ReadWrite, bitmap.PixelFormat);
var bpp = Image.GetPixelFormatSize(data.PixelFormat) / 8;
PixelFormat dataFormat = bitmap.PixelFormat;
if (dataFormat == PixelFormat.Format24bppRgb || dataFormat == PixelFormat.Format32bppRgb)
{
return new Rectangle(0, 0, bitmap.Width, bitmap.Height);
}
List<int> transColors = null;
if ((dataFormat & PixelFormat.Indexed) == PixelFormat.Indexed)
{
dataFormat = PixelFormat.Format8bppIndexed;
Color[] entries = bitmap.Palette.Entries;
transColors = new List<int>();
for (Int32 i = 0; i < entries.Length; i++)
{
if (entries[i].A == 0)
transColors.Add(i);
}
}
data = bitmap.LockBits(new Rectangle(0, 0, bitmap.Width, bitmap.Height), ImageLockMode.ReadWrite, dataFormat);
var bytes = new byte[data.Stride * data.Height];
Marshal.Copy(data.Scan0, bytes, 0, bytes.Length);
return ImageUtils.CalculateOpaqueBounds(bytes, data.Width, data.Height, bpp, data.Stride);
if (bitmap.PixelFormat == PixelFormat.Format8bppIndexed)
{
return CalculateOpaqueBounds8bpp(bytes, data.Width, data.Height, data.Stride, transColors);
}
else
{
var bpp = Image.GetPixelFormatSize(dataFormat) / 8;
return CalculateOpaqueBoundsHiCol(bytes, data.Width, data.Height, bpp, data.Stride);
}
}
finally
{
@ -394,8 +419,22 @@ namespace MobiusEditor.Utility
}
}
public static Rectangle CalculateOpaqueBounds(byte[] data, int width, int height, int bytespp, int stride)
/// <summary>
/// Calculates the actually opaque bounds of a 24bpp or 32bpp image given as bytes.
/// </summary>
/// <param name="data">Image data.</param>
/// <param name="width">Image width.</param>
/// <param name="height">Image height.</param>
/// <param name="bytespp">Bytes per pixel.</param>
/// <param name="stride">Stride of the image.</param>
/// <returns></returns>
public static Rectangle CalculateOpaqueBoundsHiCol(byte[] data, int width, int height, int bytespp, int stride)
{
// Only handle 32bpp data.
if (bytespp != 4)
{
return new Rectangle(0, 0, width, height);
}
// Modified this function to result in (0,0,0,0) when the image is empty, rather than retaining the full size.
int lineWidth = width * bytespp;
bool isTransparentRow(int y)
@ -462,5 +501,77 @@ namespace MobiusEditor.Utility
return opaqueBounds;
}
private static Rectangle CalculateOpaqueBounds8bpp(byte[] data, int width, int height, int stride, List<int> transparentColors)
{
HashSet<int> trMap = new HashSet<int>(transparentColors);
// Only handle 32bpp data.
// Modified this function to result in (0,0,0,0) when the image is empty, rather than retaining the full size.
int lineWidth = width;
bool isTransparentRow(int y)
{
int start = y * stride;
int end = start + lineWidth;
for (var i = start; i < end; ++i)
{
if (!trMap.Contains(data[i]))
{
return false;
}
}
return true;
}
var opaqueBounds = new Rectangle(0, 0, width, height);
for (int y = height - 1; y >= 0; --y)
{
if (!isTransparentRow(y))
{
break;
}
opaqueBounds.Height = y;
}
int endHeight = opaqueBounds.Height;
for (int y = 0; y < endHeight; ++y)
{
if (!isTransparentRow(y))
{
opaqueBounds.Y = y;
opaqueBounds.Height = endHeight - y;
break;
}
}
bool isTransparentColumn(int x)
{
var pos = opaqueBounds.Top * stride + x;
for (var y = 0; y < opaqueBounds.Height; ++y)
{
if (!trMap.Contains(data[pos]))
{
return false;
}
pos += stride;
}
return true;
}
for (int x = width - 1; x >= 0; --x)
{
if (!isTransparentColumn(x))
{
break;
}
opaqueBounds.Width = x;
}
int endWidth = opaqueBounds.Width;
for (int x = 0; x < endWidth; ++x)
{
if (!isTransparentColumn(x))
{
opaqueBounds.X = x;
opaqueBounds.Width = endWidth - x;
break;
}
}
return opaqueBounds;
}
}
}

View File

@ -30,12 +30,6 @@ namespace MobiusEditor.Utility
public string Name { get; private set; }
private byte[] classicRemap;
public byte[] ClassicRemap => classicRemap ?? (Variant == null ? null : teamColorManager.GetItem(Variant)?.ClassicRemap);
private byte[] classicRemapStructures;
public byte[] ClassicRemapStructures => classicRemapStructures ?? (Variant == null ? null : teamColorManager.GetItem(Variant)?.ClassicRemapStructures);
private Color? lowerBounds;
public Color LowerBounds => lowerBounds.HasValue ? lowerBounds.Value : ((Variant != null) ? teamColorManager.GetItem(Variant).LowerBounds : default);
@ -100,8 +94,6 @@ namespace MobiusEditor.Utility
{
this.Name = col.Name;
this.Variant = col.Variant;
this.classicRemap = col.ClassicRemap;
this.classicRemapStructures = col.ClassicRemapStructures;
this.lowerBounds = col.LowerBounds;
this.upperBounds = col.UpperBounds;
this.fudge = col.Fudge;
@ -128,14 +120,6 @@ namespace MobiusEditor.Utility
this.radarMapColor = radarMapColor;
}
public void Load(string name, string variant, byte[] classicRemap, byte[] classicRemapStructures)
{
this.Name = name;
this.Variant = variant;
this.classicRemap = classicRemap;
this.classicRemapStructures = classicRemapStructures;
}
public void Load(string xml)
{
XmlDocument xmlDoc = new XmlDocument();
@ -262,17 +246,27 @@ namespace MobiusEditor.Utility
public void ApplyToImage(Bitmap image, out Rectangle opaqueBounds)
{
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);
var bytesPerPixel = Image.GetPixelFormatSize(data.PixelFormat) / 8;
var bytes = new byte[data.Stride * data.Height];
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.CalculateOpaqueBounds(bytes, width, height, bytesPerPixel, stride);
opaqueBounds = ImageUtils.CalculateOpaqueBoundsHiCol(bytes, width, height, bytesPerPixel, stride);
ApplyToImage(bytes, width, height, bytesPerPixel, stride, opaqueBounds);
Marshal.Copy(bytes, 0, data.Scan0, bytes.Length);
}
@ -287,6 +281,11 @@ namespace MobiusEditor.Utility
public void ApplyToImage(byte[] bytes, int width, int height, int bytesPerPixel, int stride, Rectangle? opaqueBounds)
{
// Only handle 24bpp and 32bpp data.
if (bytesPerPixel != 3 && bytesPerPixel != 4)
{
return;
}
Rectangle bounds = opaqueBounds ?? new Rectangle(0, 0, width, height);
float frac(float x) => x - (int)x;
float lerp(float x, float y, float t) => (x * (1.0f - t)) + (y * t);
@ -305,8 +304,9 @@ namespace MobiusEditor.Utility
for (int y = bounds.Top; y < bounds.Bottom; y++)
{
int addr = lineStart + bounds.Left * bytesPerPixel;
for (int x = bounds.Left; x < width; ++x)
for (int x = bounds.Left; x < bounds.Right; ++x)
{
// 4-byte pixel = [B,G,R,A]. This code remains the same if it's 24 bpp since the alpha byte is never processed.
var pixel = Color.FromArgb(bytes[addr + 2], bytes[addr + 1], bytes[addr + 0]);
(float r, float g, float b) = (pixel.R.ToLinear(), pixel.G.ToLinear(), pixel.B.ToLinear());
(float x, float y, float z, float w) K = (0.0f, -1.0f / 3.0f, 2.0f / 3.0f, -1.0f);

View File

@ -44,9 +44,9 @@ The file "CnCTDRAMapEditor.exe.config" contains settings to customise the editor
A mod entry can either be a Steam workshop ID, or a folder name. The paths will initially be looked up in the mods folder of the respective game in the CnCRemastered\mods\ folder under your Documents folder, but the loading system will also check the Steam workshop files for a matching mod. Sole Survivor will use Tiberian Dawn mods. Note that mods can only apply graphical changes from the tileset and house color xml files; the editor can't read any data from compiled dll files. This mods system is mostly meant to apply graphical fixes to the editor.
The **ModsToLoadTD** and **ModsToLoadSS** settings will have the `GraphicsFixesTD` mod set by default, to complete the incomplete TD Remastered graphics set, meaning the mod will automatically be loaded if found. Note that the editor has no way to check whether mods are enabled in the game, so that makes no difference.
The **ModsToLoadTD** and **ModsToLoadSS** settings will have the `GraphicsFixesTD` mod set by default, to complete the incomplete TD Remastered graphics set, meaning the mod will automatically be loaded if found. Similarly, the **ModsToLoadRA** setting will have the `GraphicsFixesRA` mod set. Note that the editor has no way to check whether mods are enabled in the game, so that makes no difference.
You can find the mod [on the Steam workshop](https://steamcommunity.com/sharedfiles/filedetails/?id=2844969675) and [on ModDB](https://www.moddb.com/games/command-conquer-remastered/addons/graphicsfixestd).
You can find them on the Steam workshop ([GraphicsFixesTD](https://steamcommunity.com/sharedfiles/filedetails/?id=2844969675), [GraphicsFixesRA](https://steamcommunity.com/sharedfiles/filedetails/?id=2978875641)) and on ModDB ([GraphicsFixesTD](https://www.moddb.com/games/command-conquer-remastered/addons/graphicsfixestd), [GraphicsFixesRA](https://www.moddb.com/games/cc-red-alert-remastered/addons/graphicsfixesra)).
### Defaults:

View File

@ -16,7 +16,7 @@ Right now, I'm not really looking into making this a joint project. Specific bug
Simply unpack the editor into a new folder on your disk somewhere. On first startup, it will automatically try to detect the folder in which the game is installed, and if it can't find it, it will show a popup asking you to locate it. Note that this autodetect only works on Steam installations of the game.
It is advised to install the `GraphicsFixesTD` and `GraphicsFixesRA` mods, to fix errors and add missing bits in the Remastered graphics. The editor will use the mods automatically when they are installed, even if they are not enabled inside the game. You can find them on the Steam workshop ([GraphicsFixesTD](https://steamcommunity.com/sharedfiles/filedetails/?id=2844969675), [GraphicsFixesRA](https://steamcommunity.com/sharedfiles/filedetails/?id=2978875641)) and on ModDB ([GraphicsFixesTD](https://www.moddb.com/games/command-conquer-remastered/addons/graphicsfixestd), [GraphicsFixesRA](https://www.moddb.com/games/command-conquer-remastered/addons/graphicsfixesra)).
It is advised to install the `GraphicsFixesTD` and `GraphicsFixesRA` mods, to fix errors and add missing bits in the Remastered graphics. The editor will use the mods automatically when they are installed, even if they are not enabled inside the game. You can find them on the Steam workshop ([GraphicsFixesTD](https://steamcommunity.com/sharedfiles/filedetails/?id=2844969675), [GraphicsFixesRA](https://steamcommunity.com/sharedfiles/filedetails/?id=2978875641)) and on ModDB ([GraphicsFixesTD](https://www.moddb.com/games/command-conquer-remastered/addons/graphicsfixestd), [GraphicsFixesRA](https://www.moddb.com/games/cc-red-alert-remastered/addons/graphicsfixesra)).
**The GraphicsFixesRA mod is not currently configured in the settings file to load automatically; this will be amended in the next release. To add it manually, edit `CnCTDRAMapEditor.exe.config` and replace the `<value />` under the "ModsToLoadRA" setting with `<value>2978875641;GraphicsFixesRA</value>`**